Compare commits

...

120 Commits

Author SHA1 Message Date
Jon Chambers
ef9a7fda9a Publish outstanding SQS operation count as a gauge. 2021-07-27 11:15:41 -04:00
Chris Eager
13447df1e0 Update validation for NotNull items in IncomingMessagesList 2021-07-27 10:39:30 -04:00
Jon Chambers
3608c5bfb0 Wait for outstanding requests to be resolved before shutting down the directory queue. 2021-07-27 10:36:53 -04:00
Jon Chambers
34dbff6786 Switch to an async SQS client. 2021-07-27 10:36:53 -04:00
Jon Chambers
a6066bfc2f Migrate DirectoryQueueTest to JUnit 5. 2021-07-27 10:36:53 -04:00
Jon Chambers
8579190cdf Consolidate account creation/directory updates into AccountsManager 2021-07-27 10:27:47 -04:00
Chris Eager
917f667229 Remove AccountController and KeysController from websocket 2021-07-26 14:27:43 -05:00
Chris Eager
317a551bdb Migrate MetricsRequestEventListenerTest to JUnit 5 2021-07-26 12:06:29 -05:00
Chris Eager
27e9271473 Add request path and user agent to unhandled exception logging 2021-07-26 12:06:29 -05:00
Fedor Indutny
11dff6c546 more controllers 2021-07-26 12:06:17 -05:00
Fedor Indutny
e6712937ca fix indent 2021-07-26 12:06:17 -05:00
Fedor Indutny
cf8887bb5a Provide more WebSocket endpoints 2021-07-26 12:06:17 -05:00
Chris Eager
696340f780 Migrate DeviceControllerTest to JUnit 5 2021-07-26 11:18:17 -05:00
Chris Eager
86ddcbaa08 Migrate CertificateControllerTest to JUnit 5 2021-07-26 11:18:17 -05:00
Chris Eager
2144d2a8d8 Migrate AttachmentControllerTest to JUnit 5 2021-07-26 11:18:17 -05:00
Chris Eager
f7af861b31 Migrate SecureStorageControllerTest to JUnit 5 2021-07-26 11:18:17 -05:00
Chris Eager
208a09b3ae Migrate RemoteConfigControllerTest to JUnit 5 2021-07-26 11:18:17 -05:00
Chris Eager
831023e41d Migrate PaymentsControllerTest to JUnit 5 2021-07-26 11:18:17 -05:00
Chris Eager
ff627793d6 Migrate DirectoryControllerTest to JUnit 5 2021-07-26 11:18:17 -05:00
Chris Eager
f971c76a99 Migrate StickerControllerTest to JUnit 5 2021-07-26 11:18:17 -05:00
Chris Eager
8f41176c76 Enable "sms" transport for +98 2021-07-26 10:40:05 -05:00
Ehren Kret
31bbbbb5e0 Raise default message TTL to 14 days 2021-07-20 14:08:08 -05:00
Jon Chambers
effcd6038d Also record dimensional metrics for circuit breakers and retries. 2021-07-19 16:56:16 -04:00
Jon Chambers
12be7d49c2 Clear one-time pre-keys on re-registration. 2021-07-19 10:05:01 -04:00
Jon Chambers
14863b575e Clear one-time pre-keys when a device is unlinked. 2021-07-19 10:05:01 -04:00
Jon Chambers
32a95f96ff Add a pessimistic locking system for operations on recently-deleted account records 2021-07-16 16:52:58 -04:00
Jon Chambers
b757d4b334 Measure how many "send message" requests are still using e164-based addressing. 2021-07-16 16:52:58 -04:00
Chris Eager
bd03d910fe Set authenticated device after updating last seen 2021-07-16 16:52:58 -04:00
Chris Eager
01ef855157 Return a non-stale account from base authenticator when last seen is updated 2021-07-16 16:52:58 -04:00
Chris Eager
817866caf3 Use fresh accounts to update in PushFeedbackProcessor 2021-07-16 16:52:58 -04:00
Chris Eager
158d65c6a7 Add optimistic locking to account updates 2021-07-16 16:52:58 -04:00
realturner
62022c7de1 Migrate AppConfig to SDK v2 to detect and use web identify token 2021-07-16 16:48:33 -04:00
Chris Eager
a824b5575d Add dynamic configuration for using DynamoDB in AccountsDatabaseCrawler 2021-07-06 13:01:24 -05:00
Jon Chambers
78819d5382 Remove expiration logic when checking token validity.
The data store will no longer return tokens that have expired, and we no longer need to check for expiration in application space.
2021-07-06 11:03:49 -04:00
Jon Chambers
d128bc782a Retire Postgres-backed pending account/device tables. 2021-07-06 11:03:49 -04:00
Chris Eager
530b2a310f Ensure active future is always completed 2021-07-02 15:05:11 -05:00
Chris Eager
d5b0d99a54 Remove unused method 2021-07-02 15:05:11 -05:00
Chris Eager
43be72d076 Add test for ManagedPeriodicWork; fix shutdown not awaiting active execution 2021-07-02 15:05:11 -05:00
Chris Eager
9558944e22 Add needsReconciliationIndexName to sample.yml 2021-07-02 15:05:11 -05:00
Chris Eager
0f6c866c8d Update imports 2021-07-02 15:05:11 -05:00
Chris Eager
bac78e9291 Switch DeletedAccountsTableCrawler metrics to a basic Metrics#summary 2021-07-02 15:05:11 -05:00
Chris Eager
c22ea78672 Add crawler to process migration retry accounts 2021-07-02 15:05:11 -05:00
Chris Eager
a85afe827d Avoid NPE by using scheduledFuture as the Gauge state object 2021-07-02 15:05:11 -05:00
Chris Eager
abaed821ec Add additional case to unit test 2021-07-02 15:05:11 -05:00
Chris Eager
6fa9dcd954 Refactor to use shared recurringJobExecutor 2021-07-02 15:05:11 -05:00
Chris Eager
819d59cd79 Update reconciliation crawler to use secondary index 2021-07-02 15:05:11 -05:00
Chris Eager
2f88f0eedb Refactor to use single threaded scheduled executor 2021-07-02 15:05:11 -05:00
Chris Eager
74ff491671 Rename ManagedPeriodicWorkCache to ManagedPeriodicWorkLock 2021-07-02 15:05:11 -05:00
Chris Eager
eac48a6617 Don’t delete accounts after reconciling 2021-07-02 15:05:11 -05:00
Chris Eager
19617c14f8 Improved logging in ManagedPeriodcWork 2021-07-02 15:05:11 -05:00
Chris Eager
fc7291c3e8 Migrate DeletedAccountsTableCrawler to ManagedPeriodicWork 2021-07-02 15:05:11 -05:00
Chris Eager
88db808298 Add abstract ManagedPeriodicWork 2021-07-02 15:05:11 -05:00
Chris Eager
5193abdab3 Add DeletedAccountsTableCrawler 2021-07-02 15:05:11 -05:00
Chris Eager
a315c9be92 Add DeletedAccounts DynamoDB table 2021-07-02 15:05:11 -05:00
Chris Eager
fc1541591a Add AbstractDynamoDbStore#scan 2021-07-02 15:05:11 -05:00
Chris Eager
ae97c4db9f Use editorconfig in AbstractDynamoDbStore 2021-07-02 15:05:11 -05:00
Chris Eager
26bc5973b5 Clear message queue before and after removing a device 2021-07-02 10:48:42 -05:00
Chris Eager
e52b8c8423 Implement DatadogConfig in DatadogConfiguration 2021-07-02 10:48:05 -05:00
Jon Chambers
7395489bac Add tests for pending account/device managers. 2021-07-02 11:30:13 -04:00
Jon Chambers
b384ed7f5c Add a counter for requests for delivery certificates with/without e164s. 2021-07-01 10:59:10 -04:00
Jon Chambers
e3afcae7d3 Gather data to verify safety of retiring legacy reglock system. 2021-07-01 10:58:47 -04:00
Jon Chambers
9faeed7b20 Count E164 authentications versus UUID authentications. 2021-07-01 10:51:34 -04:00
Jon Chambers
49adcca80e Use Optional.isEmpty(). 2021-07-01 10:51:34 -04:00
Jon Chambers
49c43a6816 Simplify distribution summary for "days since last seen." 2021-07-01 10:51:34 -04:00
Jon Chambers
84f85ae098 Collapse various account meters into a single, multi-dimensional counter. 2021-07-01 10:51:34 -04:00
Jon Chambers
3d581941ab Add plumbing and configuration to migrate pending accounts/devices to DynamoDB. 2021-07-01 10:50:52 -04:00
Jon Chambers
d2d39baede Add a DynamoDB-backed stored verification code store. 2021-07-01 10:50:52 -04:00
Jon Chambers
111f5ba024 Use java.time classes for stored verification code expiration; add tests. 2021-07-01 10:50:52 -04:00
Jon Chambers
ce3fb7fa99 Extract a common base class for verification code store tests. 2021-07-01 10:50:52 -04:00
Jon Chambers
fc421d3f21 Introduce a common interface for verification code stores. 2021-07-01 10:50:52 -04:00
Jon Chambers
71bea759c6 Consolidate StoredVerificationCode constructors. 2021-07-01 10:50:52 -04:00
Jon Chambers
bf1dd791a5 Drop caching for pending accounts/devices. 2021-07-01 10:50:52 -04:00
Chris Eager
4c99577c08 Add configuration for Datadog batch size 2021-06-30 16:44:25 -05:00
Graeme Connell
5d5c63e6d4 Update profile controller to S3 AWSv2. 2021-06-30 13:09:18 -06:00
Graeme Connell
42ff3f8432 Switch SQS to Amazon SDKv2. 2021-06-30 12:46:12 -06:00
Chris Eager
be6ef76486 Update DynamoDBLocal to 1.16.0 2021-06-23 13:50:58 -05:00
Chris Eager
bc297e6d34 Update wiremock-jre8 to 2.28.1 2021-06-23 13:50:58 -05:00
Chris Eager
3a526dcbd7 Update mockito to 3.11.1 2021-06-23 13:50:58 -05:00
Ehren Kret
7883352b74 Match random capability generation in test 2021-06-21 17:32:31 -05:00
Ehren Kret
982d122d18 Match random capability generation in test 2021-06-21 17:32:31 -05:00
Ehren Kret
d8d94407c6 Create announcement group capability 2021-06-21 17:32:31 -05:00
Chris Eager
28cfc54170 Update FunctionCounter builder to use non-null object and method 2021-06-11 11:27:45 -05:00
Jon Chambers
2ee7279743 Pause nstat counters. 2021-06-11 12:26:56 -04:00
Jon Chambers
eb1b073385 Add a hostname-aware reporter factory. 2021-06-10 14:23:05 -04:00
Jon Chambers
c634185b6f Standardize a utility method for getting local host names. 2021-06-10 14:23:05 -04:00
Ehren Kret
827a3af419 Code cleanup 2021-06-09 20:44:18 -05:00
Jon Chambers
2c33d22a30 Stop recording specific client versions in metrics until we know we need them again. 2021-06-08 12:25:31 -04:00
Chris Eager
b41ed9d810 Update sample.yml config 2021-06-07 17:21:36 -04:00
Jon Chambers
58d3a12eff Set hostname to lowercase to avoid strange case mismatch issues; log hostname failures. 2021-06-07 17:17:46 -04:00
Jon Chambers
88c4b2be97 Correct a misunderstanding about the metrics host tag. 2021-06-07 16:29:44 -04:00
Jon Chambers
6cbd57f19f Include environment/service/version as common metric tags. 2021-06-04 18:17:09 -04:00
Jon Chambers
5522376584 Include a host tag with metrics. 2021-06-04 18:17:09 -04:00
Jon Chambers
5089c37d28 Drop a pair of unused commands. 2021-06-04 12:35:06 -04:00
Jon Chambers
1ccf24e68c Add a command to check dynamic config files. 2021-06-04 12:34:48 -04:00
Jon Chambers
411f7298f2 Enforce validation constraints for dynamic configuration objects. 2021-06-04 12:34:48 -04:00
Jon Chambers
5b0214c6f2 Make pre-key take operations more null-safe 2021-06-04 11:18:59 -04:00
Jon Chambers
735573e61b Make reporting intervals configurable. 2021-06-03 17:50:41 -04:00
Graeme Connell
c545cff1b3 Switch DynamoDB to AWSv2.
Switch from using com.amazonaws.services.dynamodbv2 to using
software.amazon.awssdk.services.dynamodb for all current DynamoDB uses.
2021-06-03 13:37:10 -06:00
Jon Chambers
cbd9681e3e Configure histograms and exclude high-cardinality metrics. 2021-06-03 14:12:02 -04:00
Jon Chambers
ca876e40ca Add a second metric aggregator. 2021-06-03 14:12:02 -04:00
Jon Chambers
76f5a71727 Include server version in logging tags 2021-06-03 11:24:25 -04:00
Jon Chambers
117de2382d Verify that API consumers can skip/clear VOIP tokens. 2021-06-02 16:50:49 -05:00
Jon Chambers
25e7036451 Send a payload with mutable content for non-VOIP topics. 2021-06-02 16:50:49 -05:00
Jon Chambers
3131bd3dd9 Allow iOS callers to specify whether they're providing a VOIP token for preauth. 2021-06-02 16:50:49 -05:00
Chris Eager
1cf9397bbd Bump dropwizard to 2.0.22 2021-06-02 12:30:30 -05:00
brock-signal
c97be15e79 Fix NPE when a null message comes in from a client 2021-06-01 15:00:41 -06:00
Ehren Kret
164fc40990 Rename receipt type and add new client-to-client plaintext type for decryption error receipts 2021-05-28 11:33:44 -05:00
Ehren Kret
6456af6284 Upgrade to latest protobuf
This upgrades to protobuf 3.17 and uses maven to automatically rebuild
the generated code instead of using prefabricated checked in Java
files.
2021-05-28 11:33:44 -05:00
Chris Eager
81212cc13a Add jgitver configuration to ignore branch names 2021-05-27 14:35:28 -05:00
Ehren Kret
6f0750790c Add metric to count number of legacy messages sent 2021-05-27 11:13:42 -05:00
Chris Eager
3e61b5c49d Add call chain and mismatch check for push token timestamp 2021-05-27 11:10:58 -05:00
Ehren Kret
50c4df4f45 Add deploy phase bindings 2021-05-26 19:42:45 -05:00
Ehren Kret
1eb946f5fe Add jgitver extension 2021-05-26 19:42:45 -05:00
Ehren Kret
7bd402b48d Build refactor in preparations for bringing in jgitver 2021-05-26 19:42:42 -05:00
Chris Eager
90444d5b91 Bump version to 5.95 2021-05-26 11:11:00 -05:00
Chris Eager
5ee093f87c Add mismatch for signed pre-key; remove mismatch for migration version 2021-05-26 10:58:23 -05:00
Chris Eager
623743286c Bump version to 5.94 2021-05-25 11:00:44 -05:00
Chris Eager
67067f1d2d Remove last-seen and registration lock comparisons 2021-05-25 10:47:57 -05:00
Ehren Kret
07f9bb112e Use separate object for multi recipient response
`needsSync` was being sent back from the server in the JSON response
which is an unnecessary and constantly false field in multi-recipient
message sending endpoint as it's always sealed sender.
2021-05-25 10:30:39 -05:00
Ehren Kret
417d48c452 Block downgrading sender key support
Disallow linking an additional device to an account that has already
upgraded to having sender key support where the linked device does not
have sender key support. This should prompt the person attempting to
link the older application to upgrade in order to complete the linking
process.
2021-05-25 10:30:26 -05:00
165 changed files with 7335 additions and 9135 deletions

11
.gitignore vendored
View File

@@ -9,10 +9,13 @@ config/production.yml
config/federated.yml config/federated.yml
config/staging.yml config/staging.yml
config/testing.yml config/testing.yml
service/config/production.yml config/deploy.properties
service/config/federated.yml /service/config/production.yml
service/config/staging.yml /service/config/federated.yml
service/config/testing.yml /service/config/staging.yml
/service/config/testing.yml
/service/config/deploy.properties
/service/dependency-reduced-pom.xml
.opsmanage .opsmanage
put.sh put.sh
deployer-staging.properties deployer-staging.properties

9
.mvn/extensions.xml Normal file
View File

@@ -0,0 +1,9 @@
<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.0.0 http://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
<extension>
<groupId>fr.brouillard.oss</groupId>
<artifactId>jgitver-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>

14
.mvn/jgitver.config.xml Normal file
View File

@@ -0,0 +1,14 @@
<configuration xmlns="http://jgitver.github.io/maven/configuration/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jgitver.github.io/maven/configuration/1.1.0 https://jgitver.github.io/maven/configuration/jgitver-configuration-v1_1_0.xsd">
<useDirty>true</useDirty>
<useDefaultBranchingPolicy>false</useDefaultBranchingPolicy>
<branchPolicies>
<branchPolicy>
<pattern>(.*)</pattern>
<transformations>
<transformation>IGNORE</transformation>
</transformations>
</branchPolicy>
</branchPolicies>
</configuration>

View File

@@ -5,18 +5,15 @@
<parent> <parent>
<artifactId>TextSecureServer</artifactId> <artifactId>TextSecureServer</artifactId>
<groupId>org.whispersystems.textsecure</groupId> <groupId>org.whispersystems.textsecure</groupId>
<version>1.0</version> <version>JGITVER</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>gcm-sender-async</artifactId> <artifactId>gcm-sender-async</artifactId>
<version>${TextSecureServer.version}</version>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>io.github.resilience4j</groupId> <groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId> <artifactId>resilience4j-retry</artifactId>
<version>${resilience4j.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
@@ -44,6 +41,11 @@
<artifactId>jcl-over-slf4j</artifactId> <artifactId>jcl-over-slf4j</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

220
pom.xml
View File

@@ -4,9 +4,6 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging> <packaging>pom</packaging>
<prerequisites>
<maven>3.0.0</maven>
</prerequisites>
<repositories> <repositories>
<repository> <repository>
@@ -32,21 +29,37 @@
</modules> </modules>
<properties> <properties>
<dropwizard.version>2.0.21</dropwizard.version>
<aws.sdk.version>1.11.939</aws.sdk.version> <aws.sdk.version>1.11.939</aws.sdk.version>
<aws.sdk2.version>2.16.66</aws.sdk2.version> <aws.sdk2.version>2.16.66</aws.sdk2.version>
<mockito.version>2.25.1</mockito.version> <commons-codec.version>1.15</commons-codec.version>
<commons-csv.version>1.8</commons-csv.version>
<commons-io.version>2.9.0</commons-io.version>
<dropwizard.version>2.0.22</dropwizard.version>
<dropwizard-metrics-datadog.version>1.1.13</dropwizard-metrics-datadog.version>
<guava.version>30.1.1-jre</guava.version>
<jaxb.version>2.3.1</jaxb.version>
<jedis.version>2.9.0</jedis.version>
<lettuce.version>6.0.4.RELEASE</lettuce.version>
<libphonenumber.version>8.12.23</libphonenumber.version>
<logstash.logback.version>6.6</logstash.logback.version>
<micrometer.version>1.5.3</micrometer.version> <micrometer.version>1.5.3</micrometer.version>
<mockito.version>3.11.1</mockito.version>
<netty.version>4.1.65.Final</netty.version>
<netty.tcnative-boringssl-static.version>2.0.39.Final</netty.tcnative-boringssl-static.version>
<opentest4j.version>1.2.0</opentest4j.version>
<postgresql.version>9.4-1201-jdbc41</postgresql.version>
<protobuf.version>3.17.1</protobuf.version>
<pushy.version>0.14.2</pushy.version> <pushy.version>0.14.2</pushy.version>
<resilience4j.version>1.5.0</resilience4j.version> <resilience4j.version>1.5.0</resilience4j.version>
<semver4j.version>3.1.0</semver4j.version>
<slf4j.version>1.7.30</slf4j.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<TextSecureServer.version>5.93</TextSecureServer.version>
</properties> </properties>
<groupId>org.whispersystems.textsecure</groupId> <groupId>org.whispersystems.textsecure</groupId>
<artifactId>TextSecureServer</artifactId> <artifactId>TextSecureServer</artifactId>
<version>1.0</version> <version>JGITVER</version>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
@@ -57,6 +70,13 @@
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-bom</artifactId>
<version>${netty.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency> <dependency>
<groupId>com.amazonaws</groupId> <groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-bom</artifactId> <artifactId>aws-java-sdk-bom</artifactId>
@@ -85,6 +105,124 @@
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
<dependency>
<groupId>com.eatthepath</groupId>
<artifactId>pushy</artifactId>
<version>${pushy.version}</version>
</dependency>
<dependency>
<groupId>com.eatthepath</groupId>
<artifactId>pushy-dropwizard-metrics-listener</artifactId>
<version>${pushy.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
<version>${libphonenumber.version}</version>
</dependency>
<dependency>
<groupId>com.vdurmont</groupId>
<artifactId>semver4j</artifactId>
<version>${semver4j.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>${lettuce.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
<version>${netty.tcnative-boringssl-static.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb.version}</version>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>${logstash.logback.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>${commons-csv.version}</version>
</dependency>
<dependency>
<groupId>org.coursera</groupId>
<artifactId>dropwizard-metrics-datadog</artifactId>
<version>${dropwizard-metrics-datadog.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>${jaxb.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.opentest4j</groupId>
<artifactId>opentest4j</artifactId>
<version>${opentest4j.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
@@ -98,7 +236,7 @@
<dependency> <dependency>
<groupId>com.github.tomakehurst</groupId> <groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId> <artifactId>wiremock-jre8</artifactId>
<version>2.27.2</version> <version>2.28.1</version>
<scope>test</scope> <scope>test</scope>
<exclusions> <exclusions>
<exclusion> <exclusion>
@@ -131,20 +269,46 @@
</dependencies> </dependencies>
<build> <build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.0</version>
</extension>
</extensions>
<plugins> <plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.17.2:exe:${os.detected.classifier}</protocArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version> <version>3.8.1</version>
<configuration> <configuration>
<source>11</source> <source>11</source>
<target>11</target> <target>11</target>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>3.1.1</version> <version>3.2.0</version>
<configuration> <configuration>
<archive> <archive>
<manifest> <manifest>
@@ -191,11 +355,39 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId> <artifactId>maven-enforcer-plugin</artifactId>
<version>1.4.1</version> <version>3.0.0-M3</version>
<executions>
<execution>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<dependencyConvergence/>
<requireMavenVersion>
<version>3.0.0</version>
</requireMavenVersion>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>3.0.0-M1</version>
<configuration> <configuration>
<rules> <skip>true</skip>
<dependencyConvergence/> </configuration>
</rules> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.0.0-M1</version>
<configuration>
<skip>true</skip>
</configuration> </configuration>
</plugin> </plugin>

View File

@@ -5,12 +5,10 @@
<parent> <parent>
<artifactId>TextSecureServer</artifactId> <artifactId>TextSecureServer</artifactId>
<groupId>org.whispersystems.textsecure</groupId> <groupId>org.whispersystems.textsecure</groupId>
<version>1.0</version> <version>JGITVER</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>redis-dispatch</artifactId> <artifactId>redis-dispatch</artifactId>
<version>${TextSecureServer.version}</version>
<dependencies> <dependencies>
<dependency> <dependency>

View File

@@ -5,7 +5,7 @@
package org.whispersystems.dispatch; package org.whispersystems.dispatch;
public interface DispatchChannel { public interface DispatchChannel {
public void onDispatchMessage(String channel, byte[] message); void onDispatchMessage(String channel, byte[] message);
public void onDispatchSubscribed(String channel); void onDispatchSubscribed(String channel);
public void onDispatchUnsubscribed(String channel); void onDispatchUnsubscribed(String channel);
} }

View File

@@ -59,9 +59,7 @@ public class DispatchManager extends Thread {
logger.warn("Subscription error", e); logger.warn("Subscription error", e);
} }
if (previous.isPresent()) { previous.ifPresent(channel -> dispatchUnsubscription(name, channel));
dispatchUnsubscription(name, previous.get());
}
} }
public synchronized void unsubscribe(String name, DispatchChannel channel) { public synchronized void unsubscribe(String name, DispatchChannel channel) {
@@ -132,46 +130,28 @@ public class DispatchManager extends Thread {
} }
private void resubscribeAll() { private void resubscribeAll() {
new Thread() { new Thread(() -> {
@Override synchronized (DispatchManager.this) {
public void run() { try {
synchronized (DispatchManager.this) { for (String name : subscriptions.keySet()) {
try { pubSubConnection.subscribe(name);
for (String name : subscriptions.keySet()) {
pubSubConnection.subscribe(name);
}
} catch (IOException e) {
logger.warn("***** RESUBSCRIPTION ERROR *****", e);
} }
} catch (IOException e) {
logger.warn("***** RESUBSCRIPTION ERROR *****", e);
} }
} }
}.start(); }).start();
} }
private void dispatchMessage(final String name, final DispatchChannel channel, final byte[] message) { private void dispatchMessage(final String name, final DispatchChannel channel, final byte[] message) {
executor.execute(new Runnable() { executor.execute(() -> channel.onDispatchMessage(name, message));
@Override
public void run() {
channel.onDispatchMessage(name, message);
}
});
} }
private void dispatchSubscription(final String name, final DispatchChannel channel) { private void dispatchSubscription(final String name, final DispatchChannel channel) {
executor.execute(new Runnable() { executor.execute(() -> channel.onDispatchSubscribed(name));
@Override
public void run() {
channel.onDispatchSubscribed(name);
}
});
} }
private void dispatchUnsubscription(final String name, final DispatchChannel channel) { private void dispatchUnsubscription(final String name, final DispatchChannel channel) {
executor.execute(new Runnable() { executor.execute(() -> channel.onDispatchUnsubscribed(name));
@Override
public void run() {
channel.onDispatchUnsubscribed(name);
}
});
} }
} }

View File

@@ -8,6 +8,6 @@ import org.whispersystems.dispatch.redis.PubSubConnection;
public interface RedisPubSubConnectionFactory { public interface RedisPubSubConnectionFactory {
public PubSubConnection connect(); PubSubConnection connect();
} }

View File

@@ -1,6 +1,6 @@
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0" <assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd"> xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
<id>bin</id> <id>bin</id>
<includeBaseDirectory>false</includeBaseDirectory> <includeBaseDirectory>false</includeBaseDirectory>
<formats> <formats>
@@ -18,8 +18,8 @@
<directory>${project.build.directory}</directory> <directory>${project.build.directory}</directory>
<outputDirectory>/</outputDirectory> <outputDirectory>/</outputDirectory>
<includes> <includes>
<include>${parent.artifactId}-${TextSecureServer.version}.jar</include> <include>${parent.artifactId}-${project.version}.jar</include>
</includes> </includes>
</fileSet> </fileSet>
</fileSets> </fileSets>
</assembly> </assembly>

View File

@@ -21,9 +21,6 @@ twilio: # Twilio gateway configuration
push: push:
queueSize: # Size of push pending queue queueSize: # Size of push pending queue
redphone:
authKey: # Deprecated
turn: # TURN server configuration turn: # TURN server configuration
secret: # TURN server secret secret: # TURN server secret
uris: uris:
@@ -36,6 +33,23 @@ cacheCluster: # Redis server configuration for cache cluster
urls: urls:
- redis://redis.example.com:6379/ - redis://redis.example.com:6379/
clientPresenceCluster: # Redis server configuration for client presence cluster
urls:
- redis://redis.example.com:6379/
pubsub: # Redis server configuration for pubsub cluster
url: redis://redis.example.com:6379/
replicaUrls:
- redis://redis.example.com:6379/
pushSchedulerCluster: # Redis server configuration for push scheduler cluster
urls:
- redis://redis.example.com:6379/
rateLimitersCluster: # Redis server configuration for rate limiters cluster
urls:
- redis://redis.example.com:6379/
directory: directory:
client: # Configuration for interfacing with Contact Discovery Service cluster client: # Configuration for interfacing with Contact Discovery Service cluster
userAuthenticationTokenSharedSecret: # hex-encoded secret shared with CDS used to generate auth tokens for Signal users userAuthenticationTokenSharedSecret: # hex-encoded secret shared with CDS used to generate auth tokens for Signal users
@@ -43,13 +57,13 @@ directory:
sqs: sqs:
accessKey: # AWS SQS accessKey accessKey: # AWS SQS accessKey
accessSecret: # AWS SQS accessSecret accessSecret: # AWS SQS accessSecret
queueUrl: # AWS SQS queue url queueUrls: # AWS SQS queue urls
server: - https://sqs.example.com/directory.fifo
replicationUrl: # CDS replication endpoint base url server: # One or more CDS servers
replicationPassword: # CDS replication endpoint password - replicationName: # CDS replication name
replicationCaCertificate: # CDS replication endpoint TLS certificate trust root replicationUrl: # CDS replication endpoint base url
reconciliationChunkSize: # CDS reconciliation chunk size replicationPassword: # CDS replication endpoint password
reconciliationChunkIntervalMs: # CDS reconciliation chunk interval, in milliseconds replicationCaCertificate: # CDS replication endpoint TLS certificate trust root
messageCache: # Redis server configuration for message store cache messageCache: # Redis server configuration for message store cache
persistDelayMinutes: persistDelayMinutes:
@@ -58,16 +72,44 @@ messageCache: # Redis server configuration for message store cache
urls: urls:
- redis://redis.example.com:6379/ - redis://redis.example.com:6379/
messageStore: # Postgresql database configuration for message store
driverClass: org.postgresql.Driver
user:
password:
url:
metricsCluster: metricsCluster:
urls: urls:
- redis://redis.example.com:6379/ - redis://redis.example.com:6379/
messageDynamoDb: # DynamoDB table configuration
region:
tableName:
keysDynamoDb: # DynamoDB table configuration
region:
tableName:
accountsDynamoDb: # DynamoDB table configuration
region:
tableName:
phoneNumberTableName:
deletedAccountsDynamoDb: # DynamoDb table configuration
region:
tableName:
needsReconciliationIndexName:
migrationDeletedAccountsDynamoDb: # DynamoDB table configuration
region:
tableName:
migrationRetryAccountsDynamoDb: # DynamoDB table configuration
region:
tableName:
pushChallengeDynamoDb: # DynamoDB table configuration
region:
tableName:
reportMessageDynamoDb: # DynamoDB table configuration
region:
tableName:
awsAttachments: # AWS S3 configuration awsAttachments: # AWS S3 configuration
accessKey: accessKey:
accessSecret: accessSecret:
@@ -81,18 +123,22 @@ gcpAttachments: # GCP Storage configuration
pathPrefix: pathPrefix:
rsaSigningKey: rsaSigningKey:
profiles: # AWS S3 configuration abuseDatabase: # Postgresql database configuration
accessKey:
accessSecret:
bucket:
region:
database: # Postgresql database configuration
driverClass: org.postgresql.Driver driverClass: org.postgresql.Driver
user: user:
password: password:
url: url:
accountsDatabase: # Postgresql database configuration
driverClass: org.postgresql.Driver
user:
password:
url:
accountDatabaseCrawler:
chunkSize: # accounts per run
chunkIntervalMs: # time per run
apn: # Apple Push Notifications configuration apn: # Apple Push Notifications configuration
sandbox: true sandbox: true
bundleId: bundleId:
@@ -104,11 +150,52 @@ gcm: # GCM Configuration
senderId: senderId:
apiKey: apiKey:
micrometer: # Micrometer metrics config cdn:
- name: "example" accessKey: # AWS Access Key ID
- uri: "https://metrics.example.com/" accessSecret: # AWS Access Secret
- apiKey: bucket: # S3 Bucket name
- accountId: region: # AWS region
wavefront: # Wavefront micrometer metrics config
uri: # Wavefront proxy endpoint
batchSize: # Number of measurements to send per request
datadog:
apiKey:
environment:
unidentifiedDelivery:
certificate:
privateKey:
expiresDays:
voiceVerification:
url: https://cdn-ca.signal.org/verification/
locales:
- en
recaptcha:
secret:
storageService:
uri:
userAuthenticationTokenSharedSecret:
storageCaCertificate:
backupService:
uri:
userAuthenticationTokenSharedSecret:
backupCaCertificate:
zkConfig:
serverPublic:
serverSecret:
enabled:
appConfig:
application:
environment:
configuration:
remoteConfig: remoteConfig:
authorizedTokens: authorizedTokens:
@@ -118,9 +205,22 @@ remoteConfig:
- # Nth authorized token - # Nth authorized token
globalConfig: # keys and values that are given to clients on GET /v1/config globalConfig: # keys and values that are given to clients on GET /v1/config
paymentService:
paymentsService:
userAuthenticationTokenSharedSecret: # hex-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users userAuthenticationTokenSharedSecret: # hex-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
torExitNodeList:
s3Region:
s3Bucket:
objectKey:
maxSize:
asnTable:
s3Region:
s3Bucket:
objectKey:
maxSize:
donation: donation:
uri: # value uri: # value
apiKey: # value apiKey: # value

View File

@@ -5,16 +5,10 @@
<parent> <parent>
<artifactId>TextSecureServer</artifactId> <artifactId>TextSecureServer</artifactId>
<groupId>org.whispersystems.textsecure</groupId> <groupId>org.whispersystems.textsecure</groupId>
<version>1.0</version> <version>JGITVER</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>service</artifactId> <artifactId>service</artifactId>
<version>${TextSecureServer.version}</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies> <dependencies>
<dependency> <dependency>
@@ -33,29 +27,28 @@
<dependency> <dependency>
<groupId>org.whispersystems.textsecure</groupId> <groupId>org.whispersystems.textsecure</groupId>
<artifactId>redis-dispatch</artifactId> <artifactId>redis-dispatch</artifactId>
<version>${TextSecureServer.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.whispersystems.textsecure</groupId> <groupId>org.whispersystems.textsecure</groupId>
<artifactId>websocket-resources</artifactId> <artifactId>websocket-resources</artifactId>
<version>${TextSecureServer.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.whispersystems.textsecure</groupId> <groupId>org.whispersystems.textsecure</groupId>
<artifactId>gcm-sender-async</artifactId> <artifactId>gcm-sender-async</artifactId>
<version>${TextSecureServer.version}</version> <version>${project.version}</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.signal</groupId> <groupId>org.signal</groupId>
<artifactId>zkgroup-java</artifactId> <artifactId>zkgroup-java</artifactId>
<version>0.7.0</version> <version>0.7.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.whispersystems</groupId>
<artifactId>curve25519-java</artifactId>
<version>0.5.0</version>
</dependency>
<dependency> <dependency>
<groupId>io.dropwizard</groupId> <groupId>io.dropwizard</groupId>
@@ -134,7 +127,6 @@
<dependency> <dependency>
<groupId>net.logstash.logback</groupId> <groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId> <artifactId>logstash-logback-encoder</artifactId>
<version>6.6</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -178,8 +170,6 @@
<dependency> <dependency>
<groupId>org.glassfish.jaxb</groupId> <groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId> <artifactId>jaxb-runtime</artifactId>
<version>2.3.1</version>
<scope>runtime</scope>
</dependency> </dependency>
<dependency> <dependency>
@@ -204,17 +194,10 @@
<dependency> <dependency>
<groupId>commons-codec</groupId> <groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId> <artifactId>commons-codec</artifactId>
<version>1.13</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId> <artifactId>commons-csv</artifactId>
<version>1.8</version>
</dependency>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.2</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -225,12 +208,6 @@
<dependency> <dependency>
<groupId>io.github.resilience4j</groupId> <groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId> <artifactId>resilience4j-circuitbreaker</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.github.resilience4j</groupId> <groupId>io.github.resilience4j</groupId>
@@ -245,7 +222,14 @@
<groupId>io.micrometer</groupId> <groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-wavefront</artifactId> <artifactId>micrometer-registry-wavefront</artifactId>
</dependency> </dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-datadog</artifactId>
</dependency>
<dependency>
<groupId>org.coursera</groupId>
<artifactId>dropwizard-metrics-datadog</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId> <artifactId>jackson-core</artifactId>
@@ -271,10 +255,26 @@
<artifactId>jackson-jaxrs-json-provider</artifactId> <artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency> </dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sts</artifactId>
</dependency>
<dependency> <dependency>
<groupId>software.amazon.awssdk</groupId> <groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId> <artifactId>s3</artifactId>
</dependency> </dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sqs</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>appconfig</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.amazonaws</groupId> <groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-core</artifactId> <artifactId>aws-java-sdk-core</artifactId>
@@ -285,90 +285,65 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.amazonaws</groupId> <groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sqs</artifactId> <artifactId>dynamodb-lock-client</artifactId>
</dependency> <version>1.1.0</version>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-appconfig</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-dynamodb</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.4-1201-jdbc41</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.whispersystems</groupId>
<artifactId>curve25519-java</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>com.eatthepath</groupId>
<artifactId>pushy</artifactId>
<version>${pushy.version}</version>
</dependency>
<dependency>
<groupId>com.eatthepath</groupId>
<artifactId>pushy-dropwizard-metrics-listener</artifactId>
<version>${pushy.version}</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>io.dropwizard.metrics</groupId> <groupId>commons-logging</groupId>
<artifactId>metrics-core</artifactId> <artifactId>commons-logging</artifactId>
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.eatthepath</groupId>
<artifactId>pushy</artifactId>
</dependency>
<dependency>
<groupId>com.eatthepath</groupId>
<artifactId>pushy-dropwizard-metrics-listener</artifactId>
</dependency>
<dependency> <dependency>
<groupId>io.netty</groupId> <groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId> <artifactId>netty-tcnative-boringssl-static</artifactId>
<version>2.0.34.Final</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.vdurmont</groupId> <groupId>com.vdurmont</groupId>
<artifactId>semver4j</artifactId> <artifactId>semver4j</artifactId>
<version>3.1.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.protobuf</groupId> <groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId> <artifactId>protobuf-java</artifactId>
<version>2.6.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.googlecode.libphonenumber</groupId> <groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId> <artifactId>libphonenumber</artifactId>
<version>8.12.21</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -450,7 +425,7 @@
<dependency> <dependency>
<groupId>com.amazonaws</groupId> <groupId>com.amazonaws</groupId>
<artifactId>DynamoDBLocal</artifactId> <artifactId>DynamoDBLocal</artifactId>
<version>1.13.6</version> <version>1.16.0</version>
<scope>test</scope> <scope>test</scope>
<exclusions> <exclusions>
<exclusion> <exclusion>
@@ -470,12 +445,12 @@
<build> <build>
<finalName>${parent.artifactId}-${TextSecureServer.version}</finalName> <finalName>${project.parent.artifactId}-${project.version}</finalName>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>1.6</version> <version>3.2.4</version>
<configuration> <configuration>
<createDependencyReducedPom>true</createDependencyReducedPom> <createDependencyReducedPom>true</createDependencyReducedPom>
<filters> <filters>
@@ -508,8 +483,9 @@
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId> <artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version> <version>3.3.0</version>
<configuration> <configuration>
<descriptors> <descriptors>
<descriptor>assembly.xml</descriptor> <descriptor>assembly.xml</descriptor>
@@ -525,6 +501,58 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>properties-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<id>read-deploy-configuration</id>
<phase>deploy</phase>
<goals>
<goal>read-project-properties</goal>
</goals>
<configuration>
<files>${project.basedir}/config/deploy.properties</files>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.bazaarvoice.maven.plugins</groupId>
<artifactId>s3-upload-maven-plugin</artifactId>
<version>1.5</version>
<configuration>
<source>${project.build.directory}/${project.build.finalName}-bin.tar.gz</source>
<bucketName>${deploy.bucketName}</bucketName>
<destination>${project.build.finalName}-bin.tar.gz</destination>
</configuration>
<executions>
<execution>
<id>deploy-to-s3</id>
<phase>deploy</phase>
<goals>
<goal>s3-upload</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>templating-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<id>filter-src</id>
<goals>
<goal>filter-sources</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins> </plugins>
</build> </build>
</project> </project>

View File

@@ -1,3 +0,0 @@
all:
protoc --java_out=../src/main/java/ TextSecure.proto PubSubMessage.proto

View File

@@ -0,0 +1,15 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm;
public class WhisperServerVersion {
private static final String VERSION = "${project.version}";
public static String getServerVersion() {
return VERSION;
}
}

View File

@@ -21,6 +21,8 @@ import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration;
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration; import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration; import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration; 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.DirectoryConfiguration;
import org.whispersystems.textsecuregcm.configuration.DonationConfiguration; import org.whispersystems.textsecuregcm.configuration.DonationConfiguration;
import org.whispersystems.textsecuregcm.configuration.DynamoDbConfiguration; import org.whispersystems.textsecuregcm.configuration.DynamoDbConfiguration;
@@ -29,7 +31,7 @@ import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguratio
import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration; import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
import org.whispersystems.textsecuregcm.configuration.MessageCacheConfiguration; import org.whispersystems.textsecuregcm.configuration.MessageCacheConfiguration;
import org.whispersystems.textsecuregcm.configuration.MessageDynamoDbConfiguration; import org.whispersystems.textsecuregcm.configuration.MessageDynamoDbConfiguration;
import org.whispersystems.textsecuregcm.configuration.MicrometerConfiguration; import org.whispersystems.textsecuregcm.configuration.WavefrontConfiguration;
import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration; import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.PushConfiguration; import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration; import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
@@ -79,7 +81,12 @@ public class WhisperServerConfiguration extends Configuration {
@NotNull @NotNull
@Valid @Valid
@JsonProperty @JsonProperty
private MicrometerConfiguration micrometer; private WavefrontConfiguration wavefront;
@NotNull
@Valid
@JsonProperty
private DatadogConfiguration datadog;
@NotNull @NotNull
@Valid @Valid
@@ -151,6 +158,16 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty @JsonProperty
private DynamoDbConfiguration migrationRetryAccountsDynamoDb; private DynamoDbConfiguration migrationRetryAccountsDynamoDb;
@Valid
@NotNull
@JsonProperty
private DeletedAccountsDynamoDbConfiguration deletedAccountsDynamoDb;
@Valid
@NotNull
@JsonProperty
private DynamoDbConfiguration deletedAccountsLockDynamoDb;
@Valid @Valid
@NotNull @NotNull
@JsonProperty @JsonProperty
@@ -161,6 +178,16 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty @JsonProperty
private DynamoDbConfiguration reportMessageDynamoDb; private DynamoDbConfiguration reportMessageDynamoDb;
@Valid
@NotNull
@JsonProperty
private DynamoDbConfiguration pendingAccountsDynamoDb;
@Valid
@NotNull
@JsonProperty
private DynamoDbConfiguration pendingDevicesDynamoDb;
@Valid @Valid
@NotNull @NotNull
@JsonProperty @JsonProperty
@@ -365,6 +392,14 @@ public class WhisperServerConfiguration extends Configuration {
return migrationRetryAccountsDynamoDb; return migrationRetryAccountsDynamoDb;
} }
public DeletedAccountsDynamoDbConfiguration getDeletedAccountsDynamoDbConfiguration() {
return deletedAccountsDynamoDb;
}
public DynamoDbConfiguration getDeletedAccountsLockDynamoDbConfiguration() {
return deletedAccountsLockDynamoDb;
}
public DatabaseConfiguration getAbuseDatabaseConfiguration() { public DatabaseConfiguration getAbuseDatabaseConfiguration() {
return abuseDatabase; return abuseDatabase;
} }
@@ -393,8 +428,12 @@ public class WhisperServerConfiguration extends Configuration {
return cdn; return cdn;
} }
public MicrometerConfiguration getMicrometerConfiguration() { public WavefrontConfiguration getWavefrontConfiguration() {
return micrometer; return wavefront;
}
public DatadogConfiguration getDatadogConfiguration() {
return datadog;
} }
public UnidentifiedDeliveryConfiguration getDeliveryCertificate() { public UnidentifiedDeliveryConfiguration getDeliveryCertificate() {
@@ -455,6 +494,14 @@ public class WhisperServerConfiguration extends Configuration {
return reportMessageDynamoDb; return reportMessageDynamoDb;
} }
public DynamoDbConfiguration getPendingAccountsDynamoDbConfiguration() {
return pendingAccountsDynamoDb;
}
public DynamoDbConfiguration getPendingDevicesDynamoDbConfiguration() {
return pendingDevicesDynamoDb;
}
public MonitoredS3ObjectConfiguration getTorExitNodeListConfiguration() { public MonitoredS3ObjectConfiguration getTorExitNodeListConfiguration() {
return torExitNodeList; return torExitNodeList;
} }

View File

@@ -7,18 +7,9 @@ package org.whispersystems.textsecuregcm;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import com.amazonaws.ClientConfiguration; import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.auth.InstanceProfileCredentialsProvider; import com.amazonaws.auth.InstanceProfileCredentialsProvider;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsync;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsyncClientBuilder;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.jdbi3.strategies.DefaultNameStrategy; import com.codahale.metrics.jdbi3.strategies.DefaultNameStrategy;
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonAutoDetect;
@@ -39,8 +30,12 @@ import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment; import io.dropwizard.setup.Environment;
import io.lettuce.core.resource.ClientResources; import io.lettuce.core.resource.ClientResources;
import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.Meter.Id;
import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
import io.micrometer.datadog.DatadogMeterRegistry;
import io.micrometer.wavefront.WavefrontConfig; import io.micrometer.wavefront.WavefrontConfig;
import io.micrometer.wavefront.WavefrontMeterRegistry; import io.micrometer.wavefront.WavefrontMeterRegistry;
import java.net.http.HttpClient; import java.net.http.HttpClient;
@@ -65,6 +60,8 @@ import org.jdbi.v3.core.Jdbi;
import org.signal.zkgroup.ServerSecretParams; import org.signal.zkgroup.ServerSecretParams;
import org.signal.zkgroup.auth.ServerZkAuthOperations; import org.signal.zkgroup.auth.ServerZkAuthOperations;
import org.signal.zkgroup.profiles.ServerZkProfileOperations; import org.signal.zkgroup.profiles.ServerZkProfileOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dispatch.DispatchManager; import org.whispersystems.dispatch.DispatchManager;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.CertificateGenerator; import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
@@ -119,9 +116,9 @@ import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge;
import org.whispersystems.textsecuregcm.metrics.GarbageCollectionGauges; import org.whispersystems.textsecuregcm.metrics.GarbageCollectionGauges;
import org.whispersystems.textsecuregcm.metrics.MaxFileDescriptorGauge; import org.whispersystems.textsecuregcm.metrics.MaxFileDescriptorGauge;
import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener; import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener;
import org.whispersystems.textsecuregcm.metrics.MetricsRequestEventListener;
import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge; import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge; import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
import org.whispersystems.textsecuregcm.metrics.NstatCounters;
import org.whispersystems.textsecuregcm.metrics.OperatingSystemMemoryGauge; import org.whispersystems.textsecuregcm.metrics.OperatingSystemMemoryGauge;
import org.whispersystems.textsecuregcm.metrics.PushLatencyManager; import org.whispersystems.textsecuregcm.metrics.PushLatencyManager;
import org.whispersystems.textsecuregcm.metrics.TrafficSource; import org.whispersystems.textsecuregcm.metrics.TrafficSource;
@@ -158,6 +155,10 @@ import org.whispersystems.textsecuregcm.storage.AccountsDynamoDb;
import org.whispersystems.textsecuregcm.storage.AccountsDynamoDbMigrator; import org.whispersystems.textsecuregcm.storage.AccountsDynamoDbMigrator;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ActiveUserCounter; import org.whispersystems.textsecuregcm.storage.ActiveUserCounter;
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsDirectoryReconciler;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsTableCrawler;
import org.whispersystems.textsecuregcm.storage.DirectoryReconciler; import org.whispersystems.textsecuregcm.storage.DirectoryReconciler;
import org.whispersystems.textsecuregcm.storage.DirectoryReconciliationClient; import org.whispersystems.textsecuregcm.storage.DirectoryReconciliationClient;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
@@ -169,10 +170,7 @@ import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.MigrationDeletedAccounts; import org.whispersystems.textsecuregcm.storage.MigrationDeletedAccounts;
import org.whispersystems.textsecuregcm.storage.MigrationRetryAccounts; import org.whispersystems.textsecuregcm.storage.MigrationRetryAccounts;
import org.whispersystems.textsecuregcm.storage.PendingAccounts; import org.whispersystems.textsecuregcm.storage.MigrationRetryAccountsTableCrawler;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.storage.PendingDevices;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.storage.Profiles; import org.whispersystems.textsecuregcm.storage.Profiles;
import org.whispersystems.textsecuregcm.storage.ProfilesManager; import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.PubSubManager; import org.whispersystems.textsecuregcm.storage.PubSubManager;
@@ -184,36 +182,49 @@ import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb; import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
import org.whispersystems.textsecuregcm.storage.ReportMessageManager; import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
import org.whispersystems.textsecuregcm.storage.ReservedUsernames; import org.whispersystems.textsecuregcm.storage.ReservedUsernames;
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
import org.whispersystems.textsecuregcm.storage.Usernames; import org.whispersystems.textsecuregcm.storage.Usernames;
import org.whispersystems.textsecuregcm.storage.UsernamesManager; import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.storage.VerificationCodeStore;
import org.whispersystems.textsecuregcm.util.AsnManager; import org.whispersystems.textsecuregcm.util.AsnManager;
import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.DynamoDbFromConfig;
import org.whispersystems.textsecuregcm.util.HostnameUtil;
import org.whispersystems.textsecuregcm.util.TorExitNodeManager; import org.whispersystems.textsecuregcm.util.TorExitNodeManager;
import org.whispersystems.textsecuregcm.util.logging.LoggingUnhandledExceptionMapper;
import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener; import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler; import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler;
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener; import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator; import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
import org.whispersystems.textsecuregcm.workers.CertificateCommand; import org.whispersystems.textsecuregcm.workers.CertificateCommand;
import org.whispersystems.textsecuregcm.workers.CheckDynamicConfigurationCommand;
import org.whispersystems.textsecuregcm.workers.DeleteUserCommand; import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
import org.whispersystems.textsecuregcm.workers.GetRedisCommandStatsCommand; import org.whispersystems.textsecuregcm.workers.ServerVersionCommand;
import org.whispersystems.textsecuregcm.workers.GetRedisSlowlogCommand;
import org.whispersystems.textsecuregcm.workers.SetCrawlerAccelerationTask; import org.whispersystems.textsecuregcm.workers.SetCrawlerAccelerationTask;
import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask; import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask;
import org.whispersystems.textsecuregcm.workers.VacuumCommand; import org.whispersystems.textsecuregcm.workers.VacuumCommand;
import org.whispersystems.textsecuregcm.workers.ZkParamsCommand; import org.whispersystems.textsecuregcm.workers.ZkParamsCommand;
import org.whispersystems.websocket.WebSocketResourceProviderFactory; import org.whispersystems.websocket.WebSocketResourceProviderFactory;
import org.whispersystems.websocket.setup.WebSocketEnvironment; import org.whispersystems.websocket.setup.WebSocketEnvironment;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.s3.S3Client;
public class WhisperServerService extends Application<WhisperServerConfiguration> { public class WhisperServerService extends Application<WhisperServerConfiguration> {
private static final Logger log = LoggerFactory.getLogger(WhisperServerService.class);
@Override @Override
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) { public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
bootstrap.addCommand(new VacuumCommand()); bootstrap.addCommand(new VacuumCommand());
bootstrap.addCommand(new DeleteUserCommand()); bootstrap.addCommand(new DeleteUserCommand());
bootstrap.addCommand(new CertificateCommand()); bootstrap.addCommand(new CertificateCommand());
bootstrap.addCommand(new ZkParamsCommand()); bootstrap.addCommand(new ZkParamsCommand());
bootstrap.addCommand(new GetRedisSlowlogCommand()); bootstrap.addCommand(new ServerVersionCommand());
bootstrap.addCommand(new GetRedisCommandStatsCommand()); bootstrap.addCommand(new CheckDynamicConfigurationCommand());
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") { bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") {
@Override @Override
@@ -239,8 +250,13 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
@Override @Override
public void run(WhisperServerConfiguration config, Environment environment) public void run(WhisperServerConfiguration config, Environment environment)
throws Exception { throws Exception {
SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics()); SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics());
final DistributionStatisticConfig defaultDistributionStatisticConfig = DistributionStatisticConfig.builder()
.percentiles(.75, .95, .99, .999)
.build();
final WavefrontConfig wavefrontConfig = new WavefrontConfig() { final WavefrontConfig wavefrontConfig = new WavefrontConfig() {
@Override @Override
public String get(final String key) { public String get(final String key) {
@@ -249,25 +265,45 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
@Override @Override
public String uri() { public String uri() {
return config.getMicrometerConfiguration().getUri(); return config.getWavefrontConfiguration().getUri();
} }
@Override @Override
public int batchSize() { public int batchSize() {
return config.getMicrometerConfiguration().getBatchSize(); return config.getWavefrontConfiguration().getBatchSize();
} }
}; };
Metrics.addRegistry(new WavefrontMeterRegistry(wavefrontConfig, Clock.SYSTEM) { Metrics.addRegistry(new WavefrontMeterRegistry(wavefrontConfig, Clock.SYSTEM) {
@Override @Override
protected DistributionStatisticConfig defaultHistogramConfig() { protected DistributionStatisticConfig defaultHistogramConfig() {
return DistributionStatisticConfig.builder() return defaultDistributionStatisticConfig.merge(super.defaultHistogramConfig());
.percentiles(.75, .95, .99, .999)
.build()
.merge(super.defaultHistogramConfig());
} }
}); });
{
final DatadogMeterRegistry datadogMeterRegistry = new DatadogMeterRegistry(config.getDatadogConfiguration(), Clock.SYSTEM);
datadogMeterRegistry.config().commonTags(
Tags.of(
"service", "chat",
"host", HostnameUtil.getLocalHostname(),
"version", WhisperServerVersion.getServerVersion(),
"env", config.getDatadogConfiguration().getEnvironment()))
.meterFilter(MeterFilter.denyNameStartsWith(MetricsRequestEventListener.REQUEST_COUNTER_NAME))
.meterFilter(MeterFilter.denyNameStartsWith(MetricsRequestEventListener.ANDROID_REQUEST_COUNTER_NAME))
.meterFilter(MeterFilter.denyNameStartsWith(MetricsRequestEventListener.DESKTOP_REQUEST_COUNTER_NAME))
.meterFilter(MeterFilter.denyNameStartsWith(MetricsRequestEventListener.IOS_REQUEST_COUNTER_NAME))
.meterFilter(new MeterFilter() {
@Override
public DistributionStatisticConfig configure(final Id id, final DistributionStatisticConfig config) {
return defaultDistributionStatisticConfig.merge(config);
}
});
Metrics.addRegistry(datadogMeterRegistry);
}
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
environment.getObjectMapper().setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); environment.getObjectMapper().setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
environment.getObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); environment.getObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
@@ -279,82 +315,57 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
FaultTolerantDatabase accountDatabase = new FaultTolerantDatabase("accounts_database", accountJdbi, config.getAccountsDatabaseConfiguration().getCircuitBreakerConfiguration()); 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());
AmazonDynamoDBClientBuilder messageDynamoDbClientBuilder = AmazonDynamoDBClientBuilder DynamoDbClient messageDynamoDb = DynamoDbFromConfig.client(config.getMessageDynamoDbConfiguration(),
.standard() software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
.withRegion(config.getMessageDynamoDbConfiguration().getRegion())
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) config.getMessageDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
.withRequestTimeout((int) config.getMessageDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
.withCredentials(InstanceProfileCredentialsProvider.getInstance());
AmazonDynamoDBClientBuilder keysDynamoDbClientBuilder = AmazonDynamoDBClientBuilder DynamoDbClient preKeyDynamoDb = DynamoDbFromConfig.client(config.getKeysDynamoDbConfiguration(),
.standard() software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
.withRegion(config.getKeysDynamoDbConfiguration().getRegion())
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) config.getKeysDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
.withRequestTimeout((int) config.getKeysDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
.withCredentials(InstanceProfileCredentialsProvider.getInstance());
AmazonDynamoDBClientBuilder accountsDynamoDbClientBuilder = AmazonDynamoDBClientBuilder DynamoDbClient accountsDynamoDbClient = DynamoDbFromConfig.client(config.getAccountsDynamoDbConfiguration(),
.standard() software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
.withRegion(config.getAccountsDynamoDbConfiguration().getRegion())
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) config.getAccountsDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
.withRequestTimeout((int) config.getAccountsDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
.withCredentials(InstanceProfileCredentialsProvider.getInstance());
// The thread pool core & max sizes are set via dynamic configuration within AccountsDynamoDb // The thread pool core & max sizes are set via dynamic configuration within AccountsDynamoDb
ThreadPoolExecutor accountsDynamoDbMigrationThreadPool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, ThreadPoolExecutor accountsDynamoDbMigrationThreadPool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS,
new LinkedBlockingDeque<>()); new LinkedBlockingDeque<>());
AmazonDynamoDBAsyncClientBuilder accountsDynamoDbAsyncClientBuilder = AmazonDynamoDBAsyncClientBuilder DynamoDbAsyncClient accountsDynamoDbAsyncClient = DynamoDbFromConfig.asyncClient(config.getAccountsDynamoDbConfiguration(),
.standard() software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create(),
.withRegion(accountsDynamoDbClientBuilder.getRegion()) accountsDynamoDbMigrationThreadPool);
.withClientConfiguration(accountsDynamoDbClientBuilder.getClientConfiguration())
.withCredentials(accountsDynamoDbClientBuilder.getCredentials())
.withExecutorFactory(() -> accountsDynamoDbMigrationThreadPool);
AmazonDynamoDBClientBuilder migrationDeletedAccountsDynamoDbClientBuilder = AmazonDynamoDBClientBuilder DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client(config.getDeletedAccountsDynamoDbConfiguration(),
.standard() software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
.withRegion(config.getMigrationDeletedAccountsDynamoDbConfiguration().getRegion())
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) config.getMigrationDeletedAccountsDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
.withRequestTimeout((int) config.getMigrationDeletedAccountsDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
.withCredentials(InstanceProfileCredentialsProvider.getInstance());
AmazonDynamoDBClientBuilder migrationRetryAccountsDynamoDbClientBuilder = AmazonDynamoDBClientBuilder DynamoDbClient recentlyDeletedAccountsDynamoDb = DynamoDbFromConfig.client(config.getMigrationDeletedAccountsDynamoDbConfiguration(),
.standard() software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
.withRegion(config.getMigrationRetryAccountsDynamoDbConfiguration().getRegion())
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) config.getMigrationRetryAccountsDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
.withRequestTimeout((int) config.getMigrationRetryAccountsDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
.withCredentials(InstanceProfileCredentialsProvider.getInstance());
AmazonDynamoDBClientBuilder pushChallengeDynamoDbClientBuilder = AmazonDynamoDBClientBuilder DynamoDbClient pushChallengeDynamoDbClient = DynamoDbFromConfig.client(config.getPushChallengeDynamoDbConfiguration(),
.standard() software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
.withRegion(config.getPushChallengeDynamoDbConfiguration().getRegion())
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) config.getPushChallengeDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
.withRequestTimeout((int) config.getPushChallengeDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
.withCredentials(InstanceProfileCredentialsProvider.getInstance());
AmazonDynamoDBClientBuilder reportMessageDynamoDbClientBuilder = AmazonDynamoDBClientBuilder DynamoDbClient reportMessageDynamoDbClient = DynamoDbFromConfig.client(config.getReportMessageDynamoDbConfiguration(),
.standard() software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
.withRegion(config.getReportMessageDynamoDbConfiguration().getRegion())
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) config.getReportMessageDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
.withRequestTimeout((int) config.getReportMessageDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
.withCredentials(InstanceProfileCredentialsProvider.getInstance());
DynamoDB messageDynamoDb = new DynamoDB(messageDynamoDbClientBuilder.build()); DynamoDbClient migrationRetryAccountsDynamoDb = DynamoDbFromConfig.client(config.getMigrationRetryAccountsDynamoDbConfiguration(),
DynamoDB preKeyDynamoDb = new DynamoDB(keysDynamoDbClientBuilder.build()); software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
AmazonDynamoDB accountsDynamoDbClient = accountsDynamoDbClientBuilder.build(); DynamoDbClient pendingAccountsDynamoDbClient = DynamoDbFromConfig.client(config.getPendingAccountsDynamoDbConfiguration(),
AmazonDynamoDBAsync accountsDynamodbAsyncClient = accountsDynamoDbAsyncClientBuilder.build(); software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
DynamoDB recentlyDeletedAccountsDynamoDb = new DynamoDB(migrationDeletedAccountsDynamoDbClientBuilder.build()); DynamoDbClient pendingDevicesDynamoDbClient = DynamoDbFromConfig.client(config.getPendingDevicesDynamoDbConfiguration(),
DynamoDB migrationRetryAccountsDynamoDb = new DynamoDB(migrationRetryAccountsDynamoDbClientBuilder.build()); software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
AmazonDynamoDB deletedAccountsLockDynamoDbClient = AmazonDynamoDBClientBuilder.standard()
.withRegion(config.getDeletedAccountsLockDynamoDbConfiguration().getRegion())
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) config.getDeletedAccountsLockDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
.withRequestTimeout((int) config.getDeletedAccountsLockDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
.withCredentials(InstanceProfileCredentialsProvider.getInstance())
.build();
DeletedAccounts deletedAccounts = new DeletedAccounts(deletedAccountsDynamoDbClient, config.getDeletedAccountsDynamoDbConfiguration().getTableName(), config.getDeletedAccountsDynamoDbConfiguration().getNeedsReconciliationIndexName());
MigrationDeletedAccounts migrationDeletedAccounts = new MigrationDeletedAccounts(recentlyDeletedAccountsDynamoDb, config.getMigrationDeletedAccountsDynamoDbConfiguration().getTableName()); MigrationDeletedAccounts migrationDeletedAccounts = new MigrationDeletedAccounts(recentlyDeletedAccountsDynamoDb, config.getMigrationDeletedAccountsDynamoDbConfiguration().getTableName());
MigrationRetryAccounts migrationRetryAccounts = new MigrationRetryAccounts(migrationRetryAccountsDynamoDb, config.getMigrationRetryAccountsDynamoDbConfiguration().getTableName()); MigrationRetryAccounts migrationRetryAccounts = new MigrationRetryAccounts(migrationRetryAccountsDynamoDb, config.getMigrationRetryAccountsDynamoDbConfiguration().getTableName());
Accounts accounts = new Accounts(accountDatabase); Accounts accounts = new Accounts(accountDatabase);
AccountsDynamoDb accountsDynamoDb = new AccountsDynamoDb(accountsDynamoDbClient, accountsDynamodbAsyncClient, accountsDynamoDbMigrationThreadPool, new DynamoDB(accountsDynamoDbClient), config.getAccountsDynamoDbConfiguration().getTableName(), config.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(), migrationDeletedAccounts, migrationRetryAccounts); AccountsDynamoDb accountsDynamoDb = new AccountsDynamoDb(accountsDynamoDbClient, accountsDynamoDbAsyncClient, accountsDynamoDbMigrationThreadPool, config.getAccountsDynamoDbConfiguration().getTableName(), config.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(), migrationDeletedAccounts, migrationRetryAccounts);
PendingAccounts pendingAccounts = new PendingAccounts(accountDatabase);
PendingDevices pendingDevices = new PendingDevices (accountDatabase);
Usernames usernames = new Usernames(accountDatabase); Usernames usernames = new Usernames(accountDatabase);
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase); ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
Profiles profiles = new Profiles(accountDatabase); Profiles profiles = new Profiles(accountDatabase);
@@ -362,8 +373,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messageDynamoDb, config.getMessageDynamoDbConfiguration().getTableName(), config.getMessageDynamoDbConfiguration().getTimeToLive()); MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messageDynamoDb, config.getMessageDynamoDbConfiguration().getTableName(), config.getMessageDynamoDbConfiguration().getTimeToLive());
AbusiveHostRules abusiveHostRules = new AbusiveHostRules(abuseDatabase); AbusiveHostRules abusiveHostRules = new AbusiveHostRules(abuseDatabase);
RemoteConfigs remoteConfigs = new RemoteConfigs(accountDatabase); RemoteConfigs remoteConfigs = new RemoteConfigs(accountDatabase);
PushChallengeDynamoDb pushChallengeDynamoDb = new PushChallengeDynamoDb(new DynamoDB(pushChallengeDynamoDbClientBuilder.build()), config.getPushChallengeDynamoDbConfiguration().getTableName()); PushChallengeDynamoDb pushChallengeDynamoDb = new PushChallengeDynamoDb(pushChallengeDynamoDbClient, config.getPushChallengeDynamoDbConfiguration().getTableName());
ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(new DynamoDB(reportMessageDynamoDbClientBuilder.build()), config.getReportMessageDynamoDbConfiguration().getTableName()); ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(reportMessageDynamoDbClient, config.getReportMessageDynamoDbConfiguration().getTableName());
VerificationCodeStore pendingAccounts = new VerificationCodeStore(pendingAccountsDynamoDbClient, config.getPendingAccountsDynamoDbConfiguration().getTableName());
VerificationCodeStore pendingDevices = new VerificationCodeStore(pendingDevicesDynamoDbClient, config.getPendingDevicesDynamoDbConfiguration().getTableName());
RedisClientFactory pubSubClientFactory = new RedisClientFactory("pubsub_cache", config.getPubsubCacheConfiguration().getUrl(), config.getPubsubCacheConfiguration().getReplicaUrls(), config.getPubsubCacheConfiguration().getCircuitBreakerConfiguration()); RedisClientFactory pubSubClientFactory = new RedisClientFactory("pubsub_cache", config.getPubsubCacheConfiguration().getUrl(), config.getPubsubCacheConfiguration().getReplicaUrls(), config.getPubsubCacheConfiguration().getCircuitBreakerConfiguration());
ReplicatedJedisPool pubsubClient = pubSubClientFactory.getRedisClientPool(); ReplicatedJedisPool pubsubClient = pubSubClientFactory.getRedisClientPool();
@@ -390,7 +403,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
BlockingQueue<Runnable> keyspaceNotificationDispatchQueue = new ArrayBlockingQueue<>(10_000); BlockingQueue<Runnable> keyspaceNotificationDispatchQueue = new ArrayBlockingQueue<>(10_000);
Metrics.gaugeCollectionSize(name(getClass(), "keyspaceNotificationDispatchQueueSize"), Collections.emptyList(), keyspaceNotificationDispatchQueue); Metrics.gaugeCollectionSize(name(getClass(), "keyspaceNotificationDispatchQueueSize"), Collections.emptyList(), keyspaceNotificationDispatchQueue);
ScheduledExecutorService recurringJobExecutor = environment.lifecycle().scheduledExecutorService(name(getClass(), "recurringJob-%d")).threads(2).build(); ScheduledExecutorService recurringJobExecutor = environment.lifecycle().scheduledExecutorService(name(getClass(), "recurringJob-%d")).threads(3).build();
ScheduledExecutorService declinedMessageReceiptExecutor = environment.lifecycle().scheduledExecutorService(name(getClass(), "declined-receipt-%d")).threads(2).build(); ScheduledExecutorService declinedMessageReceiptExecutor = environment.lifecycle().scheduledExecutorService(name(getClass(), "declined-receipt-%d")).threads(2).build();
ScheduledExecutorService retrySchedulingExecutor = environment.lifecycle().scheduledExecutorService(name(getClass(), "retry-%d")).threads(2).build(); ScheduledExecutorService retrySchedulingExecutor = environment.lifecycle().scheduledExecutorService(name(getClass(), "retry-%d")).threads(2).build();
ExecutorService keyspaceNotificationDispatchExecutor = environment.lifecycle().executorService(name(getClass(), "keyspaceNotification-%d")).maxThreads(16).workQueue(keyspaceNotificationDispatchQueue).build(); ExecutorService keyspaceNotificationDispatchExecutor = environment.lifecycle().executorService(name(getClass(), "keyspaceNotification-%d")).maxThreads(16).workQueue(keyspaceNotificationDispatchQueue).build();
@@ -420,15 +433,16 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator, storageServiceExecutor, config.getSecureStorageServiceConfiguration()); SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator, storageServiceExecutor, config.getSecureStorageServiceConfiguration());
ClientPresenceManager clientPresenceManager = new ClientPresenceManager(clientPresenceCluster, recurringJobExecutor, keyspaceNotificationDispatchExecutor); ClientPresenceManager clientPresenceManager = new ClientPresenceManager(clientPresenceCluster, recurringJobExecutor, keyspaceNotificationDispatchExecutor);
DirectoryQueue directoryQueue = new DirectoryQueue(config.getDirectoryConfiguration().getSqsConfiguration()); DirectoryQueue directoryQueue = new DirectoryQueue(config.getDirectoryConfiguration().getSqsConfiguration());
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, cacheCluster); StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager(pendingDevices, cacheCluster); StoredVerificationCodeManager pendingDevicesManager = new StoredVerificationCodeManager(pendingDevices);
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster); UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster);
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster); ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster);
MessagesCache messagesCache = new MessagesCache(messagesCluster, messagesCluster, keyspaceNotificationDispatchExecutor); MessagesCache messagesCache = new MessagesCache(messagesCluster, messagesCluster, keyspaceNotificationDispatchExecutor);
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster); PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster);
ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, Metrics.globalRegistry); ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, Metrics.globalRegistry);
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, pushLatencyManager, reportMessageManager); MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, pushLatencyManager, reportMessageManager);
AccountsManager accountsManager = new AccountsManager(accounts, accountsDynamoDb, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager, secureStorageClient, secureBackupClient, experimentEnrollmentManager, dynamicConfigurationManager); DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts, deletedAccountsLockDynamoDbClient, config.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
AccountsManager accountsManager = new AccountsManager(accounts, accountsDynamoDb, cacheCluster, deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager, pendingAccountsManager, secureStorageClient, secureBackupClient, experimentEnrollmentManager, dynamicConfigurationManager);
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs); RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(accountsManager, messagesManager); DeadLetterHandler deadLetterHandler = new DeadLetterHandler(accountsManager, messagesManager);
DispatchManager dispatchManager = new DispatchManager(pubSubClientFactory, Optional.of(deadLetterHandler)); DispatchManager dispatchManager = new DispatchManager(pubSubClientFactory, Optional.of(deadLetterHandler));
@@ -460,13 +474,17 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
MessagePersister messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, dynamicConfigurationManager, Duration.ofMinutes(config.getMessageCacheConfiguration().getPersistDelayMinutes())); MessagePersister messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, dynamicConfigurationManager, Duration.ofMinutes(config.getMessageCacheConfiguration().getPersistDelayMinutes()));
final List<DeletedAccountsDirectoryReconciler> deletedAccountsDirectoryReconcilers = new ArrayList<>();
final List<AccountDatabaseCrawlerListener> accountDatabaseCrawlerListeners = new ArrayList<>(); final List<AccountDatabaseCrawlerListener> accountDatabaseCrawlerListeners = new ArrayList<>();
accountDatabaseCrawlerListeners.add(new PushFeedbackProcessor(accountsManager, directoryQueue)); accountDatabaseCrawlerListeners.add(new PushFeedbackProcessor(accountsManager));
accountDatabaseCrawlerListeners.add(new ActiveUserCounter(config.getMetricsFactory(), cacheCluster)); accountDatabaseCrawlerListeners.add(new ActiveUserCounter(config.getMetricsFactory(), cacheCluster));
for (DirectoryServerConfiguration directoryServerConfiguration : config.getDirectoryConfiguration().getDirectoryServerConfiguration()) { for (DirectoryServerConfiguration directoryServerConfiguration : config.getDirectoryConfiguration().getDirectoryServerConfiguration()) {
final DirectoryReconciliationClient directoryReconciliationClient = new DirectoryReconciliationClient(directoryServerConfiguration); final DirectoryReconciliationClient directoryReconciliationClient = new DirectoryReconciliationClient(directoryServerConfiguration);
final DirectoryReconciler directoryReconciler = new DirectoryReconciler(directoryServerConfiguration.getReplicationName(), directoryReconciliationClient); final DirectoryReconciler directoryReconciler = new DirectoryReconciler(directoryServerConfiguration.getReplicationName(), directoryReconciliationClient);
accountDatabaseCrawlerListeners.add(directoryReconciler); accountDatabaseCrawlerListeners.add(directoryReconciler);
final DeletedAccountsDirectoryReconciler deletedAccountsDirectoryReconciler = new DeletedAccountsDirectoryReconciler(directoryServerConfiguration.getReplicationName(), directoryReconciliationClient);
deletedAccountsDirectoryReconcilers.add(deletedAccountsDirectoryReconciler);
} }
accountDatabaseCrawlerListeners.add(new AccountCleaner(accountsManager)); accountDatabaseCrawlerListeners.add(new AccountCleaner(accountsManager));
accountDatabaseCrawlerListeners.add(new RegistrationLockVersionCounter(metricsCluster, config.getMetricsFactory())); accountDatabaseCrawlerListeners.add(new RegistrationLockVersionCounter(metricsCluster, config.getMetricsFactory()));
@@ -478,13 +496,18 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, ftxClient, config.getPaymentsServiceConfiguration().getPaymentCurrencies()); CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, ftxClient, config.getPaymentsServiceConfiguration().getPaymentCurrencies());
AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(cacheCluster); AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(cacheCluster);
AccountDatabaseCrawler accountDatabaseCrawler = new AccountDatabaseCrawler(accountsManager, accountDatabaseCrawlerCache, accountDatabaseCrawlerListeners, config.getAccountDatabaseCrawlerConfiguration().getChunkSize(), config.getAccountDatabaseCrawlerConfiguration().getChunkIntervalMs()); AccountDatabaseCrawler accountDatabaseCrawler = new AccountDatabaseCrawler(accountsManager, accountDatabaseCrawlerCache, accountDatabaseCrawlerListeners, config.getAccountDatabaseCrawlerConfiguration().getChunkSize(), config.getAccountDatabaseCrawlerConfiguration().getChunkIntervalMs(), dynamicConfigurationManager);
DeletedAccountsTableCrawler deletedAccountsTableCrawler = new DeletedAccountsTableCrawler(deletedAccountsManager, deletedAccountsDirectoryReconcilers, cacheCluster, recurringJobExecutor);
MigrationRetryAccountsTableCrawler migrationRetryAccountsTableCrawler = new MigrationRetryAccountsTableCrawler(migrationRetryAccounts, accountsManager, accountsDynamoDb, cacheCluster, recurringJobExecutor);
apnSender.setApnFallbackManager(apnFallbackManager); apnSender.setApnFallbackManager(apnFallbackManager);
environment.lifecycle().manage(apnFallbackManager); environment.lifecycle().manage(apnFallbackManager);
environment.lifecycle().manage(pubSubManager); environment.lifecycle().manage(pubSubManager);
environment.lifecycle().manage(messageSender); environment.lifecycle().manage(messageSender);
environment.lifecycle().manage(accountDatabaseCrawler); environment.lifecycle().manage(accountDatabaseCrawler);
environment.lifecycle().manage(deletedAccountsTableCrawler);
environment.lifecycle().manage(migrationRetryAccountsTableCrawler);
environment.lifecycle().manage(remoteConfigsManager); environment.lifecycle().manage(remoteConfigsManager);
environment.lifecycle().manage(messagesCache); environment.lifecycle().manage(messagesCache);
environment.lifecycle().manage(messagePersister); environment.lifecycle().manage(messagePersister);
@@ -492,10 +515,16 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.lifecycle().manage(currencyManager); environment.lifecycle().manage(currencyManager);
environment.lifecycle().manage(torExitNodeManager); environment.lifecycle().manage(torExitNodeManager);
environment.lifecycle().manage(asnManager); environment.lifecycle().manage(asnManager);
environment.lifecycle().manage(directoryQueue);
AWSCredentials credentials = new BasicAWSCredentials(config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret()); StaticCredentialsProvider cdnCredentialsProvider = StaticCredentialsProvider
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials); .create(AwsBasicCredentials.create(
AmazonS3 cdnS3Client = AmazonS3Client.builder().withCredentials(credentialsProvider).withRegion(config.getCdnConfiguration().getRegion()).build(); config.getCdnConfiguration().getAccessKey(),
config.getCdnConfiguration().getAccessSecret()));
S3Client cdnS3Client = S3Client.builder()
.credentialsProvider(cdnCredentialsProvider)
.region(Region.of(config.getCdnConfiguration().getRegion()))
.build();
PostPolicyGenerator profileCdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket(), config.getCdnConfiguration().getAccessKey()); PostPolicyGenerator profileCdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket(), config.getCdnConfiguration().getAccessKey());
PolicySigner profileCdnPolicySigner = new PolicySigner(config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion()); PolicySigner profileCdnPolicySigner = new PolicySigner(config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion());
@@ -504,17 +533,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
ServerZkAuthOperations zkAuthOperations = new ServerZkAuthOperations(zkSecretParams); ServerZkAuthOperations zkAuthOperations = new ServerZkAuthOperations(zkSecretParams);
boolean isZkEnabled = config.getZkConfig().isEnabled(); boolean isZkEnabled = config.getZkConfig().isEnabled();
AttachmentControllerV1 attachmentControllerV1 = new AttachmentControllerV1(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getBucket());
AttachmentControllerV2 attachmentControllerV2 = new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getRegion(), config.getAwsAttachmentsConfiguration().getBucket());
AttachmentControllerV3 attachmentControllerV3 = new AttachmentControllerV3(rateLimiters, config.getGcpAttachmentsConfiguration().getDomain(), config.getGcpAttachmentsConfiguration().getEmail(), config.getGcpAttachmentsConfiguration().getMaxSizeInBytes(), config.getGcpAttachmentsConfiguration().getPathPrefix(), config.getGcpAttachmentsConfiguration().getRsaSigningKey());
DonationController donationController = new DonationController(donationExecutor, config.getDonationConfiguration());
KeysController keysController = new KeysController(rateLimiters, keysDynamoDb, accountsManager, directoryQueue, preKeyRateLimiter, dynamicConfigurationManager, rateLimitChallengeManager);
MessageController messageController = new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, unsealedSenderRateLimiter, apnFallbackManager, dynamicConfigurationManager, rateLimitChallengeManager, reportMessageManager, metricsCluster, declinedMessageReceiptExecutor);
ProfileController profileController = new ProfileController(rateLimiters, accountsManager, profilesManager, usernamesManager, dynamicConfigurationManager, cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations, isZkEnabled);
StickerController stickerController = new StickerController(rateLimiters, config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket());
RemoteConfigController remoteConfigController = new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().getAuthorizedTokens(), config.getRemoteConfigConfiguration().getGlobalConfig());
ChallengeController challengeController = new ChallengeController(rateLimitChallengeManager);
AuthFilter<BasicCredentials, Account> accountAuthFilter = new BasicCredentialAuthFilter.Builder<Account>().setAuthenticator(accountAuthenticator).buildAuthFilter (); AuthFilter<BasicCredentials, Account> accountAuthFilter = new BasicCredentialAuthFilter.Builder<Account>().setAuthenticator(accountAuthenticator).buildAuthFilter ();
AuthFilter<BasicCredentials, DisabledPermittedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAccount>().setAuthenticator(disabledPermittedAccountAuthenticator).buildAuthFilter(); AuthFilter<BasicCredentials, DisabledPermittedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAccount>().setAuthenticator(disabledPermittedAccountAuthenticator).buildAuthFilter();
@@ -527,25 +545,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
DisabledPermittedAccount.class, disabledPermittedAccountAuthFilter))); DisabledPermittedAccount.class, disabledPermittedAccountAuthFilter)));
environment.jersey().register(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class))); environment.jersey().register(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class)));
environment.jersey().register(new TimestampResponseFilter()); environment.jersey().register(new TimestampResponseFilter());
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, usernamesManager, abusiveHostRules, rateLimiters, smsSender, directoryQueue, messagesManager, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender, backupCredentialsGenerator, verifyExperimentEnrollmentManager));
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, directoryQueue, rateLimiters, config.getMaxDevices()));
environment.jersey().register(new DirectoryController(directoryCredentialsGenerator));
environment.jersey().register(new ProvisioningController(rateLimiters, provisioningManager));
environment.jersey().register(new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays()), zkAuthOperations, isZkEnabled));
environment.jersey().register(new VoiceVerificationController(config.getVoiceVerificationConfiguration().getUrl(), config.getVoiceVerificationConfiguration().getLocales())); environment.jersey().register(new VoiceVerificationController(config.getVoiceVerificationConfiguration().getUrl(), config.getVoiceVerificationConfiguration().getLocales()));
environment.jersey().register(new SecureStorageController(storageCredentialsGenerator));
environment.jersey().register(new SecureBackupController(backupCredentialsGenerator));
environment.jersey().register(new PaymentsController(currencyManager, paymentsCredentialsGenerator));
environment.jersey().register(attachmentControllerV1);
environment.jersey().register(attachmentControllerV2);
environment.jersey().register(attachmentControllerV3);
environment.jersey().register(donationController);
environment.jersey().register(keysController);
environment.jersey().register(messageController);
environment.jersey().register(profileController);
environment.jersey().register(stickerController);
environment.jersey().register(remoteConfigController);
environment.jersey().register(challengeController);
/// ///
WebSocketEnvironment<Account> webSocketEnvironment = new WebSocketEnvironment<>(environment, config.getWebSocketConfiguration(), 90000); WebSocketEnvironment<Account> webSocketEnvironment = new WebSocketEnvironment<>(environment, config.getWebSocketConfiguration(), 90000);
@@ -554,13 +554,34 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
webSocketEnvironment.jersey().register(MultiRecipientMessageProvider.class); webSocketEnvironment.jersey().register(MultiRecipientMessageProvider.class);
webSocketEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET)); webSocketEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET));
webSocketEnvironment.jersey().register(new KeepAliveController(clientPresenceManager)); webSocketEnvironment.jersey().register(new KeepAliveController(clientPresenceManager));
webSocketEnvironment.jersey().register(messageController);
webSocketEnvironment.jersey().register(profileController); // these should be common, but use @Auth DisabledPermittedAccount, which isnt supported yet on websocket
webSocketEnvironment.jersey().register(attachmentControllerV1); environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, usernamesManager, abusiveHostRules, rateLimiters, smsSender, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender, backupCredentialsGenerator, verifyExperimentEnrollmentManager));
webSocketEnvironment.jersey().register(attachmentControllerV2); environment.jersey().register(new KeysController(rateLimiters, keysDynamoDb, accountsManager, preKeyRateLimiter, dynamicConfigurationManager, rateLimitChallengeManager));
webSocketEnvironment.jersey().register(attachmentControllerV3);
webSocketEnvironment.jersey().register(donationController); final List<Object> commonControllers = List.of(
webSocketEnvironment.jersey().register(remoteConfigController); new AttachmentControllerV1(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getBucket()),
new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getRegion(), config.getAwsAttachmentsConfiguration().getBucket()),
new AttachmentControllerV3(rateLimiters, config.getGcpAttachmentsConfiguration().getDomain(), config.getGcpAttachmentsConfiguration().getEmail(), config.getGcpAttachmentsConfiguration().getMaxSizeInBytes(), config.getGcpAttachmentsConfiguration().getPathPrefix(), config.getGcpAttachmentsConfiguration().getRsaSigningKey()),
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays()), zkAuthOperations, isZkEnabled),
new ChallengeController(rateLimitChallengeManager),
new DeviceController(pendingDevicesManager, accountsManager, messagesManager, keysDynamoDb, rateLimiters, config.getMaxDevices()),
new DirectoryController(directoryCredentialsGenerator),
new DonationController(donationExecutor, config.getDonationConfiguration()),
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, unsealedSenderRateLimiter, apnFallbackManager, dynamicConfigurationManager, rateLimitChallengeManager, reportMessageManager, metricsCluster, declinedMessageReceiptExecutor),
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
new ProfileController(rateLimiters, accountsManager, profilesManager, usernamesManager, dynamicConfigurationManager, cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations, isZkEnabled),
new ProvisioningController(rateLimiters, provisioningManager),
new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().getAuthorizedTokens(), config.getRemoteConfigConfiguration().getGlobalConfig()),
new SecureBackupController(backupCredentialsGenerator),
new SecureStorageController(storageCredentialsGenerator),
new StickerController(rateLimiters, config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket())
);
for (Object controller : commonControllers) {
environment.jersey().register(controller);
webSocketEnvironment.jersey().register(controller);
}
WebSocketEnvironment<Account> provisioningEnvironment = new WebSocketEnvironment<>(environment, webSocketEnvironment.getRequestLog(), 60000); WebSocketEnvironment<Account> provisioningEnvironment = new WebSocketEnvironment<>(environment, webSocketEnvironment.getRequestLog(), 60000);
provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(pubSubManager)); provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(pubSubManager));
@@ -606,23 +627,24 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
BufferPoolGauges.registerMetrics(); BufferPoolGauges.registerMetrics();
GarbageCollectionGauges.registerMetrics(); GarbageCollectionGauges.registerMetrics();
new NstatCounters().registerMetrics(recurringJobExecutor, wavefrontConfig.step());
} }
private void registerExceptionMappers(Environment environment, WebSocketEnvironment<Account> webSocketEnvironment, WebSocketEnvironment<Account> provisioningEnvironment) { private void registerExceptionMappers(Environment environment, WebSocketEnvironment<Account> webSocketEnvironment, WebSocketEnvironment<Account> provisioningEnvironment) {
environment.jersey().register(new LoggingUnhandledExceptionMapper());
environment.jersey().register(new IOExceptionMapper()); environment.jersey().register(new IOExceptionMapper());
environment.jersey().register(new RateLimitExceededExceptionMapper()); environment.jersey().register(new RateLimitExceededExceptionMapper());
environment.jersey().register(new InvalidWebsocketAddressExceptionMapper()); environment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
environment.jersey().register(new DeviceLimitExceededExceptionMapper()); environment.jersey().register(new DeviceLimitExceededExceptionMapper());
environment.jersey().register(new RetryLaterExceptionMapper()); environment.jersey().register(new RetryLaterExceptionMapper());
webSocketEnvironment.jersey().register(new LoggingUnhandledExceptionMapper());
webSocketEnvironment.jersey().register(new IOExceptionMapper()); webSocketEnvironment.jersey().register(new IOExceptionMapper());
webSocketEnvironment.jersey().register(new RateLimitExceededExceptionMapper()); webSocketEnvironment.jersey().register(new RateLimitExceededExceptionMapper());
webSocketEnvironment.jersey().register(new InvalidWebsocketAddressExceptionMapper()); webSocketEnvironment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
webSocketEnvironment.jersey().register(new DeviceLimitExceededExceptionMapper()); webSocketEnvironment.jersey().register(new DeviceLimitExceededExceptionMapper());
webSocketEnvironment.jersey().register(new RetryLaterExceptionMapper()); webSocketEnvironment.jersey().register(new RetryLaterExceptionMapper());
provisioningEnvironment.jersey().register(new LoggingUnhandledExceptionMapper());
provisioningEnvironment.jersey().register(new IOExceptionMapper()); provisioningEnvironment.jersey().register(new IOExceptionMapper());
provisioningEnvironment.jersey().register(new RateLimitExceededExceptionMapper()); provisioningEnvironment.jersey().register(new RateLimitExceededExceptionMapper());
provisioningEnvironment.jersey().register(new InvalidWebsocketAddressExceptionMapper()); provisioningEnvironment.jersey().register(new InvalidWebsocketAddressExceptionMapper());

View File

@@ -5,45 +5,33 @@
package org.whispersystems.textsecuregcm.auth; package org.whispersystems.textsecuregcm.auth;
import com.codahale.metrics.Meter; import static com.codahale.metrics.MetricRegistry.name;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.auth.basic.BasicCredentials; import io.dropwizard.auth.basic.BasicCredentials;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Metrics;
import org.slf4j.Logger; import io.micrometer.core.instrument.Tags;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import java.time.Clock; import java.time.Clock;
import java.time.Duration; import java.time.Duration;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Optional; import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import static com.codahale.metrics.MetricRegistry.name; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Util;
public class BaseAccountAuthenticator { public class BaseAccountAuthenticator {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); private static final String AUTHENTICATION_COUNTER_NAME = name(BaseAccountAuthenticator.class, "authentication");
private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(), "authentication", "failed" )); private static final String AUTHENTICATION_SUCCEEDED_TAG_NAME = "succeeded";
private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(), "authentication", "succeeded" )); private static final String AUTHENTICATION_FAILURE_REASON_TAG_NAME = "reason";
private final Meter noSuchAccountMeter = metricRegistry.meter(name(getClass(), "authentication", "noSuchAccount" )); private static final String AUTHENTICATION_ENABLED_REQUIRED_TAG_NAME = "enabledRequired";
private final Meter noSuchDeviceMeter = metricRegistry.meter(name(getClass(), "authentication", "noSuchDevice" )); private static final String AUTHENTICATION_CREDENTIAL_TYPE_TAG_NAME = "credentialType";
private final Meter accountDisabledMeter = metricRegistry.meter(name(getClass(), "authentication", "accountDisabled"));
private final Meter deviceDisabledMeter = metricRegistry.meter(name(getClass(), "authentication", "deviceDisabled" ));
private final Meter invalidAuthHeaderMeter = metricRegistry.meter(name(getClass(), "authentication", "invalidHeader" ));
private final String daysSinceLastSeenDistributionName = name(getClass(), "authentication", "daysSinceLastSeen");
private static final String DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME = name(BaseAccountAuthenticator.class, "daysSinceLastSeen");
private static final String IS_PRIMARY_DEVICE_TAG = "isPrimary"; private static final String IS_PRIMARY_DEVICE_TAG = "isPrimary";
private final Logger logger = LoggerFactory.getLogger(BaseAccountAuthenticator.class);
private final AccountsManager accountsManager; private final AccountsManager accountsManager;
private final Clock clock; private final Clock clock;
@@ -58,64 +46,81 @@ public class BaseAccountAuthenticator {
} }
public Optional<Account> authenticate(BasicCredentials basicCredentials, boolean enabledRequired) { public Optional<Account> authenticate(BasicCredentials basicCredentials, boolean enabledRequired) {
boolean succeeded = false;
String failureReason = null;
String credentialType = null;
try { try {
AuthorizationHeader authorizationHeader = AuthorizationHeader.fromUserAndPassword(basicCredentials.getUsername(), basicCredentials.getPassword()); AuthorizationHeader authorizationHeader = AuthorizationHeader.fromUserAndPassword(basicCredentials.getUsername(), basicCredentials.getPassword());
Optional<Account> account = accountsManager.get(authorizationHeader.getIdentifier()); Optional<Account> account = accountsManager.get(authorizationHeader.getIdentifier());
if (!account.isPresent()) { credentialType = authorizationHeader.getIdentifier().hasNumber() ? "e164" : "uuid";
noSuchAccountMeter.mark();
if (account.isEmpty()) {
failureReason = "noSuchAccount";
return Optional.empty(); return Optional.empty();
} }
Optional<Device> device = account.get().getDevice(authorizationHeader.getDeviceId()); Optional<Device> device = account.get().getDevice(authorizationHeader.getDeviceId());
if (!device.isPresent()) { if (device.isEmpty()) {
noSuchDeviceMeter.mark(); failureReason = "noSuchDevice";
return Optional.empty(); return Optional.empty();
} }
if (enabledRequired) { if (enabledRequired) {
if (!device.get().isEnabled()) { if (!device.get().isEnabled()) {
deviceDisabledMeter.mark(); failureReason = "deviceDisabled";
return Optional.empty(); return Optional.empty();
} }
if (!account.get().isEnabled()) { if (!account.get().isEnabled()) {
accountDisabledMeter.mark(); failureReason = "accountDisabled";
return Optional.empty(); return Optional.empty();
} }
} }
if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) { if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
authenticationSucceededMeter.mark(); succeeded = true;
account.get().setAuthenticatedDevice(device.get()); final Account authenticatedAccount = updateLastSeen(account.get(), device.get());
updateLastSeen(account.get(), device.get()); authenticatedAccount.setAuthenticatedDevice(device.get());
return account; return Optional.of(authenticatedAccount);
} }
authenticationFailedMeter.mark();
return Optional.empty(); return Optional.empty();
} catch (IllegalArgumentException | InvalidAuthorizationHeaderException iae) { } catch (IllegalArgumentException | InvalidAuthorizationHeaderException iae) {
invalidAuthHeaderMeter.mark(); failureReason = "invalidHeader";
return Optional.empty(); return Optional.empty();
} finally {
Tags tags = Tags.of(
AUTHENTICATION_SUCCEEDED_TAG_NAME, String.valueOf(succeeded),
AUTHENTICATION_ENABLED_REQUIRED_TAG_NAME, String.valueOf(enabledRequired));
if (StringUtils.isNotBlank(failureReason)) {
tags = tags.and(AUTHENTICATION_FAILURE_REASON_TAG_NAME, failureReason);
}
if (StringUtils.isNotBlank(credentialType)) {
tags = tags.and(AUTHENTICATION_CREDENTIAL_TYPE_TAG_NAME, credentialType);
}
Metrics.counter(AUTHENTICATION_COUNTER_NAME, tags).increment();
} }
} }
@VisibleForTesting @VisibleForTesting
public void updateLastSeen(Account account, Device device) { public Account updateLastSeen(Account account, Device device) {
final long lastSeenOffsetSeconds = Math.abs(account.getUuid().getLeastSignificantBits()) % ChronoUnit.DAYS.getDuration().toSeconds(); final long lastSeenOffsetSeconds = Math.abs(account.getUuid().getLeastSignificantBits()) % ChronoUnit.DAYS.getDuration().toSeconds();
final long todayInMillisWithOffset = Util.todayInMillisGivenOffsetFromNow(clock, Duration.ofSeconds(lastSeenOffsetSeconds).negated()); final long todayInMillisWithOffset = Util.todayInMillisGivenOffsetFromNow(clock, Duration.ofSeconds(lastSeenOffsetSeconds).negated());
if (device.getLastSeen() < todayInMillisWithOffset) { if (device.getLastSeen() < todayInMillisWithOffset) {
DistributionSummary.builder(daysSinceLastSeenDistributionName) Metrics.summary(DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME, IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isMaster()))
.tags(IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isMaster()))
.publishPercentileHistogram()
.register(Metrics.globalRegistry)
.record(Duration.ofMillis(todayInMillisWithOffset - device.getLastSeen()).toDays()); .record(Duration.ofMillis(todayInMillisWithOffset - device.getLastSeen()).toDays());
device.setLastSeen(Util.todayInMillis(clock)); return accountsManager.updateDevice(account, device.getId(), d -> d.setLastSeen(Util.todayInMillis(clock)));
accountsManager.update(account);
} }
return account;
} }
} }

View File

@@ -33,7 +33,7 @@ public class CertificateGenerator {
this.serverCertificate = ServerCertificate.parseFrom(serverCertificate); this.serverCertificate = ServerCertificate.parseFrom(serverCertificate);
} }
public byte[] createFor(Account account, Device device, boolean includeE164) throws IOException, InvalidKeyException { public byte[] createFor(Account account, Device device, boolean includeE164) throws InvalidKeyException {
SenderCertificate.Certificate.Builder builder = SenderCertificate.Certificate.newBuilder() SenderCertificate.Certificate.Builder builder = SenderCertificate.Certificate.newBuilder()
.setSenderDevice(Math.toIntExact(device.getId())) .setSenderDevice(Math.toIntExact(device.getId()))
.setExpires(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresDays)) .setExpires(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresDays))

View File

@@ -5,36 +5,38 @@
package org.whispersystems.textsecuregcm.auth; package org.whispersystems.textsecuregcm.auth;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.util.Util;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.time.Duration;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import javax.annotation.Nullable;
import org.whispersystems.textsecuregcm.util.Util;
public class StoredVerificationCode { public class StoredVerificationCode {
@JsonProperty @JsonProperty
private String code; private final String code;
@JsonProperty @JsonProperty
private long timestamp; private final long timestamp;
@JsonProperty @JsonProperty
private String pushCode; private final String pushCode;
@JsonProperty @JsonProperty
private String twilioVerificationSid; @Nullable
private final String twilioVerificationSid;
public StoredVerificationCode() { public static final Duration EXPIRATION = Duration.ofMinutes(10);
}
public StoredVerificationCode(String code, long timestamp, String pushCode) { @JsonCreator
this(code, timestamp, pushCode, null); public StoredVerificationCode(
} @JsonProperty("code") final String code,
@JsonProperty("timestamp") final long timestamp,
@JsonProperty("pushCode") final String pushCode,
@JsonProperty("twilioVerificationSid") @Nullable final String twilioVerificationSid) {
public StoredVerificationCode(String code, long timestamp, String pushCode, String twilioVerificationSid) {
this.code = code; this.code = code;
this.timestamp = timestamp; this.timestamp = timestamp;
this.pushCode = pushCode; this.pushCode = pushCode;
@@ -58,10 +60,6 @@ public class StoredVerificationCode {
} }
public boolean isValid(String theirCodeString) { public boolean isValid(String theirCodeString) {
if (timestamp + TimeUnit.MINUTES.toMillis(10) < System.currentTimeMillis()) {
return false;
}
if (Util.isEmpty(code) || Util.isEmpty(theirCodeString)) { if (Util.isEmpty(code) || Util.isEmpty(theirCodeString)) {
return false; return false;
} }

View File

@@ -7,15 +7,15 @@ package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import javax.validation.constraints.Max; import javax.validation.constraints.Max;
import javax.validation.constraints.Min; import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.time.Duration;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
public class CircuitBreakerConfiguration { public class CircuitBreakerConfiguration {
@JsonProperty @JsonProperty
@@ -39,6 +39,9 @@ public class CircuitBreakerConfiguration {
@Min(1) @Min(1)
private long waitDurationInOpenStateInSeconds = 10; private long waitDurationInOpenStateInSeconds = 10;
@JsonProperty
private List<String> ignoredExceptions = Collections.emptyList();
public int getFailureRateThreshold() { public int getFailureRateThreshold() {
return failureRateThreshold; return failureRateThreshold;
@@ -56,6 +59,18 @@ public class CircuitBreakerConfiguration {
return waitDurationInOpenStateInSeconds; return waitDurationInOpenStateInSeconds;
} }
public List<Class> getIgnoredExceptions() {
return ignoredExceptions.stream()
.map(name -> {
try {
return Class.forName(name);
} catch (final ClassNotFoundException e) {
throw new RuntimeException(e);
}
})
.collect(Collectors.toList());
}
@VisibleForTesting @VisibleForTesting
public void setFailureRateThreshold(int failureRateThreshold) { public void setFailureRateThreshold(int failureRateThreshold) {
this.failureRateThreshold = failureRateThreshold; this.failureRateThreshold = failureRateThreshold;
@@ -76,9 +91,15 @@ public class CircuitBreakerConfiguration {
this.waitDurationInOpenStateInSeconds = seconds; this.waitDurationInOpenStateInSeconds = seconds;
} }
@VisibleForTesting
public void setIgnoredExceptions(final List<String> ignoredExceptions) {
this.ignoredExceptions = ignoredExceptions;
}
public CircuitBreakerConfig toCircuitBreakerConfig() { public CircuitBreakerConfig toCircuitBreakerConfig() {
return CircuitBreakerConfig.custom() return CircuitBreakerConfig.custom()
.failureRateThreshold(getFailureRateThreshold()) .failureRateThreshold(getFailureRateThreshold())
.ignoreExceptions(getIgnoredExceptions().toArray(new Class[0]))
.ringBufferSizeInHalfOpenState(getRingBufferSizeInHalfOpenState()) .ringBufferSizeInHalfOpenState(getRingBufferSizeInHalfOpenState())
.waitDurationInOpenState(Duration.ofSeconds(getWaitDurationInOpenStateInSeconds())) .waitDurationInOpenState(Duration.ofSeconds(getWaitDurationInOpenStateInSeconds()))
.ringBufferSizeInClosedState(getRingBufferSizeInClosedState()) .ringBufferSizeInClosedState(getRingBufferSizeInClosedState())

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.micrometer.datadog.DatadogConfig;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.Duration;
public class DatadogConfiguration implements DatadogConfig {
@JsonProperty
@NotBlank
private String apiKey;
@JsonProperty
@NotNull
private Duration step = Duration.ofSeconds(10);
@JsonProperty
@NotBlank
private String environment;
@JsonProperty
@Min(1)
private int batchSize = 5_000;
@Override
public String apiKey() {
return apiKey;
}
@Override
public Duration step() {
return step;
}
public String getEnvironment() {
return environment;
}
@Override
public int batchSize() {
return batchSize;
}
@Override
public String hostTag() {
return "host";
}
@Override
public String get(final String key) {
return null;
}
}

View File

@@ -0,0 +1,13 @@
package org.whispersystems.textsecuregcm.configuration;
import javax.validation.constraints.NotNull;
public class DeletedAccountsDynamoDbConfiguration extends DynamoDbConfiguration {
@NotNull
private String needsReconciliationIndexName;
public String getNeedsReconciliationIndexName() {
return needsReconciliationIndexName;
}
}

View File

@@ -5,13 +5,12 @@
package org.whispersystems.textsecuregcm.configuration; package org.whispersystems.textsecuregcm.configuration;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import java.time.Duration; import java.time.Duration;
import javax.validation.Valid;
public class MessageDynamoDbConfiguration extends DynamoDbConfiguration { public class MessageDynamoDbConfiguration extends DynamoDbConfiguration {
private Duration timeToLive = Duration.ofDays(7); private Duration timeToLive = Duration.ofDays(14);
@Valid @Valid
public Duration getTimeToLive() { public Duration getTimeToLive() {

View File

@@ -9,7 +9,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.Positive; import javax.validation.constraints.Positive;
public class MicrometerConfiguration { public class WavefrontConfiguration {
@JsonProperty @JsonProperty
private String uri; private String uri;

View File

@@ -23,6 +23,12 @@ public class DynamicAccountsDynamoDbMigrationConfiguration {
@JsonProperty @JsonProperty
boolean logMismatches; boolean logMismatches;
@JsonProperty
boolean dynamoCrawlerEnabled;
@JsonProperty
int dynamoCrawlerScanPageSize = 10;
public boolean isBackgroundMigrationEnabled() { public boolean isBackgroundMigrationEnabled() {
return backgroundMigrationEnabled; return backgroundMigrationEnabled;
} }
@@ -59,4 +65,12 @@ public class DynamicAccountsDynamoDbMigrationConfiguration {
public boolean isLogMismatches() { public boolean isLogMismatches() {
return logMismatches; return logMismatches;
} }
public boolean isDynamoCrawlerEnabled() {
return dynamoCrawlerEnabled;
}
public int getDynamoCrawlerScanPageSize() {
return dynamoCrawlerScanPageSize;
}
} }

View File

@@ -22,7 +22,6 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.validation.Valid; import javax.validation.Valid;
@@ -68,15 +67,13 @@ import org.whispersystems.textsecuregcm.push.GcmMessage;
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient; import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
import org.whispersystems.textsecuregcm.sms.SmsSender; import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager; import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.AbusiveHostRule; import org.whispersystems.textsecuregcm.storage.AbusiveHostRule;
import org.whispersystems.textsecuregcm.storage.AbusiveHostRules; import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.storage.UsernamesManager; import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.ForwardedIpUtil; import org.whispersystems.textsecuregcm.util.ForwardedIpUtil;
@@ -90,7 +87,6 @@ public class AccountController {
private final Logger logger = LoggerFactory.getLogger(AccountController.class); private final Logger logger = LoggerFactory.getLogger(AccountController.class);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter newUserMeter = metricRegistry.meter(name(AccountController.class, "brand_new_user" ));
private final Meter blockedHostMeter = metricRegistry.meter(name(AccountController.class, "blocked_host" )); private final Meter blockedHostMeter = metricRegistry.meter(name(AccountController.class, "blocked_host" ));
private final Meter filteredHostMeter = metricRegistry.meter(name(AccountController.class, "filtered_host" )); private final Meter filteredHostMeter = metricRegistry.meter(name(AccountController.class, "filtered_host" ));
private final Meter rateLimitedHostMeter = metricRegistry.meter(name(AccountController.class, "rate_limited_host" )); private final Meter rateLimitedHostMeter = metricRegistry.meter(name(AccountController.class, "rate_limited_host" ));
@@ -112,14 +108,12 @@ public class AccountController {
private static final String VERIFY_EXPERIMENT_TAG_NAME = "twilioVerify"; private static final String VERIFY_EXPERIMENT_TAG_NAME = "twilioVerify";
private final PendingAccountsManager pendingAccounts; private final StoredVerificationCodeManager pendingAccounts;
private final AccountsManager accounts; private final AccountsManager accounts;
private final UsernamesManager usernames; private final UsernamesManager usernames;
private final AbusiveHostRules abusiveHostRules; private final AbusiveHostRules abusiveHostRules;
private final RateLimiters rateLimiters; private final RateLimiters rateLimiters;
private final SmsSender smsSender; private final SmsSender smsSender;
private final DirectoryQueue directoryQueue;
private final MessagesManager messagesManager;
private final DynamicConfigurationManager dynamicConfigurationManager; private final DynamicConfigurationManager dynamicConfigurationManager;
private final TurnTokenGenerator turnTokenGenerator; private final TurnTokenGenerator turnTokenGenerator;
private final Map<String, Integer> testDevices; private final Map<String, Integer> testDevices;
@@ -130,14 +124,12 @@ public class AccountController {
private final TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager; private final TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager;
public AccountController(PendingAccountsManager pendingAccounts, public AccountController(StoredVerificationCodeManager pendingAccounts,
AccountsManager accounts, AccountsManager accounts,
UsernamesManager usernames, UsernamesManager usernames,
AbusiveHostRules abusiveHostRules, AbusiveHostRules abusiveHostRules,
RateLimiters rateLimiters, RateLimiters rateLimiters,
SmsSender smsSenderFactory, SmsSender smsSenderFactory,
DirectoryQueue directoryQueue,
MessagesManager messagesManager,
DynamicConfigurationManager dynamicConfigurationManager, DynamicConfigurationManager dynamicConfigurationManager,
TurnTokenGenerator turnTokenGenerator, TurnTokenGenerator turnTokenGenerator,
Map<String, Integer> testDevices, Map<String, Integer> testDevices,
@@ -153,8 +145,6 @@ public class AccountController {
this.abusiveHostRules = abusiveHostRules; this.abusiveHostRules = abusiveHostRules;
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
this.smsSender = smsSenderFactory; this.smsSender = smsSenderFactory;
this.directoryQueue = directoryQueue;
this.messagesManager = messagesManager;
this.dynamicConfigurationManager = dynamicConfigurationManager; this.dynamicConfigurationManager = dynamicConfigurationManager;
this.testDevices = testDevices; this.testDevices = testDevices;
this.turnTokenGenerator = turnTokenGenerator; this.turnTokenGenerator = turnTokenGenerator;
@@ -170,7 +160,8 @@ public class AccountController {
@Path("/{type}/preauth/{token}/{number}") @Path("/{type}/preauth/{token}/{number}")
public Response getPreAuth(@PathParam("type") String pushType, public Response getPreAuth(@PathParam("type") String pushType,
@PathParam("token") String pushToken, @PathParam("token") String pushToken,
@PathParam("number") String number) @PathParam("number") String number,
@QueryParam("voip") Optional<Boolean> useVoip)
{ {
if (!"apn".equals(pushType) && !"fcm".equals(pushType)) { if (!"apn".equals(pushType) && !"fcm".equals(pushType)) {
return Response.status(400).build(); return Response.status(400).build();
@@ -183,14 +174,15 @@ public class AccountController {
String pushChallenge = generatePushChallenge(); String pushChallenge = generatePushChallenge();
StoredVerificationCode storedVerificationCode = new StoredVerificationCode(null, StoredVerificationCode storedVerificationCode = new StoredVerificationCode(null,
System.currentTimeMillis(), System.currentTimeMillis(),
pushChallenge); pushChallenge,
null);
pendingAccounts.store(number, storedVerificationCode); pendingAccounts.store(number, storedVerificationCode);
if ("fcm".equals(pushType)) { if ("fcm".equals(pushType)) {
gcmSender.sendMessage(new GcmMessage(pushToken, number, 0, GcmMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode()))); gcmSender.sendMessage(new GcmMessage(pushToken, number, 0, GcmMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode())));
} else if ("apn".equals(pushType)) { } else if ("apn".equals(pushType)) {
apnSender.sendMessage(new ApnMessage(pushToken, number, 0, true, ApnMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode()))); apnSender.sendMessage(new ApnMessage(pushToken, number, 0, useVoip.orElse(true), ApnMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode())));
} else { } else {
throw new AssertionError(); throw new AssertionError();
} }
@@ -216,10 +208,6 @@ public class AccountController {
throw new WebApplicationException(Response.status(400).build()); throw new WebApplicationException(Response.status(400).build());
} }
if (number.startsWith("+98")) {
transport = "voice";
}
String requester = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow(); String requester = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow();
Optional<StoredVerificationCode> storedChallenge = pendingAccounts.getCodeForNumber(number); Optional<StoredVerificationCode> storedChallenge = pendingAccounts.getCodeForNumber(number);
@@ -316,6 +304,7 @@ public class AccountController {
}); });
}); });
// TODO Remove this meter when external dependencies have been resolved
metricRegistry.meter(name(AccountController.class, "create", Util.getCountryCode(number))).mark(); metricRegistry.meter(name(AccountController.class, "create", Util.getCountryCode(number))).mark();
{ {
@@ -390,7 +379,7 @@ public class AccountController {
throw new WebApplicationException(Response.status(409).build()); throw new WebApplicationException(Response.status(409).build());
} }
Account account = createAccount(number, password, signalAgent, accountAttributes); Account account = accounts.create(number, password, signalAgent, accountAttributes);
{ {
metricRegistry.meter(name(AccountController.class, "verify", Util.getCountryCode(number))).mark(); metricRegistry.meter(name(AccountController.class, "verify", Util.getCountryCode(number))).mark();
@@ -429,7 +418,6 @@ public class AccountController {
public void setGcmRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid GcmRegistrationId registrationId) { public void setGcmRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid GcmRegistrationId registrationId) {
Account account = disabledPermittedAccount.getAccount(); Account account = disabledPermittedAccount.getAccount();
Device device = account.getAuthenticatedDevice().get(); Device device = account.getAuthenticatedDevice().get();
boolean wasAccountEnabled = account.isEnabled();
if (device.getGcmId() != null && if (device.getGcmId() != null &&
device.getGcmId().equals(registrationId.getGcmRegistrationId())) device.getGcmId().equals(registrationId.getGcmRegistrationId()))
@@ -437,16 +425,12 @@ public class AccountController {
return; return;
} }
device.setApnId(null); accounts.updateDevice(account, device.getId(), d -> {
device.setVoipApnId(null); d.setApnId(null);
device.setGcmId(registrationId.getGcmRegistrationId()); d.setVoipApnId(null);
device.setFetchesMessages(false); d.setGcmId(registrationId.getGcmRegistrationId());
d.setFetchesMessages(false);
accounts.update(account); });
if (!wasAccountEnabled && account.isEnabled()) {
directoryQueue.refreshRegisteredUser(account);
}
} }
@Timed @Timed
@@ -455,12 +439,12 @@ public class AccountController {
public void deleteGcmRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount) { public void deleteGcmRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount) {
Account account = disabledPermittedAccount.getAccount(); Account account = disabledPermittedAccount.getAccount();
Device device = account.getAuthenticatedDevice().get(); Device device = account.getAuthenticatedDevice().get();
device.setGcmId(null);
device.setFetchesMessages(false);
device.setUserAgent("OWA");
accounts.update(account); accounts.updateDevice(account, device.getId(), d -> {
directoryQueue.refreshRegisteredUser(account); d.setGcmId(null);
d.setFetchesMessages(false);
d.setUserAgent("OWA");
});
} }
@Timed @Timed
@@ -470,17 +454,13 @@ public class AccountController {
public void setApnRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid ApnRegistrationId registrationId) { public void setApnRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid ApnRegistrationId registrationId) {
Account account = disabledPermittedAccount.getAccount(); Account account = disabledPermittedAccount.getAccount();
Device device = account.getAuthenticatedDevice().get(); Device device = account.getAuthenticatedDevice().get();
boolean wasAccountEnabled = account.isEnabled();
device.setApnId(registrationId.getApnRegistrationId()); accounts.updateDevice(account, device.getId(), d -> {
device.setVoipApnId(registrationId.getVoipRegistrationId()); d.setApnId(registrationId.getApnRegistrationId());
device.setGcmId(null); d.setVoipApnId(registrationId.getVoipRegistrationId());
device.setFetchesMessages(false); d.setGcmId(null);
accounts.update(account); d.setFetchesMessages(false);
});
if (!wasAccountEnabled && account.isEnabled()) {
directoryQueue.refreshRegisteredUser(account);
}
} }
@Timed @Timed
@@ -489,16 +469,16 @@ public class AccountController {
public void deleteApnRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount) { public void deleteApnRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount) {
Account account = disabledPermittedAccount.getAccount(); Account account = disabledPermittedAccount.getAccount();
Device device = account.getAuthenticatedDevice().get(); Device device = account.getAuthenticatedDevice().get();
device.setApnId(null);
device.setFetchesMessages(false);
if (device.getId() == 1) {
device.setUserAgent("OWI");
} else {
device.setUserAgent("OWP");
}
accounts.update(account); accounts.updateDevice(account, device.getId(), d -> {
directoryQueue.refreshRegisteredUser(account); d.setApnId(null);
d.setFetchesMessages(false);
if (d.getId() == 1) {
d.setUserAgent("OWI");
} else {
d.setUserAgent("OWP");
}
});
} }
@Timed @Timed
@@ -507,37 +487,43 @@ public class AccountController {
@Path("/registration_lock") @Path("/registration_lock")
public void setRegistrationLock(@Auth Account account, @Valid RegistrationLock accountLock) { public void setRegistrationLock(@Auth Account account, @Valid RegistrationLock accountLock) {
AuthenticationCredentials credentials = new AuthenticationCredentials(accountLock.getRegistrationLock()); AuthenticationCredentials credentials = new AuthenticationCredentials(accountLock.getRegistrationLock());
account.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt());
account.setPin(null);
accounts.update(account); accounts.update(account, a -> {
a.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt());
a.setPin(null);
});
} }
@Timed @Timed
@DELETE @DELETE
@Path("/registration_lock") @Path("/registration_lock")
public void removeRegistrationLock(@Auth Account account) { public void removeRegistrationLock(@Auth Account account) {
account.setRegistrationLock(null, null); accounts.update(account, a -> a.setRegistrationLock(null, null));
accounts.update(account);
} }
@Timed @Timed
@PUT @PUT
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Path("/pin/") @Path("/pin/")
public void setPin(@Auth Account account, @Valid DeprecatedPin accountLock) { public void setPin(@Auth Account account, @Valid DeprecatedPin accountLock, @HeaderParam("User-Agent") String userAgent) {
account.setPin(accountLock.getPin()); // TODO Remove once PIN-based reglocks have been deprecated
account.setRegistrationLock(null, null); logger.info("PIN set by User-Agent: {}", userAgent);
accounts.update(account); accounts.update(account, a -> {
a.setPin(accountLock.getPin());
a.setRegistrationLock(null, null);
});
} }
@Timed @Timed
@DELETE @DELETE
@Path("/pin/") @Path("/pin/")
public void removePin(@Auth Account account) {
account.setPin(null); public void removePin(@Auth Account account, @HeaderParam("User-Agent") String userAgent) {
accounts.update(account); // TODO Remove once PIN-based reglocks have been deprecated
logger.info("PIN removed by User-Agent: {}", userAgent);
accounts.update(account, a -> a.setPin(null));
} }
@Timed @Timed
@@ -545,8 +531,8 @@ public class AccountController {
@Path("/name/") @Path("/name/")
public void setName(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid DeviceName deviceName) { public void setName(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid DeviceName deviceName) {
Account account = disabledPermittedAccount.getAccount(); Account account = disabledPermittedAccount.getAccount();
account.getAuthenticatedDevice().get().setName(deviceName.getDeviceName()); Device device = account.getAuthenticatedDevice().get();
accounts.update(account); accounts.updateDevice(account, device.getId(), d -> d.setName(deviceName.getDeviceName()));
} }
@Timed @Timed
@@ -564,28 +550,25 @@ public class AccountController {
@Valid AccountAttributes attributes) @Valid AccountAttributes attributes)
{ {
Account account = disabledPermittedAccount.getAccount(); Account account = disabledPermittedAccount.getAccount();
Device device = account.getAuthenticatedDevice().get(); long deviceId = account.getAuthenticatedDevice().get().getId();
device.setFetchesMessages(attributes.getFetchesMessages()); accounts.update(account, a-> {
device.setName(attributes.getName());
device.setLastSeen(Util.todayInMillis());
device.setCapabilities(attributes.getCapabilities());
device.setRegistrationId(attributes.getRegistrationId());
device.setUserAgent(userAgent);
setAccountRegistrationLockFromAttributes(account, attributes); a.getDevice(deviceId).ifPresent(d -> {
d.setFetchesMessages(attributes.getFetchesMessages());
d.setName(attributes.getName());
d.setLastSeen(Util.todayInMillis());
d.setCapabilities(attributes.getCapabilities());
d.setRegistrationId(attributes.getRegistrationId());
d.setUserAgent(userAgent);
});
final boolean hasDiscoverabilityChange = (account.isDiscoverableByPhoneNumber() != attributes.isDiscoverableByPhoneNumber()); a.setRegistrationLockFromAttributes(attributes);
account.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey()); a.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey());
account.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess()); a.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess());
account.setDiscoverableByPhoneNumber(attributes.isDiscoverableByPhoneNumber()); a.setDiscoverableByPhoneNumber(attributes.isDiscoverableByPhoneNumber());
});
accounts.update(account);
if (hasDiscoverabilityChange) {
directoryQueue.refreshRegisteredUser(account);
}
} }
@GET @GET
@@ -723,7 +706,7 @@ public class AccountController {
@Timed @Timed
@DELETE @DELETE
@Path("/me") @Path("/me")
public void deleteAccount(@Auth Account account) { public void deleteAccount(@Auth Account account) throws InterruptedException {
accounts.delete(account, AccountsManager.DeletionReason.USER_REQUEST); accounts.delete(account, AccountsManager.DeletionReason.USER_REQUEST);
} }
@@ -737,52 +720,6 @@ public class AccountController {
return false; return false;
} }
private Account createAccount(String number, String password, String signalAgent, AccountAttributes accountAttributes) {
Optional<Account> maybeExistingAccount = accounts.get(number);
Device device = new Device();
device.setId(Device.MASTER_ID);
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
device.setName(accountAttributes.getName());
device.setCapabilities(accountAttributes.getCapabilities());
device.setCreated(System.currentTimeMillis());
device.setLastSeen(Util.todayInMillis());
device.setUserAgent(signalAgent);
Account account = new Account();
account.setNumber(number);
account.setUuid(UUID.randomUUID());
account.addDevice(device);
setAccountRegistrationLockFromAttributes(account, accountAttributes);
account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
account.setUnrestrictedUnidentifiedAccess(accountAttributes.isUnrestrictedUnidentifiedAccess());
account.setDiscoverableByPhoneNumber(accountAttributes.isDiscoverableByPhoneNumber());
if (accounts.create(account)) {
newUserMeter.mark();
}
directoryQueue.refreshRegisteredUser(account);
maybeExistingAccount.ifPresent(definitelyExistingAccount -> messagesManager.clear(definitelyExistingAccount.getUuid()));
pendingAccounts.remove(number);
return account;
}
private void setAccountRegistrationLockFromAttributes(Account account, @Valid AccountAttributes attributes) {
if (!Util.isEmpty(attributes.getPin())) {
account.setPin(attributes.getPin());
} else if (!Util.isEmpty(attributes.getRegistrationLock())) {
AuthenticationCredentials credentials = new AuthenticationCredentials(attributes.getRegistrationLock());
account.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt());
} else {
account.setPin(null);
account.setRegistrationLock(null, null);
}
}
@VisibleForTesting protected @VisibleForTesting protected
VerificationCode generateVerificationCode(String number) { VerificationCode generateVerificationCode(String number) {
if (testDevices.containsKey(number)) { if (testDevices.containsKey(number)) {

View File

@@ -5,17 +5,16 @@
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import static com.codahale.metrics.MetricRegistry.name;
import com.codahale.metrics.annotation.Timed; import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth; import io.dropwizard.auth.Auth;
import org.signal.zkgroup.auth.ServerZkAuthOperations; import io.micrometer.core.instrument.Metrics;
import org.slf4j.Logger; import java.io.IOException;
import org.slf4j.LoggerFactory; import java.security.InvalidKeyException;
import org.whispersystems.textsecuregcm.auth.CertificateGenerator; import java.util.LinkedList;
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate; import java.util.List;
import org.whispersystems.textsecuregcm.entities.GroupCredentials; import java.util.Optional;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.Util;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
@@ -24,22 +23,24 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException; import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import org.signal.zkgroup.auth.ServerZkAuthOperations;
import java.security.InvalidKeyException; import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
import java.util.LinkedList; import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
import java.util.List; import org.whispersystems.textsecuregcm.entities.GroupCredentials;
import java.util.Optional; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.Util;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v1/certificate") @Path("/v1/certificate")
public class CertificateController { public class CertificateController {
private final Logger logger = LoggerFactory.getLogger(CertificateController.class);
private final CertificateGenerator certificateGenerator; private final CertificateGenerator certificateGenerator;
private final ServerZkAuthOperations serverZkAuthOperations; private final ServerZkAuthOperations serverZkAuthOperations;
private final boolean isZkEnabled; private final boolean isZkEnabled;
private static final String GENERATE_DELIVERY_CERTIFICATE_COUNTER_NAME = name(CertificateGenerator.class, "generateCertificate");
private static final String INCLUDE_E164_TAG_NAME = "includeE164";
public CertificateController(CertificateGenerator certificateGenerator, ServerZkAuthOperations serverZkAuthOperations, boolean isZkEnabled) { public CertificateController(CertificateGenerator certificateGenerator, ServerZkAuthOperations serverZkAuthOperations, boolean isZkEnabled) {
this.certificateGenerator = certificateGenerator; this.certificateGenerator = certificateGenerator;
this.serverZkAuthOperations = serverZkAuthOperations; this.serverZkAuthOperations = serverZkAuthOperations;
@@ -51,8 +52,8 @@ public class CertificateController {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Path("/delivery") @Path("/delivery")
public DeliveryCertificate getDeliveryCertificate(@Auth Account account, public DeliveryCertificate getDeliveryCertificate(@Auth Account account,
@QueryParam("includeE164") Optional<Boolean> includeE164) @QueryParam("includeE164") Optional<Boolean> maybeIncludeE164)
throws IOException, InvalidKeyException throws InvalidKeyException
{ {
if (account.getAuthenticatedDevice().isEmpty()) { if (account.getAuthenticatedDevice().isEmpty()) {
throw new AssertionError(); throw new AssertionError();
@@ -61,7 +62,11 @@ public class CertificateController {
throw new WebApplicationException(Response.Status.BAD_REQUEST); throw new WebApplicationException(Response.Status.BAD_REQUEST);
} }
return new DeliveryCertificate(certificateGenerator.createFor(account, account.getAuthenticatedDevice().get(), includeE164.orElse(true))); final boolean includeE164 = maybeIncludeE164.orElse(true);
Metrics.counter(GENERATE_DELIVERY_CERTIFICATE_COUNTER_NAME, INCLUDE_E164_TAG_NAME, String.valueOf(includeE164)).increment();
return new DeliveryCertificate(certificateGenerator.createFor(account, account.getAuthenticatedDevice().get(), includeE164));
} }
@Timed @Timed

View File

@@ -23,8 +23,9 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager; import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode; import org.whispersystems.textsecuregcm.util.VerificationCode;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
@@ -55,24 +56,24 @@ public class DeviceController {
private static final int MAX_DEVICES = 6; private static final int MAX_DEVICES = 6;
private final PendingDevicesManager pendingDevices; private final StoredVerificationCodeManager pendingDevices;
private final AccountsManager accounts; private final AccountsManager accounts;
private final MessagesManager messages; private final MessagesManager messages;
private final KeysDynamoDb keys;
private final RateLimiters rateLimiters; private final RateLimiters rateLimiters;
private final Map<String, Integer> maxDeviceConfiguration; private final Map<String, Integer> maxDeviceConfiguration;
private final DirectoryQueue directoryQueue;
public DeviceController(PendingDevicesManager pendingDevices, public DeviceController(StoredVerificationCodeManager pendingDevices,
AccountsManager accounts, AccountsManager accounts,
MessagesManager messages, MessagesManager messages,
DirectoryQueue directoryQueue, KeysDynamoDb keys,
RateLimiters rateLimiters, RateLimiters rateLimiters,
Map<String, Integer> maxDeviceConfiguration) Map<String, Integer> maxDeviceConfiguration)
{ {
this.pendingDevices = pendingDevices; this.pendingDevices = pendingDevices;
this.accounts = accounts; this.accounts = accounts;
this.messages = messages; this.messages = messages;
this.directoryQueue = directoryQueue; this.keys = keys;
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
this.maxDeviceConfiguration = maxDeviceConfiguration; this.maxDeviceConfiguration = maxDeviceConfiguration;
} }
@@ -99,9 +100,10 @@ public class DeviceController {
throw new WebApplicationException(Response.Status.UNAUTHORIZED); throw new WebApplicationException(Response.Status.UNAUTHORIZED);
} }
account.removeDevice(deviceId); messages.clear(account.getUuid(), deviceId);
accounts.update(account); account = accounts.update(account, a -> a.removeDevice(deviceId));
directoryQueue.refreshRegisteredUser(account); keys.delete(account, deviceId);
// ensure any messages that came in after the first clear() are also removed
messages.clear(account.getUuid(), deviceId); messages.clear(account.getUuid(), deviceId);
} }
@@ -131,6 +133,7 @@ public class DeviceController {
VerificationCode verificationCode = generateVerificationCode(); VerificationCode verificationCode = generateVerificationCode();
StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(), StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(),
System.currentTimeMillis(), System.currentTimeMillis(),
null,
null); null);
pendingDevices.store(account.getNumber(), storedVerificationCode); pendingDevices.store(account.getNumber(), storedVerificationCode);
@@ -189,15 +192,16 @@ public class DeviceController {
device.setName(accountAttributes.getName()); device.setName(accountAttributes.getName());
device.setAuthenticationCredentials(new AuthenticationCredentials(password)); device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setFetchesMessages(accountAttributes.getFetchesMessages()); device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setId(account.get().getNextDeviceId());
device.setRegistrationId(accountAttributes.getRegistrationId()); device.setRegistrationId(accountAttributes.getRegistrationId());
device.setLastSeen(Util.todayInMillis()); device.setLastSeen(Util.todayInMillis());
device.setCreated(System.currentTimeMillis()); device.setCreated(System.currentTimeMillis());
device.setCapabilities(accountAttributes.getCapabilities()); device.setCapabilities(accountAttributes.getCapabilities());
account.get().addDevice(device); accounts.update(account.get(), a -> {
messages.clear(account.get().getUuid(), device.getId()); device.setId(account.get().getNextDeviceId());
accounts.update(account.get()); messages.clear(account.get().getUuid(), device.getId());
a.addDevice(device);
});;
pendingDevices.remove(number); pendingDevices.remove(number);
@@ -221,8 +225,8 @@ public class DeviceController {
@Path("/capabilities") @Path("/capabilities")
public void setCapabiltities(@Auth Account account, @Valid DeviceCapabilities capabilities) { public void setCapabiltities(@Auth Account account, @Valid DeviceCapabilities capabilities) {
assert(account.getAuthenticatedDevice().isPresent()); assert(account.getAuthenticatedDevice().isPresent());
account.getAuthenticatedDevice().get().setCapabilities(capabilities); final long deviceId = account.getAuthenticatedDevice().get().getId();
accounts.update(account); accounts.updateDevice(account, deviceId, d -> d.setCapabilities(capabilities));
} }
@VisibleForTesting protected VerificationCode generateVerificationCode() { @VisibleForTesting protected VerificationCode generateVerificationCode() {
@@ -234,9 +238,9 @@ public class DeviceController {
private boolean isCapabilityDowngrade(Account account, DeviceCapabilities capabilities, String userAgent) { private boolean isCapabilityDowngrade(Account account, DeviceCapabilities capabilities, String userAgent) {
boolean isDowngrade = false; boolean isDowngrade = false;
if (account.isGv1MigrationSupported() && !capabilities.isGv1Migration()) { isDowngrade |= account.isAnnouncementGroupSupported() && !capabilities.isAnnouncementGroup();
isDowngrade = true; isDowngrade |= account.isSenderKeySupported() && !capabilities.isSenderKey();
} isDowngrade |= account.isGv1MigrationSupported() && !capabilities.isGv1Migration();
if (account.isGroupsV2Supported()) { if (account.isGroupsV2Supported()) {
try { try {

View File

@@ -55,7 +55,6 @@ public class KeysController {
private final RateLimiters rateLimiters; private final RateLimiters rateLimiters;
private final KeysDynamoDb keysDynamoDb; private final KeysDynamoDb keysDynamoDb;
private final AccountsManager accounts; private final AccountsManager accounts;
private final DirectoryQueue directoryQueue;
private final PreKeyRateLimiter preKeyRateLimiter; private final PreKeyRateLimiter preKeyRateLimiter;
private final DynamicConfigurationManager dynamicConfigurationManager; private final DynamicConfigurationManager dynamicConfigurationManager;
@@ -69,13 +68,12 @@ public class KeysController {
private static final String PREKEY_TARGET_IDENTIFIER_TAG_NAME = "identifierType"; private static final String PREKEY_TARGET_IDENTIFIER_TAG_NAME = "identifierType";
public KeysController(RateLimiters rateLimiters, KeysDynamoDb keysDynamoDb, AccountsManager accounts, public KeysController(RateLimiters rateLimiters, KeysDynamoDb keysDynamoDb, AccountsManager accounts,
DirectoryQueue directoryQueue, PreKeyRateLimiter preKeyRateLimiter, PreKeyRateLimiter preKeyRateLimiter,
DynamicConfigurationManager dynamicConfigurationManager, DynamicConfigurationManager dynamicConfigurationManager,
RateLimitChallengeManager rateLimitChallengeManager) { RateLimitChallengeManager rateLimitChallengeManager) {
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
this.keysDynamoDb = keysDynamoDb; this.keysDynamoDb = keysDynamoDb;
this.accounts = accounts; this.accounts = accounts;
this.directoryQueue = directoryQueue;
this.preKeyRateLimiter = preKeyRateLimiter; this.preKeyRateLimiter = preKeyRateLimiter;
this.dynamicConfigurationManager = dynamicConfigurationManager; this.dynamicConfigurationManager = dynamicConfigurationManager;
@@ -100,25 +98,21 @@ public class KeysController {
public void setKeys(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid PreKeyState preKeys) { public void setKeys(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid PreKeyState preKeys) {
Account account = disabledPermittedAccount.getAccount(); Account account = disabledPermittedAccount.getAccount();
Device device = account.getAuthenticatedDevice().get(); Device device = account.getAuthenticatedDevice().get();
boolean wasAccountEnabled = account.isEnabled();
boolean updateAccount = false; boolean updateAccount = false;
if (!preKeys.getSignedPreKey().equals(device.getSignedPreKey())) { if (!preKeys.getSignedPreKey().equals(device.getSignedPreKey())) {
device.setSignedPreKey(preKeys.getSignedPreKey());
updateAccount = true; updateAccount = true;
} }
if (!preKeys.getIdentityKey().equals(account.getIdentityKey())) { if (!preKeys.getIdentityKey().equals(account.getIdentityKey())) {
account.setIdentityKey(preKeys.getIdentityKey());
updateAccount = true; updateAccount = true;
} }
if (updateAccount) { if (updateAccount) {
accounts.update(account); account = accounts.update(account, a -> {
a.getDevice(device.getId()).ifPresent(d -> d.setSignedPreKey(preKeys.getSignedPreKey()));
if (!wasAccountEnabled && account.isEnabled()) { a.setIdentityKey(preKeys.getIdentityKey());
directoryQueue.refreshRegisteredUser(account); });
}
} }
keysDynamoDb.store(account, device.getId(), preKeys.getPreKeys()); keysDynamoDb.store(account, device.getId(), preKeys.getPreKeys());
@@ -197,15 +191,9 @@ public class KeysController {
@Path("/signed") @Path("/signed")
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public void setSignedKey(@Auth Account account, @Valid SignedPreKey signedPreKey) { public void setSignedKey(@Auth Account account, @Valid SignedPreKey signedPreKey) {
Device device = account.getAuthenticatedDevice().get(); Device device = account.getAuthenticatedDevice().get();
boolean wasAccountEnabled = account.isEnabled();
device.setSignedPreKey(signedPreKey); accounts.updateDevice(account, device.getId(), d -> d.setSignedPreKey(signedPreKey));
accounts.update(account);
if (!wasAccountEnabled && account.isEnabled()) {
directoryQueue.refreshRegisteredUser(account);
}
} }
@Timed @Timed

View File

@@ -56,6 +56,7 @@ import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import io.micrometer.core.instrument.Tags;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -63,6 +64,7 @@ import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
import org.whispersystems.textsecuregcm.auth.Anonymous; import org.whispersystems.textsecuregcm.auth.Anonymous;
import org.whispersystems.textsecuregcm.auth.CombinedUnidentifiedSenderAccessKeys; import org.whispersystems.textsecuregcm.auth.CombinedUnidentifiedSenderAccessKeys;
import org.whispersystems.textsecuregcm.auth.OptionalAccess; import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicMessageRateConfiguration; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicMessageRateConfiguration;
import org.whispersystems.textsecuregcm.entities.AccountMismatchedDevices; import org.whispersystems.textsecuregcm.entities.AccountMismatchedDevices;
import org.whispersystems.textsecuregcm.entities.AccountStaleDevices; import org.whispersystems.textsecuregcm.entities.AccountStaleDevices;
@@ -76,6 +78,7 @@ import org.whispersystems.textsecuregcm.entities.MultiRecipientMessage.Recipient
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
import org.whispersystems.textsecuregcm.entities.SendMessageResponse; import org.whispersystems.textsecuregcm.entities.SendMessageResponse;
import org.whispersystems.textsecuregcm.entities.SendMultiRecipientMessageResponse;
import org.whispersystems.textsecuregcm.entities.StaleDevices; import org.whispersystems.textsecuregcm.entities.StaleDevices;
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeException; import org.whispersystems.textsecuregcm.limits.RateLimitChallengeException;
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager; import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
@@ -133,6 +136,7 @@ public class MessageController {
private final ClusterLuaScript recordInternationalUnsealedSenderMetricsScript; private final ClusterLuaScript recordInternationalUnsealedSenderMetricsScript;
private static final String LEGACY_MESSAGE_SENT_COUNTER = name(MessageController.class, "legacyMessageSent");
private static final String SENT_MESSAGE_COUNTER_NAME = name(MessageController.class, "sentMessages"); private static final String SENT_MESSAGE_COUNTER_NAME = name(MessageController.class, "sentMessages");
private static final String REJECT_UNSEALED_SENDER_COUNTER_NAME = name(MessageController.class, "rejectUnsealedSenderLimit"); private static final String REJECT_UNSEALED_SENDER_COUNTER_NAME = name(MessageController.class, "rejectUnsealedSenderLimit");
private static final String INTERNATIONAL_UNSEALED_SENDER_COUNTER_NAME = name(MessageController.class, "internationalUnsealedSender"); private static final String INTERNATIONAL_UNSEALED_SENDER_COUNTER_NAME = name(MessageController.class, "internationalUnsealedSender");
@@ -144,6 +148,7 @@ public class MessageController {
private static final String EPHEMERAL_TAG_NAME = "ephemeral"; private static final String EPHEMERAL_TAG_NAME = "ephemeral";
private static final String SENDER_TYPE_TAG_NAME = "senderType"; private static final String SENDER_TYPE_TAG_NAME = "senderType";
private static final String SENDER_COUNTRY_TAG_NAME = "senderCountry"; private static final String SENDER_COUNTRY_TAG_NAME = "senderCountry";
private static final String DESTINATION_TYPE_TAG_NAME = "destinationType";
private static final long MAX_MESSAGE_SIZE = DataSize.kibibytes(256).toBytes(); private static final long MAX_MESSAGE_SIZE = DataSize.kibibytes(256).toBytes();
@@ -230,7 +235,7 @@ public class MessageController {
contentLength += message.getBody().length(); contentLength += message.getBody().length();
} }
Metrics.summary(CONTENT_SIZE_DISTRIBUTION_NAME, UserAgentTagUtil.getUserAgentTags(userAgent)).record(contentLength); Metrics.summary(CONTENT_SIZE_DISTRIBUTION_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).record(contentLength);
if (contentLength > MAX_MESSAGE_SIZE) { if (contentLength > MAX_MESSAGE_SIZE) {
rejectOver256kibMessageMeter.mark(); rejectOver256kibMessageMeter.mark();
@@ -300,7 +305,8 @@ public class MessageController {
final List<Tag> tags = List.of(UserAgentTagUtil.getPlatformTag(userAgent), final List<Tag> tags = List.of(UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(messages.isOnline())), Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(messages.isOnline())),
Tag.of(SENDER_TYPE_TAG_NAME, senderType)); Tag.of(SENDER_TYPE_TAG_NAME, senderType),
Tag.of(DESTINATION_TYPE_TAG_NAME, destinationName.hasNumber() ? "e164" : "uuid"));
for (IncomingMessage incomingMessage : messages.getMessages()) { for (IncomingMessage incomingMessage : messages.getMessages()) {
Optional<Device> destinationDevice = destination.get().getDevice(incomingMessage.getDestinationDeviceId()); Optional<Device> destinationDevice = destination.get().getDevice(incomingMessage.getDestinationDeviceId());
@@ -417,7 +423,7 @@ public class MessageController {
uuids404.add(destinationAccount.getUuid()); uuids404.add(destinationAccount.getUuid());
} }
} }
return Response.ok(new SendMessageResponse(uuids404)).build(); return Response.ok(new SendMultiRecipientMessageResponse(uuids404)).build();
} }
private void checkAccessKeys(CombinedUnidentifiedSenderAccessKeys accessKeys, Map<UUID, Account> uuidToAccountMap) { private void checkAccessKeys(CombinedUnidentifiedSenderAccessKeys accessKeys, Map<UUID, Account> uuidToAccountMap) {
@@ -493,6 +499,11 @@ public class MessageController {
public OutgoingMessageEntityList getPendingMessages(@Auth Account account, @HeaderParam("User-Agent") String userAgent) { public OutgoingMessageEntityList getPendingMessages(@Auth Account account, @HeaderParam("User-Agent") String userAgent) {
assert account.getAuthenticatedDevice().isPresent(); assert account.getAuthenticatedDevice().isPresent();
// TODO Remove once PIN-based reglocks have been deprecated
if (account.getRegistrationLock().requiresClientRegistrationLock() && account.getRegistrationLock().hasDeprecatedPin()) {
logger.info("User-Agent with deprecated PIN-based registration lock: {}", userAgent);
}
if (!Util.isEmpty(account.getAuthenticatedDevice().get().getApnId())) { if (!Util.isEmpty(account.getAuthenticatedDevice().get().getApnId())) {
RedisOperation.unchecked(() -> apnFallbackManager.cancel(account, account.getAuthenticatedDevice().get())); RedisOperation.unchecked(() -> apnFallbackManager.cancel(account, account.getAuthenticatedDevice().get()));
} }
@@ -547,7 +558,7 @@ public class MessageController {
account.getAuthenticatedDevice().get().getId(), account.getAuthenticatedDevice().get().getId(),
source, timestamp); source, timestamp);
if (message.isPresent() && message.get().getType() != Envelope.Type.RECEIPT_VALUE) { if (message.isPresent() && message.get().getType() != Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE) {
receiptSender.sendReceipt(account, receiptSender.sendReceipt(account,
message.get().getSource(), message.get().getSource(),
message.get().getTimestamp()); message.get().getTimestamp());
@@ -569,7 +580,7 @@ public class MessageController {
if (message.isPresent()) { if (message.isPresent()) {
WebSocketConnection.recordMessageDeliveryDuration(message.get().getTimestamp(), account.getAuthenticatedDevice().get()); WebSocketConnection.recordMessageDeliveryDuration(message.get().getTimestamp(), account.getAuthenticatedDevice().get());
if (!Util.isEmpty(message.get().getSource()) && message.get().getType() != Envelope.Type.RECEIPT_VALUE) { if (!Util.isEmpty(message.get().getSource()) && message.get().getType() != Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE) {
receiptSender.sendReceipt(account, message.get().getSource(), message.get().getTimestamp()); receiptSender.sendReceipt(account, message.get().getSource(), message.get().getTimestamp());
} }
} }
@@ -603,7 +614,7 @@ public class MessageController {
Optional<byte[]> messageContent = getMessageContent(incomingMessage); Optional<byte[]> messageContent = getMessageContent(incomingMessage);
Envelope.Builder messageBuilder = Envelope.newBuilder(); Envelope.Builder messageBuilder = Envelope.newBuilder();
messageBuilder.setType(Envelope.Type.valueOf(incomingMessage.getType())) messageBuilder.setType(Envelope.Type.forNumber(incomingMessage.getType()))
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp) .setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
.setServerTimestamp(System.currentTimeMillis()); .setServerTimestamp(System.currentTimeMillis());
@@ -614,6 +625,7 @@ public class MessageController {
} }
if (messageBody.isPresent()) { if (messageBody.isPresent()) {
Metrics.counter(LEGACY_MESSAGE_SENT_COUNTER).increment();
messageBuilder.setLegacyMessage(ByteString.copyFrom(messageBody.get())); messageBuilder.setLegacyMessage(ByteString.copyFrom(messageBody.get()));
} }

View File

@@ -5,7 +5,6 @@
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import com.amazonaws.services.s3.AmazonS3;
import com.codahale.metrics.annotation.Timed; import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth; import io.dropwizard.auth.Auth;
import java.security.SecureRandom; import java.security.SecureRandom;
@@ -60,6 +59,8 @@ import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.util.ExactlySize; import org.whispersystems.textsecuregcm.util.ExactlySize;
import org.whispersystems.textsecuregcm.util.Pair; import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v1/profile") @Path("/v1/profile")
@@ -78,7 +79,7 @@ public class ProfileController {
private final ServerZkProfileOperations zkProfileOperations; private final ServerZkProfileOperations zkProfileOperations;
private final boolean isZkEnabled; private final boolean isZkEnabled;
private final AmazonS3 s3client; private final S3Client s3client;
private final String bucket; private final String bucket;
public ProfileController(RateLimiters rateLimiters, public ProfileController(RateLimiters rateLimiters,
@@ -86,7 +87,7 @@ public class ProfileController {
ProfilesManager profilesManager, ProfilesManager profilesManager,
UsernamesManager usernamesManager, UsernamesManager usernamesManager,
DynamicConfigurationManager dynamicConfigurationManager, DynamicConfigurationManager dynamicConfigurationManager,
AmazonS3 s3client, S3Client s3client,
PostPolicyGenerator policyGenerator, PostPolicyGenerator policyGenerator,
PolicySigner policySigner, PolicySigner policySigner,
String bucket, String bucket,
@@ -147,15 +148,19 @@ public class ProfileController {
currentAvatar = Optional.of(account.getAvatar()); currentAvatar = Optional.of(account.getAvatar());
} }
currentAvatar.ifPresent(s -> s3client.deleteObject(bucket, s)); currentAvatar.ifPresent(s -> s3client.deleteObject(DeleteObjectRequest.builder()
.bucket(bucket)
.key(s)
.build()));
response = Optional.of(generateAvatarUploadForm(avatar)); response = Optional.of(generateAvatarUploadForm(avatar));
} }
account.setProfileName(request.getName()); accountsManager.update(account, a -> {
account.setAvatar(avatar); a.setProfileName(request.getName());
account.setCurrentProfileVersion(request.getVersion()); a.setAvatar(avatar);
accountsManager.update(account); a.setCurrentProfileVersion(request.getVersion());
});
if (response.isPresent()) return Response.ok(response).build(); if (response.isPresent()) return Response.ok(response).build();
else return Response.ok().build(); else return Response.ok().build();
@@ -313,8 +318,7 @@ public class ProfileController {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Path("/name/{name}") @Path("/name/{name}")
public void setProfile(@Auth Account account, @PathParam("name") @ExactlySize(value = {72, 108}, payload = {Unwrapping.Unwrap.class}) Optional<String> name) { public void setProfile(@Auth Account account, @PathParam("name") @ExactlySize(value = {72, 108}, payload = {Unwrapping.Unwrap.class}) Optional<String> name) {
account.setProfileName(name.orElse(null)); accountsManager.update(account, a -> a.setProfileName(name.orElse(null)));
accountsManager.update(account);
} }
@Deprecated @Deprecated
@@ -372,11 +376,13 @@ public class ProfileController {
ProfileAvatarUploadAttributes profileAvatarUploadAttributes = generateAvatarUploadForm(objectName); ProfileAvatarUploadAttributes profileAvatarUploadAttributes = generateAvatarUploadForm(objectName);
if (previousAvatar != null && previousAvatar.startsWith("profiles/")) { if (previousAvatar != null && previousAvatar.startsWith("profiles/")) {
s3client.deleteObject(bucket, previousAvatar); s3client.deleteObject(DeleteObjectRequest.builder()
.bucket(bucket)
.key(previousAvatar)
.build());
} }
account.setAvatar(objectName); accountsManager.update(account, a -> a.setAvatar(objectName));
accountsManager.update(account);
return profileAvatarUploadAttributes; return profileAvatarUploadAttributes;
} }

View File

@@ -5,17 +5,16 @@
package org.whispersystems.textsecuregcm.entities; package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.List;
public class IncomingMessageList { public class IncomingMessageList {
@JsonProperty @JsonProperty
@NotNull @NotNull
@Valid @Valid
private List<IncomingMessage> messages; private List<@NotNull IncomingMessage> messages;
@JsonProperty @JsonProperty
private long timestamp; private long timestamp;

View File

@@ -6,24 +6,15 @@
package org.whispersystems.textsecuregcm.entities; package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.UUID;
public class SendMessageResponse { public class SendMessageResponse {
@JsonProperty @JsonProperty
private boolean needsSync; private boolean needsSync;
@JsonProperty
private List<UUID> uuids404;
public SendMessageResponse() {} public SendMessageResponse() {}
public SendMessageResponse(boolean needsSync) { public SendMessageResponse(boolean needsSync) {
this.needsSync = needsSync; this.needsSync = needsSync;
} }
public SendMessageResponse(List<UUID> uuids404) {
this.uuids404 = uuids404;
}
} }

View File

@@ -0,0 +1,22 @@
/*
* Copyright 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 SendMultiRecipientMessageResponse {
@JsonProperty
private List<UUID> uuids404;
public SendMultiRecipientMessageResponse() {
}
public SendMultiRecipientMessageResponse(final List<UUID> uuids404) {
this.uuids404 = uuids404;
}
}

View File

@@ -14,7 +14,8 @@ public class UserCapabilities {
return new UserCapabilities( return new UserCapabilities(
account.isGroupsV2Supported(), account.isGroupsV2Supported(),
account.isGv1MigrationSupported(), account.isGv1MigrationSupported(),
account.isSenderKeySupported()); account.isSenderKeySupported(),
account.isAnnouncementGroupSupported());
} }
@JsonProperty @JsonProperty
@@ -26,12 +27,16 @@ public class UserCapabilities {
@JsonProperty @JsonProperty
private boolean senderKey; private boolean senderKey;
@JsonProperty
private boolean announcementGroup;
public UserCapabilities() {} public UserCapabilities() {}
public UserCapabilities(boolean gv2, boolean gv1Migration, final boolean senderKey) { public UserCapabilities(boolean gv2, boolean gv1Migration, final boolean senderKey, final boolean announcementGroup) {
this.gv2 = gv2; this.gv2 = gv2;
this.gv1Migration = gv1Migration; this.gv1Migration = gv1Migration;
this.senderKey = senderKey; this.senderKey = senderKey;
this.announcementGroup = announcementGroup;
} }
public boolean isGv2() { public boolean isGv2() {
@@ -45,4 +50,8 @@ public class UserCapabilities {
public boolean isSenderKey() { public boolean isSenderKey() {
return senderKey; return senderKey;
} }
public boolean isAnnouncementGroup() {
return announcementGroup;
}
} }

View File

@@ -18,7 +18,7 @@ public class PreKeyRateLimiter {
private static final String RATE_LIMIT_RESET_COUNTER_NAME = name(PreKeyRateLimiter.class, "reset"); 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_COUNTER_NAME = name(PreKeyRateLimiter.class, "rateLimited");
private static final String RATE_LIMITED_PREKEYS_TOTAL_ACCOUNTS_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_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_PREKEYS_ACCOUNTS_UNENFORCED_COUNTER_NAME = name(PreKeyRateLimiter.class, "rateLimitedAccountsUnenforced");

View File

@@ -18,8 +18,13 @@ public class RateLimitResetMetricsManager {
} }
void initializeFunctionCounters(String counterKey, String hllKey) { void initializeFunctionCounters(String counterKey, String hllKey) {
FunctionCounter.builder(counterKey, null, (ignored) -> FunctionCounter
metricsCluster.<Long>withCluster(conn -> conn.sync().pfcount(hllKey))).register(meterRegistry); .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, void recordMetrics(Account account, boolean enforced, String counterKey, String hllEnforcedKey, String hllTotalKey,

View File

@@ -21,6 +21,7 @@ import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.HostnameUtil;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@@ -83,7 +84,7 @@ public class JsonMetricsReporter extends ScheduledReporter {
{ {
super(registry, "json-reporter", filter, rateUnit, durationUnit, Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("json-reporter")), true, disabledMetricAttributes); super(registry, "json-reporter", filter, rateUnit, durationUnit, Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("json-reporter")), true, disabledMetricAttributes);
this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build();
this.uri = UriBuilder.fromUri(uri).queryParam("h", InetAddress.getLocalHost().getHostName()).build(); this.uri = UriBuilder.fromUri(uri).queryParam("h", HostnameUtil.getLocalHostname()).build();
} }
@Override @Override

View File

@@ -26,6 +26,8 @@ import java.time.Duration;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import net.logstash.logback.appender.LogstashTcpSocketAppender; import net.logstash.logback.appender.LogstashTcpSocketAppender;
import net.logstash.logback.encoder.LogstashEncoder; import net.logstash.logback.encoder.LogstashEncoder;
import org.whispersystems.textsecuregcm.WhisperServerVersion;
import org.whispersystems.textsecuregcm.util.HostnameUtil;
@JsonTypeName("logstashtcpsocket") @JsonTypeName("logstashtcpsocket")
public class LogstashTcpSocketAppenderFactory extends AbstractAppenderFactory<ILoggingEvent> { public class LogstashTcpSocketAppenderFactory extends AbstractAppenderFactory<ILoggingEvent> {
@@ -76,14 +78,11 @@ public class LogstashTcpSocketAppenderFactory extends AbstractAppenderFactory<IL
final LogstashEncoder encoder = new LogstashEncoder(); final LogstashEncoder encoder = new LogstashEncoder();
final ObjectNode customFieldsNode = new ObjectNode(JsonNodeFactory.instance); final ObjectNode customFieldsNode = new ObjectNode(JsonNodeFactory.instance);
try { customFieldsNode.set("host", TextNode.valueOf(HostnameUtil.getLocalHostname()));
customFieldsNode.set("host", TextNode.valueOf(InetAddress.getLocalHost().getHostName()));
} catch (UnknownHostException e) {
customFieldsNode.set("host", TextNode.valueOf("unknown"));
}
customFieldsNode.set("service", TextNode.valueOf("chat")); customFieldsNode.set("service", TextNode.valueOf("chat"));
customFieldsNode.set("ddsource", TextNode.valueOf("logstash")); customFieldsNode.set("ddsource", TextNode.valueOf("logstash"));
customFieldsNode.set("ddtags", TextNode.valueOf("env:" + environment)); customFieldsNode.set("ddtags", TextNode.valueOf("env:" + environment + ",version:" + WhisperServerVersion.getServerVersion()));
encoder.setCustomFields(customFieldsNode.toString()); encoder.setCustomFields(customFieldsNode.toString());
final LayoutWrappingEncoder<ILoggingEvent> prefix = new LayoutWrappingEncoder<>(); final LayoutWrappingEncoder<ILoggingEvent> prefix = new LayoutWrappingEncoder<>();
final PatternLayout layout = new PatternLayout(); final PatternLayout layout = new PatternLayout();

View File

@@ -12,9 +12,9 @@ import com.vdurmont.semver4j.SemverException;
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tag;
import org.glassfish.jersey.server.ExtendedUriInfo;
import org.glassfish.jersey.server.monitoring.RequestEvent; import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.server.monitoring.RequestEventListener; import org.glassfish.jersey.server.monitoring.RequestEventListener;
import org.whispersystems.textsecuregcm.util.logging.UriInfoUtil;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
import org.whispersystems.textsecuregcm.util.ua.UserAgent; import org.whispersystems.textsecuregcm.util.ua.UserAgent;
@@ -29,18 +29,18 @@ import java.util.regex.Pattern;
/** /**
* Gathers and reports request-level metrics. * Gathers and reports request-level metrics.
*/ */
class MetricsRequestEventListener implements RequestEventListener { public class MetricsRequestEventListener implements RequestEventListener {
static final String REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "request"); public static final String REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "request");
static final String PATH_TAG = "path"; public static final String ANDROID_REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "androidRequest");
static final String STATUS_CODE_TAG = "status"; public static final String DESKTOP_REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "desktopRequest");
static final String TRAFFIC_SOURCE_TAG = "trafficSource"; public static final String IOS_REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "iosRequest");
static final String ANDROID_REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "androidRequest"); static final String PATH_TAG = "path";
static final String DESKTOP_REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "desktopRequest"); static final String STATUS_CODE_TAG = "status";
static final String IOS_REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "iosRequest"); static final String TRAFFIC_SOURCE_TAG = "trafficSource";
static final String OS_TAG = "os"; static final String OS_TAG = "os";
static final String SDK_TAG = "sdkVersion"; static final String SDK_TAG = "sdkVersion";
private static final Set<String> ACCEPTABLE_DESKTOP_OS_STRINGS = Set.of("linux", "macos", "windows"); private static final Set<String> ACCEPTABLE_DESKTOP_OS_STRINGS = Set.of("linux", "macos", "windows");
@@ -71,7 +71,7 @@ class MetricsRequestEventListener implements RequestEventListener {
if (event.getType() == RequestEvent.Type.FINISHED) { if (event.getType() == RequestEvent.Type.FINISHED) {
if (!event.getUriInfo().getMatchedTemplates().isEmpty()) { if (!event.getUriInfo().getMatchedTemplates().isEmpty()) {
final List<Tag> tags = new ArrayList<>(5); final List<Tag> tags = new ArrayList<>(5);
tags.add(Tag.of(PATH_TAG, getPathTemplate(event.getUriInfo()))); tags.add(Tag.of(PATH_TAG, UriInfoUtil.getPathTemplate(event.getUriInfo())));
tags.add(Tag.of(STATUS_CODE_TAG, String.valueOf(event.getContainerResponse().getStatus()))); tags.add(Tag.of(STATUS_CODE_TAG, String.valueOf(event.getContainerResponse().getStatus())));
tags.add(Tag.of(TRAFFIC_SOURCE_TAG, trafficSource.name().toLowerCase())); tags.add(Tag.of(TRAFFIC_SOURCE_TAG, trafficSource.name().toLowerCase()));
@@ -149,14 +149,4 @@ class MetricsRequestEventListener implements RequestEventListener {
} }
} }
@VisibleForTesting
static String getPathTemplate(final ExtendedUriInfo uriInfo) {
final StringBuilder pathBuilder = new StringBuilder();
for (int i = uriInfo.getMatchedTemplates().size() - 1; i >= 0; i--) {
pathBuilder.append(uriInfo.getMatchedTemplates().get(i).getTemplate());
}
return pathBuilder.toString();
}
} }

View File

@@ -10,6 +10,7 @@ import com.google.common.annotations.VisibleForTesting;
import io.lettuce.core.SetArgs; import io.lettuce.core.SetArgs;
import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import java.time.Duration; import java.time.Duration;
@@ -49,7 +50,7 @@ public class PushLatencyManager {
public void recordQueueRead(final UUID accountUuid, final long deviceId, final String userAgent) { public void recordQueueRead(final UUID accountUuid, final long deviceId, final String userAgent) {
getLatencyAndClearTimestamp(accountUuid, deviceId, System.currentTimeMillis()).thenAccept(latency -> { getLatencyAndClearTimestamp(accountUuid, deviceId, System.currentTimeMillis()).thenAccept(latency -> {
if (latency != null) { if (latency != null) {
Metrics.timer(TIMER_NAME, UserAgentTagUtil.getUserAgentTags(userAgent)).record(latency, TimeUnit.MILLISECONDS); Metrics.timer(TIMER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).record(latency, TimeUnit.MILLISECONDS);
} }
}); });
} }

View File

@@ -0,0 +1,74 @@
/*
* This is derived from Coursera's dropwizard datadog reporter.
* https://github.com/coursera/metrics-datadog
*/
package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.ScheduledReporter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.dropwizard.metrics.BaseReporterFactory;
import java.util.EnumSet;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.coursera.metrics.datadog.DatadogReporter;
import org.coursera.metrics.datadog.DatadogReporter.Expansion;
import org.coursera.metrics.datadog.DefaultMetricNameFormatterFactory;
import org.coursera.metrics.datadog.DynamicTagsCallbackFactory;
import org.coursera.metrics.datadog.MetricNameFormatterFactory;
import org.coursera.metrics.datadog.transport.AbstractTransportFactory;
import org.whispersystems.textsecuregcm.util.HostnameUtil;
@JsonTypeName("signal-datadog")
public class SignalDatadogReporterFactory extends BaseReporterFactory {
@JsonProperty
private List<String> tags = null;
@Valid
@JsonProperty
private DynamicTagsCallbackFactory dynamicTagsCallback = null;
@JsonProperty
private String prefix = null;
@Valid
@NotNull
@JsonProperty
private MetricNameFormatterFactory metricNameFormatter = new DefaultMetricNameFormatterFactory();
@Valid
@NotNull
@JsonProperty
private AbstractTransportFactory transport = null;
private static final EnumSet<Expansion> EXPANSIONS = EnumSet.of(
Expansion.COUNT,
Expansion.MIN,
Expansion.MAX,
Expansion.MEAN,
Expansion.MEDIAN,
Expansion.P75,
Expansion.P95,
Expansion.P99,
Expansion.P999
);
public ScheduledReporter build(MetricRegistry registry) {
return DatadogReporter.forRegistry(registry)
.withTransport(transport.build())
.withHost(HostnameUtil.getLocalHostname())
.withTags(tags)
.withPrefix(prefix)
.withExpansions(EXPANSIONS)
.withMetricNameFormatter(metricNameFormatter.build())
.withDynamicTagCallback(dynamicTagsCallback != null ? dynamicTagsCallback.build() : null)
.filter(getFilter())
.convertDurationsTo(getDurationUnit())
.convertRatesTo(getRateUnit())
.build();
}
}

View File

@@ -16,7 +16,8 @@ public class ApnMessage {
NOTIFICATION, CHALLENGE, RATE_LIMIT_CHALLENGE NOTIFICATION, CHALLENGE, RATE_LIMIT_CHALLENGE
} }
public static final String APN_NOTIFICATION_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}}"; public static final String APN_VOIP_NOTIFICATION_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}}";
public static final String APN_NSE_NOTIFICATION_PAYLOAD = "{\"aps\":{\"mutable-content\":1,\"alert\":{\"loc-key\":\"APN_Message\"}}}";
public static final String APN_CHALLENGE_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}, \"challenge\" : \"%s\"}"; public static final String APN_CHALLENGE_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}, \"challenge\" : \"%s\"}";
public static final String APN_RATE_LIMIT_CHALLENGE_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}, \"rateLimitChallenge\" : \"%s\"}"; public static final String APN_RATE_LIMIT_CHALLENGE_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}, \"rateLimitChallenge\" : \"%s\"}";
public static final long MAX_EXPIRATION = Integer.MAX_VALUE * 1000L; public static final long MAX_EXPIRATION = Integer.MAX_VALUE * 1000L;
@@ -48,7 +49,7 @@ public class ApnMessage {
public String getMessage() { public String getMessage() {
switch (type) { switch (type) {
case NOTIFICATION: case NOTIFICATION:
return APN_NOTIFICATION_PAYLOAD; return this.isVoip() ? APN_VOIP_NOTIFICATION_PAYLOAD : APN_NSE_NOTIFICATION_PAYLOAD;
case CHALLENGE: case CHALLENGE:
return String.format(APN_CHALLENGE_PAYLOAD, challengeData.orElseThrow(AssertionError::new)); return String.format(APN_CHALLENGE_PAYLOAD, challengeData.orElseThrow(AssertionError::new));

View File

@@ -110,8 +110,8 @@ public class GCMSender {
Device device = account.get().getDevice(message.getDeviceId()).get(); Device device = account.get().getDevice(message.getDeviceId()).get();
if (device.getUninstalledFeedbackTimestamp() == 0) { if (device.getUninstalledFeedbackTimestamp() == 0) {
device.setUninstalledFeedbackTimestamp(Util.todayInMillis()); accountsManager.updateDevice(account.get(), message.getDeviceId(), d ->
accountsManager.update(account.get()); d.setUninstalledFeedbackTimestamp(Util.todayInMillis()));
} }
} }
@@ -122,15 +122,11 @@ public class GCMSender {
logger.warn(String.format("Actually received 'CanonicalRegistrationId' ::: (canonical=%s), (original=%s)", logger.warn(String.format("Actually received 'CanonicalRegistrationId' ::: (canonical=%s), (original=%s)",
result.getCanonicalRegistrationId(), message.getGcmId())); result.getCanonicalRegistrationId(), message.getGcmId()));
Optional<Account> account = getAccountForEvent(message); getAccountForEvent(message).ifPresent(account ->
accountsManager.updateDevice(
if (account.isPresent()) { account,
//noinspection OptionalGetWithoutIsPresent message.getDeviceId(),
Device device = account.get().getDevice(message.getDeviceId()).get(); d -> d.setGcmId(result.getCanonicalRegistrationId())));
device.setGcmId(result.getCanonicalRegistrationId());
accountsManager.update(account.get());
}
canonical.mark(); canonical.mark();
} }

View File

@@ -43,7 +43,7 @@ public class ReceiptSender {
.setSourceUuid(source.getUuid().toString()) .setSourceUuid(source.getUuid().toString())
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId()) .setSourceDevice((int) source.getAuthenticatedDevice().get().getId())
.setTimestamp(messageId) .setTimestamp(messageId)
.setType(Envelope.Type.RECEIPT); .setType(Envelope.Type.SERVER_DELIVERY_RECEIPT);
if (source.getRelay().isPresent()) { if (source.getRelay().isPresent()) {
message.setRelay(source.getRelay().get()); message.setRelay(source.getRelay().get());

View File

@@ -6,34 +6,32 @@ package org.whispersystems.textsecuregcm.sqs;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
import com.amazonaws.services.sqs.model.MessageAttributeValue;
import com.amazonaws.services.sqs.model.SendMessageBatchRequest;
import com.amazonaws.services.sqs.model.SendMessageBatchRequestEntry;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer; import com.codahale.metrics.Timer;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.lifecycle.Managed;
import io.micrometer.core.instrument.Metrics;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.concurrent.atomic.AtomicInteger;
import com.google.common.collect.Iterables;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.SqsConfiguration; import org.whispersystems.textsecuregcm.configuration.SqsConfiguration;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Pair; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.exception.SdkServiceException;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.sqs.SqsAsyncClient;
import software.amazon.awssdk.services.sqs.model.MessageAttributeValue;
import software.amazon.awssdk.services.sqs.model.SendMessageRequest;
public class DirectoryQueue { public class DirectoryQueue implements Managed {
private static final Logger logger = LoggerFactory.getLogger(DirectoryQueue.class); private static final Logger logger = LoggerFactory.getLogger(DirectoryQueue.class);
@@ -42,75 +40,113 @@ public class DirectoryQueue {
private final Meter clientErrorMeter = metricRegistry.meter(name(DirectoryQueue.class, "clientError")); private final Meter clientErrorMeter = metricRegistry.meter(name(DirectoryQueue.class, "clientError"));
private final Timer sendMessageBatchTimer = metricRegistry.timer(name(DirectoryQueue.class, "sendMessageBatch")); private final Timer sendMessageBatchTimer = metricRegistry.timer(name(DirectoryQueue.class, "sendMessageBatch"));
private final List<String> queueUrls; private final List<String> queueUrls;
private final AmazonSQS sqs; private final SqsAsyncClient sqs;
public DirectoryQueue(SqsConfiguration sqsConfig) { private final AtomicInteger outstandingRequests = new AtomicInteger();
final AWSCredentials credentials = new BasicAWSCredentials(sqsConfig.getAccessKey(), sqsConfig.getAccessSecret());
final AWSStaticCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
this.queueUrls = sqsConfig.getQueueUrls(); private enum UpdateAction {
this.sqs = AmazonSQSClientBuilder.standard().withRegion(sqsConfig.getRegion()).withCredentials(credentialsProvider).build(); ADD("add"),
} DELETE("delete");
@VisibleForTesting private final String action;
DirectoryQueue(final List<String> queueUrls, final AmazonSQS sqs) {
this.queueUrls = queueUrls;
this.sqs = sqs;
}
public void refreshRegisteredUser(final Account account) { UpdateAction(final String action) {
refreshRegisteredUsers(List.of(account)); this.action = action;
} }
public void refreshRegisteredUsers(final List<Account> accounts) { public MessageAttributeValue toMessageAttributeValue() {
final List<Pair<Account, String>> accountsAndActions = accounts.stream() return MessageAttributeValue.builder().dataType("String").stringValue(action).build();
.map(account -> new Pair<>(account, account.isEnabled() && account.isDiscoverableByPhoneNumber() ? "add" : "delete"))
.collect(Collectors.toList());
sendUpdateMessages(accountsAndActions);
}
public void deleteAccount(final Account account) {
sendUpdateMessages(List.of(new Pair<>(account, "delete")));
}
private void sendUpdateMessages(final List<Pair<Account, String>> accountsAndActions) {
for (final String queueUrl : queueUrls) {
for (final List<Pair<Account, String>> partition : Iterables.partition(accountsAndActions, 10)) {
final List<SendMessageBatchRequestEntry> entries = partition.stream().map(pair -> {
final Account account = pair.first();
final String action = pair.second();
return new SendMessageBatchRequestEntry()
.withMessageBody("-")
.withId(UUID.randomUUID().toString())
.withMessageDeduplicationId(UUID.randomUUID().toString())
.withMessageGroupId(account.getNumber())
.withMessageAttributes(Map.of(
"id", new MessageAttributeValue().withDataType("String").withStringValue(account.getNumber()),
"uuid", new MessageAttributeValue().withDataType("String").withStringValue(account.getUuid().toString()),
"action", new MessageAttributeValue().withDataType("String").withStringValue(action)
));
}).collect(Collectors.toList());
final SendMessageBatchRequest sendMessageBatchRequest = new SendMessageBatchRequest()
.withQueueUrl(queueUrl)
.withEntries(entries);
try (final Timer.Context ignored = sendMessageBatchTimer.time()) {
sqs.sendMessageBatch(sendMessageBatchRequest);
} catch (AmazonServiceException ex) {
serviceErrorMeter.mark();
logger.warn("sqs service error: ", ex);
} catch (AmazonClientException ex) {
clientErrorMeter.mark();
logger.warn("sqs client error: ", ex);
} catch (Throwable t) {
logger.warn("sqs unexpected error: ", t);
}
}
} }
} }
public DirectoryQueue(SqsConfiguration sqsConfig) {
StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(AwsBasicCredentials.create(
sqsConfig.getAccessKey(), sqsConfig.getAccessSecret()));
this.queueUrls = sqsConfig.getQueueUrls();
this.sqs = SqsAsyncClient.builder()
.region(Region.of(sqsConfig.getRegion()))
.credentialsProvider(credentialsProvider)
.build();
Metrics.gauge(name(getClass(), "outstandingRequests"), outstandingRequests);
}
@VisibleForTesting
DirectoryQueue(final List<String> queueUrls, final SqsAsyncClient sqs) {
this.queueUrls = queueUrls;
this.sqs = sqs;
}
@Override
public void start() throws Exception {
}
@Override
public void stop() throws Exception {
synchronized (outstandingRequests) {
while (outstandingRequests.get() > 0) {
outstandingRequests.wait();
}
}
sqs.close();
}
public boolean isDiscoverable(final Account account) {
return account.isEnabled() && account.isDiscoverableByPhoneNumber();
}
public void refreshAccount(final Account account) {
sendUpdateMessage(account, isDiscoverable(account) ? UpdateAction.ADD : UpdateAction.DELETE);
}
public void deleteAccount(final Account account) {
sendUpdateMessage(account, UpdateAction.DELETE);
}
private void sendUpdateMessage(final Account account, final UpdateAction action) {
for (final String queueUrl : queueUrls) {
final Timer.Context timerContext = sendMessageBatchTimer.time();
final SendMessageRequest request = SendMessageRequest.builder()
.queueUrl(queueUrl)
.messageBody("-")
.messageDeduplicationId(UUID.randomUUID().toString())
.messageGroupId(account.getNumber())
.messageAttributes(Map.of(
"id", MessageAttributeValue.builder().dataType("String").stringValue(account.getNumber()).build(),
"uuid", MessageAttributeValue.builder().dataType("String").stringValue(account.getUuid().toString()).build(),
"action", action.toMessageAttributeValue()
))
.build();
synchronized (outstandingRequests) {
outstandingRequests.incrementAndGet();
}
sqs.sendMessage(request).whenComplete((response, cause) -> {
try {
if (cause instanceof SdkServiceException) {
serviceErrorMeter.mark();
logger.warn("sqs service error", cause);
} else if (cause instanceof SdkClientException) {
clientErrorMeter.mark();
logger.warn("sqs client error", cause);
} else if (cause != null) {
logger.warn("sqs unexpected error", cause);
}
} finally {
synchronized (outstandingRequests) {
outstandingRequests.decrementAndGet();
outstandingRequests.notifyAll();
}
timerContext.close();
}
});
}
}
} }

View File

@@ -5,93 +5,91 @@
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import com.amazonaws.services.dynamodbv2.document.BatchWriteItemOutcome;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.Page;
import com.amazonaws.services.dynamodbv2.document.QueryOutcome;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.dynamodbv2.document.TableWriteItems;
import com.amazonaws.services.dynamodbv2.document.spec.QuerySpec;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import static io.micrometer.core.instrument.Metrics.counter; import static io.micrometer.core.instrument.Metrics.counter;
import static io.micrometer.core.instrument.Metrics.timer; import static io.micrometer.core.instrument.Metrics.timer;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Timer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemRequest;
import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemResponse;
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
import software.amazon.awssdk.services.dynamodb.model.WriteRequest;
public class AbstractDynamoDbStore { public class AbstractDynamoDbStore {
private final DynamoDB dynamoDb; private final DynamoDbClient dynamoDbClient;
private final Timer batchWriteItemsFirstPass = timer(name(getClass(), "batchWriteItems"), "firstAttempt", "true"); private final Timer batchWriteItemsFirstPass = timer(name(getClass(), "batchWriteItems"), "firstAttempt", "true");
private final Timer batchWriteItemsRetryPass = timer(name(getClass(), "batchWriteItems"), "firstAttempt", "false"); private final Timer batchWriteItemsRetryPass = timer(name(getClass(), "batchWriteItems"), "firstAttempt", "false");
private final Counter batchWriteItemsUnprocessed = counter(name(getClass(), "batchWriteItemsUnprocessed")); private final Counter batchWriteItemsUnprocessed = counter(name(getClass(), "batchWriteItemsUnprocessed"));
private final Logger logger = LoggerFactory.getLogger(getClass()); private final Logger logger = LoggerFactory.getLogger(getClass());
private static final int MAX_ATTEMPTS_TO_SAVE_BATCH_WRITE = 25; // This was arbitrarily chosen and may be entirely too high. private static final int MAX_ATTEMPTS_TO_SAVE_BATCH_WRITE = 25; // This was arbitrarily chosen and may be entirely too high.
public static final int DYNAMO_DB_MAX_BATCH_SIZE = 25; // This limit comes from Amazon Dynamo DB itself. It will reject batch writes larger than this. public static final int DYNAMO_DB_MAX_BATCH_SIZE = 25; // This limit comes from Amazon Dynamo DB itself. It will reject batch writes larger than this.
public static final int RESULT_SET_CHUNK_SIZE = 100; public static final int RESULT_SET_CHUNK_SIZE = 100;
public AbstractDynamoDbStore(final DynamoDB dynamoDb) { public AbstractDynamoDbStore(final DynamoDbClient dynamoDbClient) {
this.dynamoDb = dynamoDb; this.dynamoDbClient = dynamoDbClient;
}
protected DynamoDbClient db() {
return dynamoDbClient;
}
protected void executeTableWriteItemsUntilComplete(final Map<String, List<WriteRequest>> items) {
AtomicReference<BatchWriteItemResponse> outcome = new AtomicReference<>();
batchWriteItemsFirstPass.record(
() -> outcome.set(dynamoDbClient.batchWriteItem(BatchWriteItemRequest.builder().requestItems(items).build())));
int attemptCount = 0;
while (!outcome.get().unprocessedItems().isEmpty() && attemptCount < MAX_ATTEMPTS_TO_SAVE_BATCH_WRITE) {
batchWriteItemsRetryPass.record(() -> outcome.set(dynamoDbClient.batchWriteItem(BatchWriteItemRequest.builder()
.requestItems(outcome.get().unprocessedItems())
.build())));
++attemptCount;
} }
if (!outcome.get().unprocessedItems().isEmpty()) {
protected DynamoDB getDynamoDb() { int totalItems = outcome.get().unprocessedItems().values().stream().mapToInt(List::size).sum();
return dynamoDb; logger.error(
"Attempt count ({}) reached max ({}}) before applying all batch writes to dynamo. {} unprocessed items remain.",
attemptCount, MAX_ATTEMPTS_TO_SAVE_BATCH_WRITE, totalItems);
batchWriteItemsUnprocessed.increment(totalItems);
} }
}
protected void executeTableWriteItemsUntilComplete(final TableWriteItems items) { protected List<Map<String, AttributeValue>> scan(ScanRequest scanRequest, int max) {
AtomicReference<BatchWriteItemOutcome> outcome = new AtomicReference<>();
batchWriteItemsFirstPass.record(() -> outcome.set(dynamoDb.batchWriteItem(items))); return db().scanPaginator(scanRequest)
int attemptCount = 0; .items()
while (!outcome.get().getUnprocessedItems().isEmpty() && attemptCount < MAX_ATTEMPTS_TO_SAVE_BATCH_WRITE) { .stream()
batchWriteItemsRetryPass.record(() -> outcome.set(dynamoDb.batchWriteItemUnprocessed(outcome.get().getUnprocessedItems()))); .limit(max)
++attemptCount; .collect(Collectors.toList());
} }
if (!outcome.get().getUnprocessedItems().isEmpty()) {
logger.error("Attempt count ({}) reached max ({}}) before applying all batch writes to dynamo. {} unprocessed items remain.", attemptCount, MAX_ATTEMPTS_TO_SAVE_BATCH_WRITE, outcome.get().getUnprocessedItems().size()); static <T> void writeInBatches(final Iterable<T> items, final Consumer<List<T>> action) {
batchWriteItemsUnprocessed.increment(outcome.get().getUnprocessedItems().size()); final List<T> batch = new ArrayList<>(DYNAMO_DB_MAX_BATCH_SIZE);
}
for (T item : items) {
batch.add(item);
if (batch.size() == DYNAMO_DB_MAX_BATCH_SIZE) {
action.accept(batch);
batch.clear();
}
} }
if (!batch.isEmpty()) {
protected long countItemsMatchingQuery(final Table table, final QuerySpec querySpec) { action.accept(batch);
// This is very confusing, but does appear to be the intended behavior. See:
//
// - https://github.com/aws/aws-sdk-java/issues/693
// - https://github.com/aws/aws-sdk-java/issues/915
// - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.Count
long matchingItems = 0;
for (final Page<Item, QueryOutcome> page : table.query(querySpec).pages()) {
matchingItems += page.getLowLevelResult().getQueryResult().getCount();
}
return matchingItems;
}
static <T> void writeInBatches(final Iterable<T> items, final Consumer<List<T>> action) {
final List<T> batch = new ArrayList<>(DYNAMO_DB_MAX_BATCH_SIZE);
for (T item : items) {
batch.add(item);
if (batch.size() == DYNAMO_DB_MAX_BATCH_SIZE) {
action.accept(batch);
batch.clear();
}
}
if (!batch.isEmpty()) {
action.accept(batch);
}
} }
}
} }

View File

@@ -14,11 +14,19 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import javax.security.auth.Subject; import javax.security.auth.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier; import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock; import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.util.Util;
public class Account implements Principal { public class Account implements Principal {
@JsonIgnore
private static final Logger logger = LoggerFactory.getLogger(Account.class);
@JsonIgnore @JsonIgnore
private UUID uuid; private UUID uuid;
@@ -58,12 +66,15 @@ public class Account implements Principal {
@JsonProperty("inCds") @JsonProperty("inCds")
private boolean discoverableByPhoneNumber = true; private boolean discoverableByPhoneNumber = true;
@JsonProperty("_ddbV")
private int dynamoDbMigrationVersion;
@JsonIgnore @JsonIgnore
private Device authenticatedDevice; private Device authenticatedDevice;
@JsonProperty
private int version;
@JsonIgnore
private boolean stale;
public Account() {} public Account() {}
@VisibleForTesting @VisibleForTesting
@@ -75,47 +86,68 @@ public class Account implements Principal {
} }
public Optional<Device> getAuthenticatedDevice() { public Optional<Device> getAuthenticatedDevice() {
requireNotStale();
return Optional.ofNullable(authenticatedDevice); return Optional.ofNullable(authenticatedDevice);
} }
public void setAuthenticatedDevice(Device device) { public void setAuthenticatedDevice(Device device) {
requireNotStale();
this.authenticatedDevice = device; this.authenticatedDevice = device;
} }
public UUID getUuid() { public UUID getUuid() {
// this is the one method that may be called on a stale account
return uuid; return uuid;
} }
public void setUuid(UUID uuid) { public void setUuid(UUID uuid) {
requireNotStale();
this.uuid = uuid; this.uuid = uuid;
} }
public void setNumber(String number) { public void setNumber(String number) {
requireNotStale();
this.number = number; this.number = number;
} }
public String getNumber() { public String getNumber() {
requireNotStale();
return number; return number;
} }
public void addDevice(Device device) { public void addDevice(Device device) {
requireNotStale();
this.devices.remove(device); this.devices.remove(device);
this.devices.add(device); this.devices.add(device);
} }
public void removeDevice(long deviceId) { public void removeDevice(long deviceId) {
requireNotStale();
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, false, 0, null, 0, 0, "NA", 0, null)); this.devices.remove(new Device(deviceId, null, null, null, null, null, null, false, 0, null, 0, 0, "NA", 0, null));
} }
public Set<Device> getDevices() { public Set<Device> getDevices() {
requireNotStale();
return devices; return devices;
} }
public Optional<Device> getMasterDevice() { public Optional<Device> getMasterDevice() {
requireNotStale();
return getDevice(Device.MASTER_ID); return getDevice(Device.MASTER_ID);
} }
public Optional<Device> getDevice(long deviceId) { public Optional<Device> getDevice(long deviceId) {
requireNotStale();
for (Device device : devices) { for (Device device : devices) {
if (device.getId() == deviceId) { if (device.getId() == deviceId) {
return Optional.of(device); return Optional.of(device);
@@ -126,36 +158,58 @@ public class Account implements Principal {
} }
public boolean isGroupsV2Supported() { public boolean isGroupsV2Supported() {
requireNotStale();
return devices.stream() return devices.stream()
.filter(Device::isEnabled) .filter(Device::isEnabled)
.allMatch(Device::isGroupsV2Supported); .allMatch(Device::isGroupsV2Supported);
} }
public boolean isStorageSupported() { public boolean isStorageSupported() {
requireNotStale();
return devices.stream().anyMatch(device -> device.getCapabilities() != null && device.getCapabilities().isStorage()); return devices.stream().anyMatch(device -> device.getCapabilities() != null && device.getCapabilities().isStorage());
} }
public boolean isTransferSupported() { public boolean isTransferSupported() {
requireNotStale();
return getMasterDevice().map(Device::getCapabilities).map(Device.DeviceCapabilities::isTransfer).orElse(false); return getMasterDevice().map(Device::getCapabilities).map(Device.DeviceCapabilities::isTransfer).orElse(false);
} }
public boolean isGv1MigrationSupported() { public boolean isGv1MigrationSupported() {
requireNotStale();
return devices.stream() return devices.stream()
.filter(Device::isEnabled) .filter(Device::isEnabled)
.allMatch(device -> device.getCapabilities() != null && device.getCapabilities().isGv1Migration()); .allMatch(device -> device.getCapabilities() != null && device.getCapabilities().isGv1Migration());
} }
public boolean isSenderKeySupported() { public boolean isSenderKeySupported() {
requireNotStale();
return devices.stream() return devices.stream()
.filter(Device::isEnabled) .filter(Device::isEnabled)
.allMatch(device -> device.getCapabilities() != null && device.getCapabilities().isSenderKey()); .allMatch(device -> device.getCapabilities() != null && device.getCapabilities().isSenderKey());
} }
public boolean isAnnouncementGroupSupported() {
requireNotStale();
return devices.stream()
.filter(Device::isEnabled)
.allMatch(device -> device.getCapabilities() != null && device.getCapabilities().isAnnouncementGroup());
}
public boolean isEnabled() { public boolean isEnabled() {
requireNotStale();
return getMasterDevice().map(Device::isEnabled).orElse(false); return getMasterDevice().map(Device::isEnabled).orElse(false);
} }
public long getNextDeviceId() { public long getNextDeviceId() {
requireNotStale();
long highestDevice = Device.MASTER_ID; long highestDevice = Device.MASTER_ID;
for (Device device : devices) { for (Device device : devices) {
@@ -170,6 +224,8 @@ public class Account implements Principal {
} }
public int getEnabledDeviceCount() { public int getEnabledDeviceCount() {
requireNotStale();
int count = 0; int count = 0;
for (Device device : devices) { for (Device device : devices) {
@@ -180,22 +236,32 @@ public class Account implements Principal {
} }
public boolean isRateLimited() { public boolean isRateLimited() {
requireNotStale();
return true; return true;
} }
public Optional<String> getRelay() { public Optional<String> getRelay() {
requireNotStale();
return Optional.empty(); return Optional.empty();
} }
public void setIdentityKey(String identityKey) { public void setIdentityKey(String identityKey) {
requireNotStale();
this.identityKey = identityKey; this.identityKey = identityKey;
} }
public String getIdentityKey() { public String getIdentityKey() {
requireNotStale();
return identityKey; return identityKey;
} }
public long getLastSeen() { public long getLastSeen() {
requireNotStale();
long lastSeen = 0; long lastSeen = 0;
for (Device device : devices) { for (Device device : devices) {
@@ -208,78 +274,139 @@ public class Account implements Principal {
} }
public Optional<String> getCurrentProfileVersion() { public Optional<String> getCurrentProfileVersion() {
requireNotStale();
return Optional.ofNullable(currentProfileVersion); return Optional.ofNullable(currentProfileVersion);
} }
public void setCurrentProfileVersion(String currentProfileVersion) { public void setCurrentProfileVersion(String currentProfileVersion) {
requireNotStale();
this.currentProfileVersion = currentProfileVersion; this.currentProfileVersion = currentProfileVersion;
} }
public String getProfileName() { public String getProfileName() {
requireNotStale();
return name; return name;
} }
public void setProfileName(String name) { public void setProfileName(String name) {
requireNotStale();
this.name = name; this.name = name;
} }
public String getAvatar() { public String getAvatar() {
requireNotStale();
return avatar; return avatar;
} }
public void setAvatar(String avatar) { public void setAvatar(String avatar) {
requireNotStale();
this.avatar = avatar; this.avatar = avatar;
} }
public void setPin(String pin) { public void setPin(String pin) {
requireNotStale();
this.pin = pin; this.pin = pin;
} }
public void setRegistrationLockFromAttributes(final AccountAttributes attributes) {
if (!Util.isEmpty(attributes.getPin())) {
setPin(attributes.getPin());
} else if (!Util.isEmpty(attributes.getRegistrationLock())) {
AuthenticationCredentials credentials = new AuthenticationCredentials(attributes.getRegistrationLock());
setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt());
} else {
setPin(null);
setRegistrationLock(null, null);
}
}
public void setRegistrationLock(String registrationLock, String registrationLockSalt) { public void setRegistrationLock(String registrationLock, String registrationLockSalt) {
requireNotStale();
this.registrationLock = registrationLock; this.registrationLock = registrationLock;
this.registrationLockSalt = registrationLockSalt; this.registrationLockSalt = registrationLockSalt;
} }
public StoredRegistrationLock getRegistrationLock() { public StoredRegistrationLock getRegistrationLock() {
requireNotStale();
return new StoredRegistrationLock(Optional.ofNullable(registrationLock), Optional.ofNullable(registrationLockSalt), Optional.ofNullable(pin), getLastSeen()); return new StoredRegistrationLock(Optional.ofNullable(registrationLock), Optional.ofNullable(registrationLockSalt), Optional.ofNullable(pin), getLastSeen());
} }
public Optional<byte[]> getUnidentifiedAccessKey() { public Optional<byte[]> getUnidentifiedAccessKey() {
requireNotStale();
return Optional.ofNullable(unidentifiedAccessKey); return Optional.ofNullable(unidentifiedAccessKey);
} }
public void setUnidentifiedAccessKey(byte[] unidentifiedAccessKey) { public void setUnidentifiedAccessKey(byte[] unidentifiedAccessKey) {
requireNotStale();
this.unidentifiedAccessKey = unidentifiedAccessKey; this.unidentifiedAccessKey = unidentifiedAccessKey;
} }
public boolean isUnrestrictedUnidentifiedAccess() { public boolean isUnrestrictedUnidentifiedAccess() {
requireNotStale();
return unrestrictedUnidentifiedAccess; return unrestrictedUnidentifiedAccess;
} }
public void setUnrestrictedUnidentifiedAccess(boolean unrestrictedUnidentifiedAccess) { public void setUnrestrictedUnidentifiedAccess(boolean unrestrictedUnidentifiedAccess) {
requireNotStale();
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
} }
public boolean isFor(AmbiguousIdentifier identifier) { public boolean isFor(AmbiguousIdentifier identifier) {
requireNotStale();
if (identifier.hasUuid()) return identifier.getUuid().equals(uuid); if (identifier.hasUuid()) return identifier.getUuid().equals(uuid);
else if (identifier.hasNumber()) return identifier.getNumber().equals(number); else if (identifier.hasNumber()) return identifier.getNumber().equals(number);
else throw new AssertionError(); else throw new AssertionError();
} }
public boolean isDiscoverableByPhoneNumber() { public boolean isDiscoverableByPhoneNumber() {
requireNotStale();
return this.discoverableByPhoneNumber; return this.discoverableByPhoneNumber;
} }
public void setDiscoverableByPhoneNumber(final boolean discoverableByPhoneNumber) { public void setDiscoverableByPhoneNumber(final boolean discoverableByPhoneNumber) {
requireNotStale();
this.discoverableByPhoneNumber = discoverableByPhoneNumber; this.discoverableByPhoneNumber = discoverableByPhoneNumber;
} }
public int getDynamoDbMigrationVersion() { public int getVersion() {
return dynamoDbMigrationVersion; requireNotStale();
return version;
} }
public void setDynamoDbMigrationVersion(int dynamoDbMigrationVersion) { public void setVersion(int version) {
this.dynamoDbMigrationVersion = dynamoDbMigrationVersion; requireNotStale();
this.version = version;
}
public void markStale() {
stale = true;
}
private void requireNotStale() {
assert !stale;
//noinspection ConstantConditions
if (stale) {
logger.error("Accessor called on stale account", new RuntimeException());
}
} }
// Principal implementation // Principal implementation

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public class AccountCrawlChunk {
private final List<Account> accounts;
@Nullable
private final UUID lastUuid;
public AccountCrawlChunk(final List<Account> accounts, @Nullable final UUID lastUuid) {
this.accounts = accounts;
this.lastUuid = lastUuid;
}
public List<Account> getAccounts() {
return accounts;
}
public Optional<UUID> getLastUuid() {
return Optional.ofNullable(lastUuid);
}
}

View File

@@ -4,22 +4,21 @@
*/ */
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer; import com.codahale.metrics.Timer;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger; import io.dropwizard.lifecycle.Managed;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import static com.codahale.metrics.MetricRegistry.name; import org.slf4j.LoggerFactory;
import io.dropwizard.lifecycle.Managed; import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class AccountDatabaseCrawler implements Managed, Runnable { public class AccountDatabaseCrawler implements Managed, Runnable {
@@ -38,6 +37,8 @@ public class AccountDatabaseCrawler implements Managed, Runnable {
private final AccountDatabaseCrawlerCache cache; private final AccountDatabaseCrawlerCache cache;
private final List<AccountDatabaseCrawlerListener> listeners; private final List<AccountDatabaseCrawlerListener> listeners;
private final DynamicConfigurationManager dynamicConfigurationManager;
private AtomicBoolean running = new AtomicBoolean(false); private AtomicBoolean running = new AtomicBoolean(false);
private boolean finished; private boolean finished;
@@ -45,7 +46,8 @@ public class AccountDatabaseCrawler implements Managed, Runnable {
AccountDatabaseCrawlerCache cache, AccountDatabaseCrawlerCache cache,
List<AccountDatabaseCrawlerListener> listeners, List<AccountDatabaseCrawlerListener> listeners,
int chunkSize, int chunkSize,
long chunkIntervalMs) long chunkIntervalMs,
DynamicConfigurationManager dynamicConfigurationManager)
{ {
this.accounts = accounts; this.accounts = accounts;
this.chunkSize = chunkSize; this.chunkSize = chunkSize;
@@ -53,6 +55,8 @@ public class AccountDatabaseCrawler implements Managed, Runnable {
this.workerId = UUID.randomUUID().toString(); this.workerId = UUID.randomUUID().toString();
this.cache = cache; this.cache = cache;
this.listeners = listeners; this.listeners = listeners;
this.dynamicConfigurationManager = dynamicConfigurationManager;
} }
@Override @Override
@@ -93,14 +97,15 @@ public class AccountDatabaseCrawler implements Managed, Runnable {
@VisibleForTesting @VisibleForTesting
public boolean doPeriodicWork() { public boolean doPeriodicWork() {
if (cache.claimActiveWork(workerId, WORKER_TTL_MS)) { if (cache.claimActiveWork(workerId, WORKER_TTL_MS)) {
try { try {
long startTimeMs = System.currentTimeMillis(); final long startTimeMs = System.currentTimeMillis();
processChunk(); processChunk();
if (cache.isAccelerated()) { if (cache.isAccelerated()) {
return true; return true;
} }
long endTimeMs = System.currentTimeMillis(); final long endTimeMs = System.currentTimeMillis();
long sleepIntervalMs = chunkIntervalMs - (endTimeMs - startTimeMs); final long sleepIntervalMs = chunkIntervalMs - (endTimeMs - startTimeMs);
if (sleepIntervalMs > 0) sleepWhileRunning(sleepIntervalMs); if (sleepIntervalMs > 0) sleepWhileRunning(sleepIntervalMs);
} finally { } finally {
cache.releaseActiveWork(workerId); cache.releaseActiveWork(workerId);
@@ -110,42 +115,67 @@ public class AccountDatabaseCrawler implements Managed, Runnable {
} }
private void processChunk() { private void processChunk() {
Optional<UUID> fromUuid = cache.getLastUuid(); final boolean useDynamo = dynamicConfigurationManager.getConfiguration()
.getAccountsDynamoDbMigrationConfiguration()
.isDynamoCrawlerEnabled();
if (!fromUuid.isPresent()) { listeners.stream().filter(listener -> listener instanceof DirectoryReconciler)
.forEach(reconciler -> ((DirectoryReconciler) reconciler).setUseV3Endpoints(useDynamo));
final Optional<UUID> fromUuid = getLastUuid(useDynamo);
if (fromUuid.isEmpty()) {
listeners.forEach(AccountDatabaseCrawlerListener::onCrawlStart); listeners.forEach(AccountDatabaseCrawlerListener::onCrawlStart);
} }
List<Account> chunkAccounts = readChunk(fromUuid, chunkSize); final AccountCrawlChunk chunkAccounts = readChunk(fromUuid, chunkSize, useDynamo);
if (chunkAccounts.isEmpty()) { if (chunkAccounts.getAccounts().isEmpty()) {
logger.info("Finished crawl"); logger.info("Finished crawl");
listeners.forEach(listener -> listener.onCrawlEnd(fromUuid)); listeners.forEach(listener -> listener.onCrawlEnd(fromUuid));
cache.setLastUuid(Optional.empty()); cacheLastUuid(Optional.empty(), useDynamo);
cache.setAccelerated(false); cache.setAccelerated(false);
} else { } else {
try { try {
for (AccountDatabaseCrawlerListener listener : listeners) { for (AccountDatabaseCrawlerListener listener : listeners) {
listener.timeAndProcessCrawlChunk(fromUuid, chunkAccounts); listener.timeAndProcessCrawlChunk(fromUuid, chunkAccounts.getAccounts());
} }
cache.setLastUuid(Optional.of(chunkAccounts.get(chunkAccounts.size() - 1).getUuid())); cacheLastUuid(chunkAccounts.getLastUuid(), useDynamo);
} catch (AccountDatabaseCrawlerRestartException e) { } catch (AccountDatabaseCrawlerRestartException e) {
cache.setLastUuid(Optional.empty()); cacheLastUuid(Optional.empty(), useDynamo);
cache.setAccelerated(false); cache.setAccelerated(false);
} }
} }
} }
private List<Account> readChunk(Optional<UUID> fromUuid, int chunkSize) { private AccountCrawlChunk readChunk(Optional<UUID> fromUuid, int chunkSize, boolean useDynamo) {
try (Timer.Context timer = readChunkTimer.time()) { try (Timer.Context timer = readChunkTimer.time()) {
if (fromUuid.isPresent()) { if (fromUuid.isPresent()) {
return accounts.getAllFrom(fromUuid.get(), chunkSize); return useDynamo
? accounts.getAllFromDynamo(fromUuid.get(), chunkSize)
: accounts.getAllFrom(fromUuid.get(), chunkSize);
} }
return accounts.getAllFrom(chunkSize); return useDynamo
? accounts.getAllFromDynamo(chunkSize)
: accounts.getAllFrom(chunkSize);
}
}
private Optional<UUID> getLastUuid(final boolean useDynamo) {
if (useDynamo) {
return cache.getLastUuidDynamo();
} else {
return cache.getLastUuid();
}
}
private void cacheLastUuid(final Optional<UUID> lastUuid, final boolean useDynamo) {
if (useDynamo) {
cache.setLastUuidDynamo(lastUuid);
} else {
cache.setLastUuid(lastUuid);
} }
} }

View File

@@ -21,6 +21,8 @@ public class AccountDatabaseCrawlerCache {
private static final String LAST_UUID_KEY = "account_database_crawler_cache_last_uuid"; private static final String LAST_UUID_KEY = "account_database_crawler_cache_last_uuid";
private static final String ACCELERATE_KEY = "account_database_crawler_cache_accelerate"; private static final String ACCELERATE_KEY = "account_database_crawler_cache_accelerate";
private static final String LAST_UUID_DYNAMO_KEY = "account_database_crawler_cache_last_uuid_dynamo";
private static final long LAST_NUMBER_TTL_MS = 86400_000L; private static final long LAST_NUMBER_TTL_MS = 86400_000L;
private final FaultTolerantRedisCluster cacheCluster; private final FaultTolerantRedisCluster cacheCluster;
@@ -66,4 +68,19 @@ public class AccountDatabaseCrawlerCache {
} }
} }
public Optional<UUID> getLastUuidDynamo() {
final String lastUuidString = cacheCluster.withCluster(connection -> connection.sync().get(LAST_UUID_DYNAMO_KEY));
if (lastUuidString == null) return Optional.empty();
else return Optional.of(UUID.fromString(lastUuidString));
}
public void setLastUuidDynamo(Optional<UUID> lastUuid) {
if (lastUuid.isPresent()) {
cacheCluster.useCluster(connection -> connection.sync().psetex(LAST_UUID_DYNAMO_KEY, LAST_NUMBER_TTL_MS, lastUuid.get().toString()));
} else {
cacheCluster.useCluster(connection -> connection.sync().del(LAST_UUID_DYNAMO_KEY));
}
}
} }

View File

@@ -7,7 +7,7 @@ public interface AccountStore {
boolean create(Account account); boolean create(Account account);
void update(Account account); void update(Account account) throws ContestedOptimisticLockException;
Optional<Account> get(String number); Optional<Account> get(String number);

View File

@@ -9,9 +9,11 @@ import static com.codahale.metrics.MetricRegistry.name;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer; import com.codahale.metrics.Timer;
import com.codahale.metrics.Timer.Context;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import org.jdbi.v3.core.transaction.TransactionIsolationLevel; import org.jdbi.v3.core.transaction.TransactionIsolationLevel;
@@ -21,10 +23,11 @@ import org.whispersystems.textsecuregcm.util.SystemMapper;
public class Accounts implements AccountStore { public class Accounts implements AccountStore {
public static final String ID = "id"; public static final String ID = "id";
public static final String UID = "uuid"; public static final String UID = "uuid";
public static final String NUMBER = "number"; public static final String NUMBER = "number";
public static final String DATA = "data"; public static final String DATA = "data";
public static final String VERSION = "version";
private static final ObjectMapper mapper = SystemMapper.getMapper(); private static final ObjectMapper mapper = SystemMapper.getMapper();
@@ -49,15 +52,19 @@ public class Accounts implements AccountStore {
public boolean create(Account account) { public boolean create(Account account) {
return database.with(jdbi -> jdbi.inTransaction(TransactionIsolationLevel.SERIALIZABLE, handle -> { return database.with(jdbi -> jdbi.inTransaction(TransactionIsolationLevel.SERIALIZABLE, handle -> {
try (Timer.Context ignored = createTimer.time()) { try (Timer.Context ignored = createTimer.time()) {
UUID uuid = handle.createQuery("INSERT INTO accounts (" + NUMBER + ", " + UID + ", " + DATA + ") VALUES (:number, :uuid, CAST(:data AS json)) ON CONFLICT(number) DO UPDATE SET data = EXCLUDED.data RETURNING uuid") final Map<String, Object> resultMap = handle.createQuery("INSERT INTO accounts (" + NUMBER + ", " + UID + ", " + DATA + ") VALUES (:number, :uuid, CAST(:data AS json)) ON CONFLICT(number) DO UPDATE SET " + DATA + " = EXCLUDED.data, " + VERSION + " = accounts.version + 1 RETURNING uuid, version")
.bind("number", account.getNumber()) .bind("number", account.getNumber())
.bind("uuid", account.getUuid()) .bind("uuid", account.getUuid())
.bind("data", mapper.writeValueAsString(account)) .bind("data", mapper.writeValueAsString(account))
.mapTo(UUID.class) .mapToMap()
.findOnly(); .findOnly();
final UUID uuid = (UUID) resultMap.get(UID);
final int version = (int) resultMap.get(VERSION);
boolean isNew = uuid.equals(account.getUuid()); boolean isNew = uuid.equals(account.getUuid());
account.setUuid(uuid); account.setUuid(uuid);
account.setVersion(version);
return isNew; return isNew;
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
@@ -66,13 +73,23 @@ public class Accounts implements AccountStore {
} }
@Override @Override
public void update(Account account) { public void update(Account account) throws ContestedOptimisticLockException {
database.use(jdbi -> jdbi.useHandle(handle -> { database.use(jdbi -> jdbi.useHandle(handle -> {
try (Timer.Context ignored = updateTimer.time()) { try (Timer.Context ignored = updateTimer.time()) {
handle.createUpdate("UPDATE accounts SET " + DATA + " = CAST(:data AS json) WHERE " + UID + " = :uuid") final int newVersion = account.getVersion() + 1;
int rowsModified = handle.createUpdate("UPDATE accounts SET " + DATA + " = CAST(:data AS json), " + VERSION + " = :newVersion WHERE " + UID + " = :uuid AND " + VERSION + " = :version")
.bind("uuid", account.getUuid()) .bind("uuid", account.getUuid())
.bind("data", mapper.writeValueAsString(account)) .bind("data", mapper.writeValueAsString(account))
.bind("version", account.getVersion())
.bind("newVersion", newVersion)
.execute(); .execute();
if (rowsModified == 0) {
throw new ContestedOptimisticLockException();
}
account.setVersion(newVersion);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }
@@ -103,20 +120,21 @@ public class Accounts implements AccountStore {
})); }));
} }
public List<Account> getAllFrom(UUID from, int length) { public AccountCrawlChunk getAllFrom(UUID from, int length) {
return database.with(jdbi -> jdbi.withHandle(handle -> { final List<Account> accounts = database.with(jdbi -> jdbi.withHandle(handle -> {
try (Timer.Context ignored = getAllFromOffsetTimer.time()) { try (Context ignored = getAllFromOffsetTimer.time()) {
return handle.createQuery("SELECT * FROM accounts WHERE " + UID + " > :from ORDER BY " + UID + " LIMIT :limit") return handle.createQuery("SELECT * FROM accounts WHERE " + UID + " > :from ORDER BY " + UID + " LIMIT :limit")
.bind("from", from) .bind("from", from)
.bind("limit", length) .bind("limit", length)
.mapTo(Account.class) .mapTo(Account.class)
.list(); .list();
} }
})); }));
return buildChunkForAccounts(accounts);
} }
public List<Account> getAllFrom(int length) { public AccountCrawlChunk getAllFrom(int length) {
return database.with(jdbi -> jdbi.withHandle(handle -> { final List<Account> accounts = database.with(jdbi -> jdbi.withHandle(handle -> {
try (Timer.Context ignored = getAllFromTimer.time()) { try (Timer.Context ignored = getAllFromTimer.time()) {
return handle.createQuery("SELECT * FROM accounts ORDER BY " + UID + " LIMIT :limit") return handle.createQuery("SELECT * FROM accounts ORDER BY " + UID + " LIMIT :limit")
.bind("limit", length) .bind("limit", length)
@@ -124,6 +142,12 @@ public class Accounts implements AccountStore {
.list(); .list();
} }
})); }));
return buildChunkForAccounts(accounts);
}
private AccountCrawlChunk buildChunkForAccounts(final List<Account> accounts) {
return new AccountCrawlChunk(accounts, accounts.isEmpty() ? null : accounts.get(accounts.size() - 1).getUuid());
} }
@Override @Override

View File

@@ -1,26 +1,11 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import com.amazonaws.handlers.AsyncHandler;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsync;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.PrimaryKey;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.dynamodbv2.document.spec.GetItemSpec;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.CancellationReason;
import com.amazonaws.services.dynamodbv2.model.Delete;
import com.amazonaws.services.dynamodbv2.model.GetItemResult;
import com.amazonaws.services.dynamodbv2.model.Put;
import com.amazonaws.services.dynamodbv2.model.ReturnValuesOnConditionCheckFailure;
import com.amazonaws.services.dynamodbv2.model.TransactWriteItem;
import com.amazonaws.services.dynamodbv2.model.TransactWriteItemsRequest;
import com.amazonaws.services.dynamodbv2.model.TransactWriteItemsResult;
import com.amazonaws.services.dynamodbv2.model.TransactionCanceledException;
import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Counter;
@@ -33,12 +18,32 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.SystemMapper; import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.UUIDUtil; import org.whispersystems.textsecuregcm.util.UUIDUtil;
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.CancellationReason;
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
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;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest;
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
import software.amazon.awssdk.services.dynamodb.model.TransactionConflictException;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse;
public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountStore { public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountStore {
@@ -48,12 +53,11 @@ public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountSt
static final String ATTR_ACCOUNT_E164 = "P"; static final String ATTR_ACCOUNT_E164 = "P";
// account, serialized to JSON // account, serialized to JSON
static final String ATTR_ACCOUNT_DATA = "D"; static final String ATTR_ACCOUNT_DATA = "D";
// internal version for optimistic locking
static final String ATTR_VERSION = "V";
static final String ATTR_MIGRATION_VERSION = "V"; private final DynamoDbClient client;
private final DynamoDbAsyncClient asyncClient;
private final AmazonDynamoDB client;
private final Table accountsTable;
private final AmazonDynamoDBAsync asyncClient;
private final ThreadPoolExecutor migrationThreadPool; private final ThreadPoolExecutor migrationThreadPool;
@@ -61,27 +65,29 @@ public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountSt
private final MigrationRetryAccounts migrationRetryAccounts; private final MigrationRetryAccounts migrationRetryAccounts;
private final String phoneNumbersTableName; private final String phoneNumbersTableName;
private final String accountsTableName;
private static final Timer CREATE_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "create")); private static final Timer CREATE_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "create"));
private static final Timer UPDATE_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "update")); private static final Timer UPDATE_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "update"));
private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "getByNumber")); private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "getByNumber"));
private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "getByUuid")); private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "getByUuid"));
private static final Timer GET_ALL_FROM_START_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "getAllFrom"));
private static final Timer GET_ALL_FROM_OFFSET_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "getAllFromOffset"));
private static final Timer DELETE_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "delete")); private static final Timer DELETE_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "delete"));
private final Logger logger = LoggerFactory.getLogger(AccountsDynamoDb.class); private final Logger logger = LoggerFactory.getLogger(AccountsDynamoDb.class);
public AccountsDynamoDb(AmazonDynamoDB client, AmazonDynamoDBAsync asyncClient, public AccountsDynamoDb(DynamoDbClient client, DynamoDbAsyncClient asyncClient,
ThreadPoolExecutor migrationThreadPool, DynamoDB dynamoDb, String accountsTableName, String phoneNumbersTableName, ThreadPoolExecutor migrationThreadPool, String accountsTableName, String phoneNumbersTableName,
MigrationDeletedAccounts migrationDeletedAccounts, MigrationDeletedAccounts migrationDeletedAccounts,
MigrationRetryAccounts accountsMigrationErrors) { MigrationRetryAccounts accountsMigrationErrors) {
super(dynamoDb); super(client);
this.client = client; this.client = client;
this.accountsTable = dynamoDb.getTable(accountsTableName);
this.phoneNumbersTableName = phoneNumbersTableName;
this.asyncClient = asyncClient; this.asyncClient = asyncClient;
this.phoneNumbersTableName = phoneNumbersTableName;
this.accountsTableName = accountsTableName;
this.migrationThreadPool = migrationThreadPool; this.migrationThreadPool = migrationThreadPool;
this.migrationDeletedAccounts = migrationDeletedAccounts; this.migrationDeletedAccounts = migrationDeletedAccounts;
@@ -90,39 +96,49 @@ public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountSt
@Override @Override
public boolean create(Account account) { public boolean create(Account account) {
return CREATE_TIMER.record(() -> { return CREATE_TIMER.record(() -> {
try { try {
TransactWriteItem phoneNumberConstraintPut = buildPutWriteItemForPhoneNumberConstraint(account, account.getUuid()); 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 accountPut = buildPutWriteItemForAccount(account, account.getUuid()); final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.transactItems(phoneNumberConstraintPut, accountPut)
final TransactWriteItemsRequest request = new TransactWriteItemsRequest() .build();
.withTransactItems(phoneNumberConstraintPut, accountPut);
try { try {
client.transactWriteItems(request); client.transactWriteItems(request);
} catch (TransactionCanceledException e) { } catch (TransactionCanceledException e) {
final CancellationReason accountCancellationReason = e.getCancellationReasons().get(1); final CancellationReason accountCancellationReason = e.cancellationReasons().get(1);
if ("ConditionalCheckFailed".equals(accountCancellationReason.getCode())) { if ("ConditionalCheckFailed".equals(accountCancellationReason.code())) {
throw new IllegalArgumentException("uuid present with different phone number"); throw new IllegalArgumentException("uuid present with different phone number");
} }
final CancellationReason phoneNumberConstraintCancellationReason = e.getCancellationReasons().get(0); final CancellationReason phoneNumberConstraintCancellationReason = e.cancellationReasons().get(0);
if ("ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.getCode())) { if ("ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code())) {
ByteBuffer actualAccountUuid = phoneNumberConstraintCancellationReason.getItem().get(KEY_ACCOUNT_UUID).getB(); ByteBuffer actualAccountUuid = phoneNumberConstraintCancellationReason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer();
account.setUuid(UUIDUtil.fromByteBuffer(actualAccountUuid)); account.setUuid(UUIDUtil.fromByteBuffer(actualAccountUuid));
final int version = get(account.getUuid()).get().getVersion();
account.setVersion(version);
update(account); update(account);
return false; return false;
} }
if ("TransactionConflict".equals(accountCancellationReason.code())) {
// this should only happen during concurrent update()s for an account migration
throw new ContestedOptimisticLockException();
}
// this shouldnt happen // this shouldnt happen
throw new RuntimeException("could not create account: " + extractCancellationReasonCodes(e)); throw new RuntimeException("could not create account: " + extractCancellationReasonCodes(e));
} }
@@ -134,100 +150,148 @@ public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountSt
}); });
} }
private TransactWriteItem buildPutWriteItemForAccount(Account account, UUID uuid) throws JsonProcessingException { private TransactWriteItem buildPutWriteItemForAccount(Account account, UUID uuid, Put.Builder putBuilder) throws JsonProcessingException {
return new TransactWriteItem() return TransactWriteItem.builder()
.withPut( .put(putBuilder
new Put() .tableName(accountsTableName)
.withTableName(accountsTable.getTableName()) .item(Map.of(
.withItem(Map.of( KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid),
KEY_ACCOUNT_UUID, new AttributeValue().withB(UUIDUtil.toByteBuffer(uuid)), ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
ATTR_ACCOUNT_E164, new AttributeValue(account.getNumber()), ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
ATTR_ACCOUNT_DATA, new AttributeValue() ATTR_VERSION, AttributeValues.fromInt(account.getVersion())))
.withB(ByteBuffer.wrap(SystemMapper.getMapper().writeValueAsBytes(account))), .build())
ATTR_MIGRATION_VERSION, new AttributeValue().withN( .build();
String.valueOf(account.getDynamoDbMigrationVersion()))))
.withConditionExpression("attribute_not_exists(#number) OR #number = :number")
.withExpressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
.withExpressionAttributeValues(Map.of(":number", new AttributeValue(account.getNumber()))));
} }
private TransactWriteItem buildPutWriteItemForPhoneNumberConstraint(Account account, UUID uuid) { private TransactWriteItem buildPutWriteItemForPhoneNumberConstraint(Account account, UUID uuid) {
return new TransactWriteItem() return TransactWriteItem.builder()
.withPut( .put(
new Put() Put.builder()
.withTableName(phoneNumbersTableName) .tableName(phoneNumbersTableName)
.withItem(Map.of( .item(Map.of(
ATTR_ACCOUNT_E164, new AttributeValue(account.getNumber()), ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
KEY_ACCOUNT_UUID, new AttributeValue().withB(UUIDUtil.toByteBuffer(uuid)))) KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid)))
.withConditionExpression( .conditionExpression(
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)") "attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
.withExpressionAttributeNames( .expressionAttributeNames(
Map.of("#uuid", KEY_ACCOUNT_UUID, Map.of("#uuid", KEY_ACCOUNT_UUID,
"#number", ATTR_ACCOUNT_E164)) "#number", ATTR_ACCOUNT_E164))
.withExpressionAttributeValues( .expressionAttributeValues(
Map.of(":uuid", new AttributeValue().withB(UUIDUtil.toByteBuffer(uuid)))) Map.of(":uuid", AttributeValues.fromUUID(uuid)))
.withReturnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)); .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
} }
@Override @Override
public void update(Account account) { public void update(Account account) throws ContestedOptimisticLockException {
UPDATE_TIMER.record(() -> { UPDATE_TIMER.record(() -> {
UpdateItemRequest updateItemRequest; UpdateItemRequest updateItemRequest;
try { try {
updateItemRequest = new UpdateItemRequest() updateItemRequest = UpdateItemRequest.builder()
.withTableName(accountsTable.getTableName()) .tableName(accountsTableName)
.withKey(Map.of(KEY_ACCOUNT_UUID, new AttributeValue().withB(UUIDUtil.toByteBuffer(account.getUuid())))) .key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.withUpdateExpression("SET #data = :data, #version = :version") .updateExpression("SET #data = :data ADD #version :version_increment")
.withConditionExpression("attribute_exists(#number)") .conditionExpression("attribute_exists(#number) AND #version = :version")
.withExpressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164, .expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
"#data", ATTR_ACCOUNT_DATA, "#data", ATTR_ACCOUNT_DATA,
"#version", ATTR_MIGRATION_VERSION)) "#version", ATTR_VERSION))
.withExpressionAttributeValues(Map.of(":data", new AttributeValue().withB(ByteBuffer.wrap(SystemMapper.getMapper().writeValueAsBytes(account))), .expressionAttributeValues(Map.of(
":version", new AttributeValue().withN(String.valueOf(account.getDynamoDbMigrationVersion())))); ":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
":version", AttributeValues.fromInt(account.getVersion()),
":version_increment", AttributeValues.fromInt(1)))
.returnValues(ReturnValue.UPDATED_NEW)
.build();
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }
client.updateItem(updateItemRequest); try {
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 doesnt 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;
}
}); });
} }
@Override @Override
public Optional<Account> get(String number) { public Optional<Account> get(String number) {
return GET_BY_NUMBER_TIMER.record(() -> { return GET_BY_NUMBER_TIMER.record(() -> {
final GetItemResult phoneNumberAndUuid = client.getItem(phoneNumbersTableName, final GetItemResponse response = client.getItem(GetItemRequest.builder()
Map.of(ATTR_ACCOUNT_E164, new AttributeValue(number)), true); .tableName(phoneNumbersTableName)
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
.build());
return Optional.ofNullable(phoneNumberAndUuid.getItem()) return Optional.ofNullable(response.item())
.map(item -> item.get(KEY_ACCOUNT_UUID).getB()) .map(item -> item.get(KEY_ACCOUNT_UUID))
.map(uuid -> accountsTable.getItem(new GetItemSpec() .map(uuid -> accountByUuid(uuid))
.withPrimaryKey(KEY_ACCOUNT_UUID, uuid.array())
.withConsistentRead(true)))
.map(AccountsDynamoDb::fromItem); .map(AccountsDynamoDb::fromItem);
}); });
} }
private Map<String, AttributeValue> accountByUuid(AttributeValue uuid) {
GetItemResponse r = client.getItem(GetItemRequest.builder()
.tableName(accountsTableName)
.key(Map.of(KEY_ACCOUNT_UUID, uuid))
.consistentRead(true)
.build());
return r.item().isEmpty() ? null : r.item();
}
@Override @Override
public Optional<Account> get(UUID uuid) { public Optional<Account> get(UUID uuid) {
Optional<Item> maybeItem = GET_BY_UUID_TIMER.record(() -> return GET_BY_UUID_TIMER.record(() ->
Optional.ofNullable(accountsTable.getItem(new GetItemSpec(). Optional.ofNullable(accountByUuid(AttributeValues.fromUUID(uuid)))
withPrimaryKey(new PrimaryKey(KEY_ACCOUNT_UUID, UUIDUtil.toByteBuffer(uuid))) .map(AccountsDynamoDb::fromItem));
.withConsistentRead(true))));
return maybeItem.map(AccountsDynamoDb::fromItem);
} }
@Override @Override
public void delete(UUID uuid) { public void delete(UUID uuid) {
DELETE_TIMER.record(() -> { DELETE_TIMER.record(() -> {
delete(uuid, true); delete(uuid, true);
}); });
} }
public AccountCrawlChunk getAllFrom(final UUID from, final int maxCount, final int pageSize) {
final ScanRequest.Builder scanRequestBuilder = ScanRequest.builder()
.limit(pageSize)
.exclusiveStartKey(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(from)));
return scanForChunk(scanRequestBuilder, maxCount, GET_ALL_FROM_OFFSET_TIMER);
}
public AccountCrawlChunk getAllFromStart(final int maxCount, final int pageSize) {
final ScanRequest.Builder scanRequestBuilder = ScanRequest.builder()
.limit(pageSize);
return scanForChunk(scanRequestBuilder, maxCount, GET_ALL_FROM_START_TIMER);
}
private AccountCrawlChunk scanForChunk(final ScanRequest.Builder scanRequestBuilder, final int maxCount, final Timer timer) {
scanRequestBuilder.tableName(accountsTableName);
final List<Account> accounts = timer.record(() -> scan(scanRequestBuilder.build(), maxCount)
.stream()
.map(AccountsDynamoDb::fromItem)
.collect(Collectors.toList()));
return new AccountCrawlChunk(accounts, accounts.size() > 0 ? accounts.get(accounts.size() - 1).getUuid() : null);
}
private void delete(UUID uuid, boolean saveInDeletedAccountsTable) { private void delete(UUID uuid, boolean saveInDeletedAccountsTable) {
if (saveInDeletedAccountsTable) { if (saveInDeletedAccountsTable) {
@@ -238,18 +302,22 @@ public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountSt
maybeAccount.ifPresent(account -> { maybeAccount.ifPresent(account -> {
TransactWriteItem phoneNumberDelete = new TransactWriteItem() TransactWriteItem phoneNumberDelete = TransactWriteItem.builder()
.withDelete(new Delete() .delete(Delete.builder()
.withTableName(phoneNumbersTableName) .tableName(phoneNumbersTableName)
.withKey(Map.of(ATTR_ACCOUNT_E164, new AttributeValue(account.getNumber())))); .key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber())))
.build())
.build();
TransactWriteItem accountDelete = new TransactWriteItem().withDelete( TransactWriteItem accountDelete = TransactWriteItem.builder()
new Delete() .delete(Delete.builder()
.withTableName(accountsTable.getTableName()) .tableName(accountsTableName)
.withKey(Map.of(KEY_ACCOUNT_UUID, new AttributeValue().withB(UUIDUtil.toByteBuffer(uuid))))); .key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid)))
.build())
.build();
TransactWriteItemsRequest request = new TransactWriteItemsRequest() TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.withTransactItems(phoneNumberDelete, accountDelete); .transactItems(phoneNumberDelete, accountDelete).build();
client.transactWriteItems(request); client.transactWriteItems(request);
}); });
@@ -299,64 +367,63 @@ public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountSt
try { try {
TransactWriteItem phoneNumberConstraintPut = buildPutWriteItemForPhoneNumberConstraint(account, account.getUuid()); TransactWriteItem phoneNumberConstraintPut = buildPutWriteItemForPhoneNumberConstraint(account, account.getUuid());
TransactWriteItem accountPut = buildPutWriteItemForAccount(account, account.getUuid()); TransactWriteItem accountPut = buildPutWriteItemForAccount(account, account.getUuid(), Put.builder()
accountPut.getPut() .conditionExpression("attribute_not_exists(#uuid) OR (attribute_exists(#uuid) AND #version < :version)")
.setConditionExpression("attribute_not_exists(#uuid) OR (attribute_exists(#uuid) AND #version < :version)"); .expressionAttributeNames(Map.of(
accountPut.getPut() "#uuid", KEY_ACCOUNT_UUID,
.setExpressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID, "#version", ATTR_VERSION))
"#version", ATTR_MIGRATION_VERSION)); .expressionAttributeValues(Map.of(
accountPut.getPut() ":version", AttributeValues.fromInt(account.getVersion()))));
.setExpressionAttributeValues(
Map.of(":version", new AttributeValue().withN(String.valueOf(account.getDynamoDbMigrationVersion()))));
final TransactWriteItemsRequest request = new TransactWriteItemsRequest() final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.withTransactItems(phoneNumberConstraintPut, accountPut); .transactItems(phoneNumberConstraintPut, accountPut).build();
final CompletableFuture<Boolean> resultFuture = new CompletableFuture<>(); final CompletableFuture<Boolean> resultFuture = new CompletableFuture<>();
asyncClient.transactWriteItems(request).whenCompleteAsync((result, exception) -> {
asyncClient.transactWriteItemsAsync(request, if (result != null) {
new AsyncHandler<>() { resultFuture.complete(true);
@Override return;
public void onError(Exception exception) { }
if (exception instanceof TransactionCanceledException) { if (exception instanceof CompletionException) {
// account is already migrated // whenCompleteAsync can wrap exceptions in a CompletionException; unwrap it to get to the root cause.
resultFuture.complete(false); exception = exception.getCause();
} else { }
try { if (exception instanceof TransactionCanceledException) {
migrationRetryAccounts.put(account.getUuid()); // account is already migrated
} catch (final Exception e) { resultFuture.complete(false);
logger.error("Could not store account {}", account.getUuid()); return;
} }
resultFuture.completeExceptionally(exception); try {
} migrationRetryAccounts.put(account.getUuid());
} } catch (final Exception e) {
logger.error("Could not store account {}", account.getUuid());
@Override }
public void onSuccess(TransactWriteItemsRequest request, TransactWriteItemsResult transactWriteItemsResult) { resultFuture.completeExceptionally(exception);
resultFuture.complete(true); });
}
});
return resultFuture; return resultFuture;
} catch (Exception e) { } catch (Exception e) {
return CompletableFuture.failedFuture(e); return CompletableFuture.failedFuture(e);
} }
} }
private static String extractCancellationReasonCodes(final TransactionCanceledException exception) { private static String extractCancellationReasonCodes(final TransactionCanceledException exception) {
return exception.getCancellationReasons().stream() return exception.cancellationReasons().stream()
.map(CancellationReason::getCode) .map(CancellationReason::code)
.collect(Collectors.joining(", ")); .collect(Collectors.joining(", "));
} }
@VisibleForTesting @VisibleForTesting
static Account fromItem(Item item) { static Account fromItem(Map<String, AttributeValue> item) {
if (!item.containsKey(ATTR_ACCOUNT_DATA) ||
!item.containsKey(ATTR_ACCOUNT_E164) ||
!item.containsKey(KEY_ACCOUNT_UUID)) {
throw new RuntimeException("item missing values");
}
try { try {
Account account = SystemMapper.getMapper().readValue(item.getBinary(ATTR_ACCOUNT_DATA), Account.class); Account account = SystemMapper.getMapper().readValue(item.get(ATTR_ACCOUNT_DATA).b().asByteArray(), Account.class);
account.setNumber(item.get(ATTR_ACCOUNT_E164).s());
account.setNumber(item.getString(ATTR_ACCOUNT_E164)); account.setUuid(UUIDUtil.fromByteBuffer(item.get(KEY_ACCOUNT_UUID).b().asByteBuffer()));
account.setUuid(UUIDUtil.fromByteBuffer(item.getByteBuffer(KEY_ACCOUNT_UUID))); account.setVersion(Integer.parseInt(item.get(ATTR_VERSION).n()));
return account; return account;

View File

@@ -7,7 +7,7 @@ package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer; import com.codahale.metrics.Timer;
@@ -18,18 +18,27 @@ import io.lettuce.core.RedisException;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import net.logstash.logback.argument.StructuredArguments;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier; import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient; import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
@@ -48,11 +57,16 @@ public class AccountsManager {
private static final Timer getByUuidTimer = metricRegistry.timer(name(AccountsManager.class, "getByUuid" )); 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 deleteTimer = metricRegistry.timer(name(AccountsManager.class, "delete"));
// TODO Remove this meter when external dependencies have been resolved
// Note that this is deliberately namespaced to `AccountController` for metric continuity.
private static final Meter newUserMeter = metricRegistry.meter(name(AccountController.class, "brand_new_user"));
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 redisNumberGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisNumberGet"));
private static final Timer redisUuidGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUuidGet" )); 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 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"); private static final String DELETE_COUNTER_NAME = name(AccountsManager.class, "deleteCounter");
private static final String DELETE_ERROR_COUNTER_NAME = name(AccountsManager.class, "deleteError"); private static final String DELETE_ERROR_COUNTER_NAME = name(AccountsManager.class, "deleteError");
private static final String COUNTRY_CODE_TAG_NAME = "country"; private static final String COUNTRY_CODE_TAG_NAME = "country";
@@ -67,11 +81,13 @@ public class AccountsManager {
private final Accounts accounts; private final Accounts accounts;
private final AccountsDynamoDb accountsDynamoDb; private final AccountsDynamoDb accountsDynamoDb;
private final FaultTolerantRedisCluster cacheCluster; private final FaultTolerantRedisCluster cacheCluster;
private final DeletedAccountsManager deletedAccountsManager;
private final DirectoryQueue directoryQueue; private final DirectoryQueue directoryQueue;
private final KeysDynamoDb keysDynamoDb; private final KeysDynamoDb keysDynamoDb;
private final MessagesManager messagesManager; private final MessagesManager messagesManager;
private final UsernamesManager usernamesManager; private final UsernamesManager usernamesManager;
private final ProfilesManager profilesManager; private final ProfilesManager profilesManager;
private final StoredVerificationCodeManager pendingAccounts;
private final SecureStorageClient secureStorageClient; private final SecureStorageClient secureStorageClient;
private final SecureBackupClient secureBackupClient; private final SecureBackupClient secureBackupClient;
private final ObjectMapper mapper; private final ObjectMapper mapper;
@@ -93,32 +109,65 @@ public class AccountsManager {
} }
} }
public AccountsManager(Accounts accounts, AccountsDynamoDb accountsDynamoDb, FaultTolerantRedisCluster cacheCluster, final DirectoryQueue directoryQueue, public AccountsManager(Accounts accounts, AccountsDynamoDb accountsDynamoDb, FaultTolerantRedisCluster cacheCluster,
final DeletedAccountsManager deletedAccountsManager,
final DirectoryQueue directoryQueue,
final KeysDynamoDb keysDynamoDb, final MessagesManager messagesManager, final UsernamesManager usernamesManager, final KeysDynamoDb keysDynamoDb, final MessagesManager messagesManager, final UsernamesManager usernamesManager,
final ProfilesManager profilesManager, final SecureStorageClient secureStorageClient, final ProfilesManager profilesManager,
final StoredVerificationCodeManager pendingAccounts,
final SecureStorageClient secureStorageClient,
final SecureBackupClient secureBackupClient, final SecureBackupClient secureBackupClient,
final ExperimentEnrollmentManager experimentEnrollmentManager, final DynamicConfigurationManager dynamicConfigurationManager) { final ExperimentEnrollmentManager experimentEnrollmentManager,
final DynamicConfigurationManager dynamicConfigurationManager) {
this.accounts = accounts; this.accounts = accounts;
this.accountsDynamoDb = accountsDynamoDb; this.accountsDynamoDb = accountsDynamoDb;
this.cacheCluster = cacheCluster; this.cacheCluster = cacheCluster;
this.deletedAccountsManager = deletedAccountsManager;
this.directoryQueue = directoryQueue; this.directoryQueue = directoryQueue;
this.keysDynamoDb = keysDynamoDb; this.keysDynamoDb = keysDynamoDb;
this.messagesManager = messagesManager; this.messagesManager = messagesManager;
this.usernamesManager = usernamesManager; this.usernamesManager = usernamesManager;
this.profilesManager = profilesManager; this.profilesManager = profilesManager;
this.pendingAccounts = pendingAccounts;
this.secureStorageClient = secureStorageClient; this.secureStorageClient = secureStorageClient;
this.secureBackupClient = secureBackupClient; this.secureBackupClient = secureBackupClient;
this.mapper = SystemMapper.getMapper(); this.mapper = SystemMapper.getMapper();
this.migrationComparisonMapper = mapper.copy(); this.migrationComparisonMapper = mapper.copy();
migrationComparisonMapper.addMixIn(Account.class, AccountComparisonMixin.class); migrationComparisonMapper.addMixIn(Device.class, DeviceComparisonMixin.class);
this.dynamicConfigurationManager = dynamicConfigurationManager; this.dynamicConfigurationManager = dynamicConfigurationManager;
this.experimentEnrollmentManager = experimentEnrollmentManager; this.experimentEnrollmentManager = experimentEnrollmentManager;
} }
public boolean create(Account account) { public Account create(final String number,
final String password,
final String signalAgent,
final AccountAttributes accountAttributes) {
try (Timer.Context ignored = createTimer.time()) { try (Timer.Context ignored = createTimer.time()) {
Optional<Account> maybeExistingAccount = get(number);
Device device = new Device();
device.setId(Device.MASTER_ID);
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
device.setName(accountAttributes.getName());
device.setCapabilities(accountAttributes.getCapabilities());
device.setCreated(System.currentTimeMillis());
device.setLastSeen(Util.todayInMillis());
device.setUserAgent(signalAgent);
Account account = new Account();
account.setNumber(number);
account.setUuid(UUID.randomUUID());
account.addDevice(device);
account.setRegistrationLockFromAttributes(accountAttributes);
account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
account.setUnrestrictedUnidentifiedAccess(accountAttributes.isUnrestrictedUnidentifiedAccess());
account.setDiscoverableByPhoneNumber(accountAttributes.isDiscoverableByPhoneNumber());
final UUID originalUuid = account.getUuid(); final UUID originalUuid = account.getUuid();
boolean freshUser = databaseCreate(account); boolean freshUser = databaseCreate(account);
@@ -156,29 +205,120 @@ public class AccountsManager {
redisSet(account); redisSet(account);
return freshUser; final Tags tags;
if (freshUser) {
tags = Tags.of("type", "new");
} else {
tags = Tags.of("type", "reregister");
}
Metrics.counter(CREATE_COUNTER_NAME, tags).increment();
if (!account.isDiscoverableByPhoneNumber()) {
// The newly-created account has explicitly opted out of discoverability
directoryQueue.deleteAccount(account);
}
maybeExistingAccount.ifPresent(definitelyExistingAccount -> {
messagesManager.clear(definitelyExistingAccount.getUuid());
keysDynamoDb.delete(definitelyExistingAccount);
});
pendingAccounts.remove(number);
return account;
} }
} }
public void update(Account account) { public Account update(Account account, Consumer<Account> updater) {
final boolean wasDiscoverableBeforeUpdate = directoryQueue.isDiscoverable(account);
final Account updatedAccount;
try (Timer.Context ignored = updateTimer.time()) { try (Timer.Context ignored = updateTimer.time()) {
account.setDynamoDbMigrationVersion(account.getDynamoDbMigrationVersion() + 1); updater.accept(account);
redisSet(account);
databaseUpdate(account); {
// optimistically increment version
final int originalVersion = account.getVersion();
account.setVersion(originalVersion + 1);
redisSet(account);
account.setVersion(originalVersion);
}
final UUID uuid = account.getUuid();
updatedAccount = updateWithRetries(account, updater, this::databaseUpdate, () -> databaseGet(uuid).get());
if (dynamoWriteEnabled()) { if (dynamoWriteEnabled()) {
runSafelyAndRecordMetrics(() -> { runSafelyAndRecordMetrics(() -> {
try {
dynamoUpdate(account); final Optional<Account> dynamoAccount = dynamoGet(uuid);
} catch (final ConditionalCheckFailedException e) { if (dynamoAccount.isPresent()) {
dynamoCreate(account); updater.accept(dynamoAccount.get());
Account dynamoUpdatedAccount = updateWithRetries(dynamoAccount.get(),
updater,
this::dynamoUpdate,
() -> dynamoGet(uuid).get());
return Optional.of(dynamoUpdatedAccount);
} }
return true;
}, Optional.of(account.getUuid()), true, return Optional.empty();
(databaseSuccess, dynamoSuccess) -> Optional.empty(), // both values are always true }, Optional.of(uuid), Optional.of(updatedAccount),
this::compareAccounts,
"update"); "update");
} }
// set the cache again, so that all updates are coalesced
redisSet(updatedAccount);
} }
final boolean isDiscoverableAfterUpdate = directoryQueue.isDiscoverable(updatedAccount);
if (wasDiscoverableBeforeUpdate != isDiscoverableAfterUpdate) {
directoryQueue.refreshAccount(updatedAccount);
}
return updatedAccount;
}
private Account updateWithRetries(Account account, Consumer<Account> updater, Consumer<Account> persister, Supplier<Account> retriever) {
final int maxTries = 10;
int tries = 0;
while (tries < maxTries) {
try {
persister.accept(account);
final Account updatedAccount;
try {
updatedAccount = mapper.readValue(mapper.writeValueAsBytes(account), Account.class);
updatedAccount.setUuid(account.getUuid());
} catch (final IOException e) {
// this should really, truly, never happen
throw new IllegalArgumentException(e);
}
account.markStale();
return updatedAccount;
} catch (final ContestedOptimisticLockException e) {
tries++;
account = retriever.get();
updater.accept(account);
}
}
throw new OptimisticLockRetryLimitExceededException();
}
public Account updateDevice(Account account, long deviceId, Consumer<Device> deviceUpdater) {
return update(account, a -> a.getDevice(deviceId).ifPresent(deviceUpdater));
} }
public Optional<Account> get(AmbiguousIdentifier identifier) { public Optional<Account> get(AmbiguousIdentifier identifier) {
@@ -224,15 +364,27 @@ public class AccountsManager {
} }
public List<Account> getAllFrom(int length) { public AccountCrawlChunk getAllFrom(int length) {
return accounts.getAllFrom(length); return accounts.getAllFrom(length);
} }
public List<Account> getAllFrom(UUID uuid, int length) { public AccountCrawlChunk getAllFrom(UUID uuid, int length) {
return accounts.getAllFrom(uuid, length); return accounts.getAllFrom(uuid, length);
} }
public void delete(final Account account, final DeletionReason deletionReason) { public AccountCrawlChunk getAllFromDynamo(int length) {
final int maxPageSize = dynamicConfigurationManager.getConfiguration().getAccountsDynamoDbMigrationConfiguration()
.getDynamoCrawlerScanPageSize();
return accountsDynamoDb.getAllFromStart(length, maxPageSize);
}
public AccountCrawlChunk getAllFromDynamo(UUID uuid, int length) {
final int maxPageSize = dynamicConfigurationManager.getConfiguration().getAccountsDynamoDbMigrationConfiguration()
.getDynamoCrawlerScanPageSize();
return accountsDynamoDb.getAllFrom(uuid, length, maxPageSize);
}
public void delete(final Account account, final DeletionReason deletionReason) throws InterruptedException {
try (final Timer.Context ignored = deleteTimer.time()) { try (final Timer.Context ignored = deleteTimer.time()) {
final CompletableFuture<Void> deleteStorageServiceDataFuture = secureStorageClient.deleteStoredData(account.getUuid()); final CompletableFuture<Void> deleteStorageServiceDataFuture = secureStorageClient.deleteStoredData(account.getUuid());
final CompletableFuture<Void> deleteBackupServiceDataFuture = secureBackupClient.deleteBackups(account.getUuid()); final CompletableFuture<Void> deleteBackupServiceDataFuture = secureBackupClient.deleteBackups(account.getUuid());
@@ -258,7 +410,9 @@ public class AccountsManager {
} }
} }
} catch (final Exception e) { deletedAccountsManager.put(account.getUuid(), account.getNumber());
} catch (final RuntimeException | InterruptedException e) {
logger.warn("Failed to delete account", e); logger.warn("Failed to delete account", e);
Metrics.counter(DELETE_ERROR_COUNTER_NAME, Metrics.counter(DELETE_ERROR_COUNTER_NAME,
@@ -422,6 +576,10 @@ public class AccountsManager {
return Optional.of("number"); return Optional.of("number");
} }
if (databaseAccount.getVersion() != dynamoAccount.getVersion()) {
return Optional.of("version");
}
if (!Objects.equals(databaseAccount.getIdentityKey(), dynamoAccount.getIdentityKey())) { if (!Objects.equals(databaseAccount.getIdentityKey(), dynamoAccount.getIdentityKey())) {
return Optional.of("identityKey"); return Optional.of("identityKey");
} }
@@ -459,8 +617,14 @@ public class AccountsManager {
} }
try { try {
if (!serializedEquals(databaseAccount.getRegistrationLock(), dynamoAccount.getRegistrationLock())) { if (databaseAccount.getMasterDevice().isPresent() && dynamoAccount.getMasterDevice().isPresent()) {
return Optional.of("registrationLock"); if (!Objects.equals(databaseAccount.getMasterDevice().get().getSignedPreKey(), dynamoAccount.getMasterDevice().get().getSignedPreKey())) {
return Optional.of("masterDeviceSignedPreKey");
}
if (!Objects.equals(databaseAccount.getMasterDevice().get().getPushTimestamp(), dynamoAccount.getMasterDevice().get().getPushTimestamp())) {
return Optional.of("masterDevicePushTimestamp");
}
} }
if (!serializedEquals(databaseAccount.getDevices(), dynamoAccount.getDevices())) { if (!serializedEquals(databaseAccount.getDevices(), dynamoAccount.getDevices())) {
@@ -475,10 +639,6 @@ public class AccountsManager {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
if (databaseAccount.getDynamoDbMigrationVersion() != dynamoAccount.getDynamoDbMigrationVersion()) {
return Optional.of("migrationVersion");
}
return Optional.empty(); return Optional.empty();
} }
@@ -521,15 +681,30 @@ public class AccountsManager {
if (maybeUUid.isPresent() if (maybeUUid.isPresent()
&& dynamicConfigurationManager.getConfiguration().getAccountsDynamoDbMigrationConfiguration().isLogMismatches()) { && dynamicConfigurationManager.getConfiguration().getAccountsDynamoDbMigrationConfiguration().isLogMismatches()) {
logger.info("Mismatched {} for {}", mismatchDescription, maybeUUid.get());
final String abbreviatedCallChain = getAbbreviatedCallChain(new RuntimeException().getStackTrace());
logger.info("Mismatched account data: {}", StructuredArguments.entries(Map.of(
"type", mismatchDescription,
"uuid", maybeUUid.get(),
"callChain", abbreviatedCallChain
)));
} }
}); });
} }
private static abstract class AccountComparisonMixin extends Account { private String getAbbreviatedCallChain(final StackTraceElement[] stackTrace) {
return Arrays.stream(stackTrace)
.filter(stackTraceElement -> stackTraceElement.getClassName().contains("org.whispersystems"))
.filter(stackTraceElement -> !(stackTraceElement.getClassName().endsWith("AccountsManager") && stackTraceElement.getMethodName().contains("compare")))
.map(stackTraceElement -> StringUtils.substringAfterLast(stackTraceElement.getClassName(), ".") + ":" + stackTraceElement.getMethodName())
.collect(Collectors.joining(" -> "));
}
private static abstract class DeviceComparisonMixin extends Device {
@JsonIgnore @JsonIgnore
private int dynamoDbMigrationVersion; private long lastSeen;
} }

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
public class ChunkProcessingFailedException extends Exception {
public ChunkProcessingFailedException(String message) {
super(message);
}
public ChunkProcessingFailedException(Exception cause) {
super(cause);
}
}

View File

@@ -0,0 +1,13 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
public class ContestedOptimisticLockException extends RuntimeException {
public ContestedOptimisticLockException() {
super(null, null, true, false);
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.Pair;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.KeysAndAttributes;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
public class DeletedAccounts extends AbstractDynamoDbStore {
// e164, primary key
static final String KEY_ACCOUNT_E164 = "P";
static final String ATTR_ACCOUNT_UUID = "U";
static final String ATTR_EXPIRES = "E";
static final String ATTR_NEEDS_CDS_RECONCILIATION = "R";
static final Duration TIME_TO_LIVE = Duration.ofDays(30);
// Note that this limit is imposed by DynamoDB itself; going above 100 will result in errors
static final int GET_BATCH_SIZE = 100;
private final String tableName;
private final String needsReconciliationIndexName;
public DeletedAccounts(final DynamoDbClient dynamoDb, final String tableName, final String needsReconciliationIndexName) {
super(dynamoDb);
this.tableName = tableName;
this.needsReconciliationIndexName = needsReconciliationIndexName;
}
void put(UUID uuid, String e164) {
db().putItem(PutItemRequest.builder()
.tableName(tableName)
.item(Map.of(
KEY_ACCOUNT_E164, AttributeValues.fromString(e164),
ATTR_ACCOUNT_UUID, AttributeValues.fromUUID(uuid),
ATTR_EXPIRES, AttributeValues.fromLong(Instant.now().plus(TIME_TO_LIVE).getEpochSecond()),
ATTR_NEEDS_CDS_RECONCILIATION, AttributeValues.fromInt(1)))
.build());
}
List<Pair<UUID, String>> listAccountsToReconcile(final int max) {
final ScanRequest scanRequest = ScanRequest.builder()
.tableName(tableName)
.indexName(needsReconciliationIndexName)
.limit(max)
.build();
return scan(scanRequest, max)
.stream()
.map(item -> new Pair<>(
AttributeValues.getUUID(item, ATTR_ACCOUNT_UUID, null),
AttributeValues.getString(item, KEY_ACCOUNT_E164, null)))
.collect(Collectors.toList());
}
Set<String> getAccountsNeedingReconciliation(final Collection<String> e164s) {
final Queue<Map<String, AttributeValue>> pendingKeys = e164s.stream()
.map(e164 -> Map.of(KEY_ACCOUNT_E164, AttributeValues.fromString(e164)))
.collect(Collectors.toCollection(() -> new ArrayDeque<>(e164s.size())));
final Set<String> accountsNeedingReconciliation = new HashSet<>(e164s.size());
final List<Map<String, AttributeValue>> batchKeys = new ArrayList<>(GET_BATCH_SIZE);
while (!pendingKeys.isEmpty()) {
batchKeys.clear();
for (int i = 0; i < GET_BATCH_SIZE && !pendingKeys.isEmpty(); i++) {
batchKeys.add(pendingKeys.remove());
}
final BatchGetItemResponse response = db().batchGetItem(BatchGetItemRequest.builder()
.requestItems(Map.of(tableName, KeysAndAttributes.builder()
.consistentRead(true)
.keys(batchKeys)
.build()))
.build());
response.responses().getOrDefault(tableName, Collections.emptyList()).stream()
.filter(attributes -> AttributeValues.getInt(attributes, ATTR_NEEDS_CDS_RECONCILIATION, 0) == 1)
.map(attributes -> AttributeValues.getString(attributes, KEY_ACCOUNT_E164, null))
.forEach(accountsNeedingReconciliation::add);
if (response.hasUnprocessedKeys() && response.unprocessedKeys().containsKey(tableName)) {
pendingKeys.addAll(response.unprocessedKeys().get(tableName).keys());
}
}
return accountsNeedingReconciliation;
}
void markReconciled(final Collection<String> phoneNumbersReconciled) {
phoneNumbersReconciled.forEach(number -> db().updateItem(
UpdateItemRequest.builder()
.tableName(tableName)
.key(Map.of(
KEY_ACCOUNT_E164, AttributeValues.fromString(number)
))
.updateExpression("REMOVE #needs_reconciliation")
.expressionAttributeNames(Map.of(
"#needs_reconciliation", ATTR_NEEDS_CDS_RECONCILIATION
))
.build()
));
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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 io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Timer;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationRequest;
import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationRequest.User;
import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationResponse;
public class DeletedAccountsDirectoryReconciler {
private final Logger logger = LoggerFactory.getLogger(DeletedAccountsDirectoryReconciler.class);
private final DirectoryReconciliationClient directoryReconciliationClient;
private final Timer deleteTimer;
private final Counter errorCounter;
public DeletedAccountsDirectoryReconciler(
final String replicationName,
final DirectoryReconciliationClient directoryReconciliationClient) {
this.directoryReconciliationClient = directoryReconciliationClient;
deleteTimer = Timer.builder(name(DeletedAccountsDirectoryReconciler.class, "delete"))
.tag("replicationName", replicationName)
.register(Metrics.globalRegistry);
errorCounter = Counter.builder(name(DeletedAccountsDirectoryReconciler.class, "error"))
.tag("replicationName", replicationName)
.register(Metrics.globalRegistry);
}
public void onCrawlChunk(final List<User> deletedUsers) throws ChunkProcessingFailedException {
try {
deleteTimer.recordCallable(() -> {
try {
final DirectoryReconciliationResponse response = directoryReconciliationClient.delete(new DirectoryReconciliationRequest(null, null, deletedUsers));
if (response.getStatus() != DirectoryReconciliationResponse.Status.OK) {
errorCounter.increment();
throw new ChunkProcessingFailedException("Response status: " + response.getStatus());
}
} catch (final Exception e) {
errorCounter.increment();
throw new ChunkProcessingFailedException(e);
}
return null;
});
} catch (final ChunkProcessingFailedException e) {
throw e;
} catch (final Exception e) {
logger.warn("Unexpected exception", e);
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import com.amazonaws.services.dynamodbv2.AcquireLockOptions;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClient;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClientOptions;
import com.amazonaws.services.dynamodbv2.LockItem;
import com.amazonaws.services.dynamodbv2.ReleaseLockOptions;
import com.amazonaws.services.dynamodbv2.model.LockCurrentlyUnavailableException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
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.Pair;
public class DeletedAccountsManager {
private final DeletedAccounts deletedAccounts;
private final AmazonDynamoDBLockClient lockClient;
private static final Logger log = LoggerFactory.getLogger(DeletedAccountsManager.class);
@FunctionalInterface
public interface DeletedAccountReconciliationConsumer {
/**
* Reconcile a list of deleted account records.
*
* @param deletedAccounts the account records to reconcile
* @return a list of account records that were successfully reconciled; accounts that were not successfully
* reconciled may be retried later
* @throws ChunkProcessingFailedException in the event of an error while processing the batch of account records
*/
Collection<String> reconcile(List<Pair<UUID, String>> deletedAccounts) throws ChunkProcessingFailedException;
}
public DeletedAccountsManager(final DeletedAccounts deletedAccounts, final AmazonDynamoDB lockDynamoDb, final String lockTableName) {
this.deletedAccounts = deletedAccounts;
lockClient = new AmazonDynamoDBLockClient(
AmazonDynamoDBLockClientOptions.builder(lockDynamoDb, lockTableName)
.withPartitionKeyName(DeletedAccounts.KEY_ACCOUNT_E164)
.withLeaseDuration(15L)
.withHeartbeatPeriod(2L)
.withTimeUnit(TimeUnit.SECONDS)
.withCreateHeartbeatBackgroundThread(true)
.build());
}
public void put(final UUID uuid, final String e164) throws InterruptedException {
withLock(e164, () -> deletedAccounts.put(uuid, e164));
}
private void withLock(final String e164, final Runnable task) throws InterruptedException {
final LockItem lockItem = lockClient.acquireLock(AcquireLockOptions.builder(e164)
.withAcquireReleasedLocksConsistently(true)
.build());
try {
task.run();
} finally {
lockClient.releaseLock(ReleaseLockOptions.builder(lockItem)
.withBestEffort(true)
.build());
}
}
public void lockAndReconcileAccounts(final int max, final DeletedAccountReconciliationConsumer consumer) throws ChunkProcessingFailedException {
final List<LockItem> lockItems = new ArrayList<>();
final List<Pair<UUID, String>> reconciliationCandidates = deletedAccounts.listAccountsToReconcile(max).stream()
.filter(pair -> {
boolean lockAcquired = false;
try {
lockItems.add(lockClient.acquireLock(AcquireLockOptions.builder(pair.second())
.withAcquireReleasedLocksConsistently(true)
.withShouldSkipBlockingWait(true)
.build()));
lockAcquired = true;
} catch (final InterruptedException e) {
log.warn("Interrupted while acquiring lock for reconciliation", e);
} catch (final LockCurrentlyUnavailableException ignored) {
}
return lockAcquired;
})
.collect(Collectors.toList());
assert lockItems.size() == reconciliationCandidates.size();
// A deleted account's status may have changed in the time between getting a list of candidates and acquiring a lock
// on the candidate records. Now that we hold the lock, check which of the candidates still need to be reconciled.
final Set<String> numbersNeedingReconciliationAfterLock =
deletedAccounts.getAccountsNeedingReconciliation(reconciliationCandidates.stream()
.map(Pair::second)
.collect(Collectors.toList()));
final List<Pair<UUID, String>> accountsToReconcile = reconciliationCandidates.stream()
.filter(candidate -> numbersNeedingReconciliationAfterLock.contains(candidate.second()))
.collect(Collectors.toList());
try {
deletedAccounts.markReconciled(consumer.reconcile(accountsToReconcile));
} finally {
lockItems.forEach(lockItem -> lockClient.releaseLock(ReleaseLockOptions.builder(lockItem).withBestEffort(true).build()));
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name;
import io.micrometer.core.instrument.Metrics;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Collectors;
import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationRequest.User;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.util.Pair;
public class DeletedAccountsTableCrawler extends ManagedPeriodicWork {
private static final Duration WORKER_TTL = Duration.ofMinutes(2);
private static final Duration RUN_INTERVAL = Duration.ofMinutes(15);
private static final String ACTIVE_WORKER_KEY = "deleted_accounts_crawler_cache_active_worker";
private static final int MAX_BATCH_SIZE = 5_000;
private static final String BATCH_SIZE_DISTRIBUTION_NAME = name(DeletedAccountsTableCrawler.class, "batchSize");
private final DeletedAccountsManager deletedAccountsManager;
private final List<DeletedAccountsDirectoryReconciler> reconcilers;
public DeletedAccountsTableCrawler(
final DeletedAccountsManager deletedAccountsManager,
final List<DeletedAccountsDirectoryReconciler> reconcilers,
final FaultTolerantRedisCluster cluster,
final ScheduledExecutorService executorService) throws IOException {
super(new ManagedPeriodicWorkLock(ACTIVE_WORKER_KEY, cluster), WORKER_TTL, RUN_INTERVAL, executorService);
this.deletedAccountsManager = deletedAccountsManager;
this.reconcilers = reconcilers;
}
@Override
public void doPeriodicWork() throws Exception {
deletedAccountsManager.lockAndReconcileAccounts(MAX_BATCH_SIZE, deletedAccounts -> {
final List<User> deletedUsers = deletedAccounts.stream()
.map(pair -> new User(pair.first(), pair.second()))
.collect(Collectors.toList());
for (DeletedAccountsDirectoryReconciler reconciler : reconcilers) {
reconciler.onCrawlChunk(deletedUsers);
}
final List<String> reconciledPhoneNumbers = deletedAccounts.stream()
.map(Pair::second)
.collect(Collectors.toList());
Metrics.summary(BATCH_SIZE_DISTRIBUTION_NAME).record(reconciledPhoneNumbers.size());
return reconciledPhoneNumbers;
});
}
}

View File

@@ -276,10 +276,13 @@ public class Device {
@JsonProperty @JsonProperty
private boolean senderKey; private boolean senderKey;
@JsonProperty
private boolean announcementGroup;
public DeviceCapabilities() {} public DeviceCapabilities() {}
public DeviceCapabilities(boolean gv2, final boolean gv2_2, final boolean gv2_3, boolean storage, boolean transfer, public DeviceCapabilities(boolean gv2, final boolean gv2_2, final boolean gv2_3, boolean storage, boolean transfer,
boolean gv1Migration, final boolean senderKey) { boolean gv1Migration, final boolean senderKey, final boolean announcementGroup) {
this.gv2 = gv2; this.gv2 = gv2;
this.gv2_2 = gv2_2; this.gv2_2 = gv2_2;
this.gv2_3 = gv2_3; this.gv2_3 = gv2_3;
@@ -287,6 +290,7 @@ public class Device {
this.transfer = transfer; this.transfer = transfer;
this.gv1Migration = gv1Migration; this.gv1Migration = gv1Migration;
this.senderKey = senderKey; this.senderKey = senderKey;
this.announcementGroup = announcementGroup;
} }
public boolean isGv2() { public boolean isGv2() {
@@ -316,5 +320,9 @@ public class Device {
public boolean isSenderKey() { public boolean isSenderKey() {
return senderKey; return senderKey;
} }
public boolean isAnnouncementGroup() {
return announcementGroup;
}
} }
} }

View File

@@ -4,24 +4,23 @@
*/ */
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer; import com.codahale.metrics.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationRequest;
import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationResponse;
import org.whispersystems.textsecuregcm.util.Constants;
import javax.ws.rs.ProcessingException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import javax.ws.rs.ProcessingException;
import static com.codahale.metrics.MetricRegistry.name; import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationRequest;
import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationResponse;
import org.whispersystems.textsecuregcm.util.Constants;
public class DirectoryReconciler extends AccountDatabaseCrawlerListener { public class DirectoryReconciler extends AccountDatabaseCrawlerListener {
@@ -32,6 +31,8 @@ public class DirectoryReconciler extends AccountDatabaseCrawlerListener {
private final Timer sendChunkTimer; private final Timer sendChunkTimer;
private final Meter sendChunkErrorMeter; private final Meter sendChunkErrorMeter;
private boolean useV3Endpoints;
public DirectoryReconciler(String name, DirectoryReconciliationClient reconciliationClient) { public DirectoryReconciler(String name, DirectoryReconciliationClient reconciliationClient) {
this.reconciliationClient = reconciliationClient; this.reconciliationClient = reconciliationClient;
sendChunkTimer = metricRegistry.timer(name(DirectoryReconciler.class, name, "sendChunk")); sendChunkTimer = metricRegistry.timer(name(DirectoryReconciler.class, name, "sendChunk"));
@@ -45,6 +46,10 @@ public class DirectoryReconciler extends AccountDatabaseCrawlerListener {
public void onCrawlEnd(Optional<UUID> fromUuid) { public void onCrawlEnd(Optional<UUID> fromUuid) {
DirectoryReconciliationRequest request = new DirectoryReconciliationRequest(fromUuid.orElse(null), null, Collections.emptyList()); DirectoryReconciliationRequest request = new DirectoryReconciliationRequest(fromUuid.orElse(null), null, Collections.emptyList());
sendChunk(request); sendChunk(request);
if (useV3Endpoints) {
reconciliationClient.complete();
}
} }
@Override @Override
@@ -76,7 +81,12 @@ public class DirectoryReconciler extends AccountDatabaseCrawlerListener {
private DirectoryReconciliationResponse sendChunk(DirectoryReconciliationRequest request) { private DirectoryReconciliationResponse sendChunk(DirectoryReconciliationRequest request) {
try (Timer.Context timer = sendChunkTimer.time()) { try (Timer.Context timer = sendChunkTimer.time()) {
DirectoryReconciliationResponse response = reconciliationClient.sendChunk(request); DirectoryReconciliationResponse response;
if (useV3Endpoints) {
response = reconciliationClient.sendChunkV3(request);
} else {
response = reconciliationClient.sendChunk(request);
}
if (response.getStatus() != DirectoryReconciliationResponse.Status.OK) { if (response.getStatus() != DirectoryReconciliationResponse.Status.OK) {
sendChunkErrorMeter.mark(); sendChunkErrorMeter.mark();
logger.warn("reconciliation error: " + response.getStatus()); logger.warn("reconciliation error: " + response.getStatus());
@@ -89,4 +99,7 @@ public class DirectoryReconciler extends AccountDatabaseCrawlerListener {
} }
} }
public void setUseV3Endpoints(final boolean useV3Endpoints) {
this.useV3Endpoints = useV3Endpoints;
}
} }

View File

@@ -4,7 +4,16 @@
*/ */
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import javax.net.ssl.SSLContext;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import org.glassfish.jersey.SslConfigurator; import org.glassfish.jersey.SslConfigurator;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.whispersystems.textsecuregcm.configuration.DirectoryServerConfiguration; import org.whispersystems.textsecuregcm.configuration.DirectoryServerConfiguration;
@@ -14,16 +23,6 @@ import org.whispersystems.textsecuregcm.util.CertificateExpirationGauge;
import org.whispersystems.textsecuregcm.util.CertificateUtil; import org.whispersystems.textsecuregcm.util.CertificateUtil;
import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Constants;
import javax.net.ssl.SSLContext;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import static com.codahale.metrics.MetricRegistry.name;
public class DirectoryReconciliationClient { public class DirectoryReconciliationClient {
private final String replicationUrl; private final String replicationUrl;
@@ -47,6 +46,27 @@ public class DirectoryReconciliationClient {
.put(Entity.json(request), DirectoryReconciliationResponse.class); .put(Entity.json(request), DirectoryReconciliationResponse.class);
} }
public DirectoryReconciliationResponse sendChunkV3(DirectoryReconciliationRequest request) {
return client.target(replicationUrl)
.path("/v3/directory/exists")
.request(MediaType.APPLICATION_JSON_TYPE)
.put(Entity.json(request), DirectoryReconciliationResponse.class);
}
public DirectoryReconciliationResponse delete(DirectoryReconciliationRequest request) {
return client.target(replicationUrl)
.path("/v3/directory/deletes")
.request(MediaType.APPLICATION_JSON_TYPE)
.put(Entity.json(request), DirectoryReconciliationResponse.class);
}
public DirectoryReconciliationResponse complete() {
return client.target(replicationUrl)
.path("/v3/directory/complete")
.request(MediaType.APPLICATION_JSON_TYPE)
.post(null, DirectoryReconciliationResponse.class);
}
private static Client initializeClient(DirectoryServerConfiguration directoryServerConfiguration) private static Client initializeClient(DirectoryServerConfiguration directoryServerConfiguration)
throws CertificateException throws CertificateException
{ {

View File

@@ -1,27 +1,29 @@
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.InstanceProfileCredentialsProvider;
import com.amazonaws.services.appconfig.AmazonAppConfig;
import com.amazonaws.services.appconfig.AmazonAppConfigClient;
import com.amazonaws.services.appconfig.model.GetConfigurationRequest;
import com.amazonaws.services.appconfig.model.GetConfigurationResult;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import java.nio.charset.StandardCharsets; import software.amazon.awssdk.services.appconfig.AppConfigClient;
import java.util.Optional; import software.amazon.awssdk.services.appconfig.model.GetConfigurationRequest;
import java.util.UUID; import software.amazon.awssdk.services.appconfig.model.GetConfigurationResponse;
import java.util.concurrent.atomic.AtomicReference;
public class DynamicConfigurationManager { public class DynamicConfigurationManager {
@@ -29,29 +31,37 @@ public class DynamicConfigurationManager {
private final String environment; private final String environment;
private final String configurationName; private final String configurationName;
private final String clientId; private final String clientId;
private final AmazonAppConfig appConfigClient; private final AppConfigClient appConfigClient;
private final AtomicReference<DynamicConfiguration> configuration = new AtomicReference<>(); private final AtomicReference<DynamicConfiguration> configuration = new AtomicReference<>();
private final Logger logger = LoggerFactory.getLogger(DynamicConfigurationManager.class);
private GetConfigurationResult lastConfigResult; private GetConfigurationResponse lastConfigResult;
private boolean initialized = false; private boolean initialized = false;
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory()) private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule()); .registerModule(new JavaTimeModule());
private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
private static final Logger logger = LoggerFactory.getLogger(DynamicConfigurationManager.class);
public DynamicConfigurationManager(String application, String environment, String configurationName) { public DynamicConfigurationManager(String application, String environment, String configurationName) {
this(AmazonAppConfigClient.builder() this(AppConfigClient.builder()
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(10000).withRequestTimeout(10000)) .overrideConfiguration(ClientOverrideConfiguration.builder()
.withCredentials(InstanceProfileCredentialsProvider.getInstance()) .apiCallTimeout(Duration.ofMillis(10000))
.build(), .apiCallAttemptTimeout(Duration.ofMillis(10000)).build())
application, environment, configurationName, UUID.randomUUID().toString()); /* To specify specific credential provider:
https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/credentials.html
*/
.build(),
application, environment, configurationName, UUID.randomUUID().toString());
} }
@VisibleForTesting @VisibleForTesting
public DynamicConfigurationManager(AmazonAppConfig appConfigClient, String application, String environment, String configurationName, String clientId) { public DynamicConfigurationManager(AppConfigClient appConfigClient, String application, String environment,
String configurationName, String clientId) {
this.appConfigClient = appConfigClient; this.appConfigClient = appConfigClient;
this.application = application; this.application = application;
this.environment = environment; this.environment = environment;
@@ -92,19 +102,24 @@ public class DynamicConfigurationManager {
} }
private Optional<DynamicConfiguration> retrieveDynamicConfiguration() throws JsonProcessingException { private Optional<DynamicConfiguration> retrieveDynamicConfiguration() throws JsonProcessingException {
final String previousVersion = lastConfigResult != null ? lastConfigResult.getConfigurationVersion() : null; final String previousVersion = lastConfigResult != null ? lastConfigResult.configurationVersion() : null;
lastConfigResult = appConfigClient.getConfiguration(new GetConfigurationRequest().withApplication(application) lastConfigResult = appConfigClient.getConfiguration(GetConfigurationRequest.builder()
.withEnvironment(environment) .application(application)
.withConfiguration(configurationName) .environment(environment)
.withClientId(clientId) .configuration(configurationName)
.withClientConfigurationVersion(previousVersion)); .clientId(clientId)
.clientConfigurationVersion(previousVersion)
.build());
final Optional<DynamicConfiguration> maybeDynamicConfiguration; final Optional<DynamicConfiguration> maybeDynamicConfiguration;
if (!StringUtils.equals(lastConfigResult.getConfigurationVersion(), previousVersion)) { if (!StringUtils.equals(lastConfigResult.configurationVersion(), previousVersion)) {
logger.info("Received new config version: {}", lastConfigResult.getConfigurationVersion()); logger.info("Received new config version: {}", lastConfigResult.configurationVersion());
maybeDynamicConfiguration = Optional.of(OBJECT_MAPPER.readValue(StandardCharsets.UTF_8.decode(lastConfigResult.getContent().asReadOnlyBuffer()).toString(), DynamicConfiguration.class));
maybeDynamicConfiguration =
parseConfiguration(
StandardCharsets.UTF_8.decode(lastConfigResult.content().asByteBuffer().asReadOnlyBuffer()).toString());
} else { } else {
// No change since last version // No change since last version
maybeDynamicConfiguration = Optional.empty(); maybeDynamicConfiguration = Optional.empty();
@@ -113,6 +128,24 @@ public class DynamicConfigurationManager {
return maybeDynamicConfiguration; return maybeDynamicConfiguration;
} }
@VisibleForTesting
public static Optional<DynamicConfiguration> parseConfiguration(final String configurationYaml)
throws JsonProcessingException {
final DynamicConfiguration configuration = OBJECT_MAPPER.readValue(configurationYaml, DynamicConfiguration.class);
final Set<ConstraintViolation<DynamicConfiguration>> violations = VALIDATOR.validate(configuration);
final Optional<DynamicConfiguration> maybeDynamicConfiguration;
if (violations.isEmpty()) {
maybeDynamicConfiguration = Optional.of(configuration);
} else {
logger.warn("Failed to validate configuration: {}", violations);
maybeDynamicConfiguration = Optional.empty();
}
return maybeDynamicConfiguration;
}
private DynamicConfiguration retrieveInitialDynamicConfiguration() { private DynamicConfiguration retrieveInitialDynamicConfiguration() {
for (;;) { for (;;) {
try { try {

View File

@@ -7,195 +7,229 @@ package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import com.amazonaws.services.dynamodbv2.document.DeleteItemOutcome;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.PrimaryKey;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.dynamodbv2.document.TableWriteItems;
import com.amazonaws.services.dynamodbv2.document.spec.DeleteItemSpec;
import com.amazonaws.services.dynamodbv2.document.spec.QuerySpec;
import com.amazonaws.services.dynamodbv2.model.ReturnValue;
import com.amazonaws.services.dynamodbv2.model.Select;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.DistributionSummary; import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.Timer;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import org.whispersystems.textsecuregcm.entities.PreKey; import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.util.UUIDUtil; import org.whispersystems.textsecuregcm.util.AttributeValues;
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.DeleteItemResponse;
import software.amazon.awssdk.services.dynamodb.model.DeleteRequest;
import software.amazon.awssdk.services.dynamodb.model.PutRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;
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 { public class KeysDynamoDb extends AbstractDynamoDbStore {
private final Table table; private final String tableName;
static final String KEY_ACCOUNT_UUID = "U"; static final String KEY_ACCOUNT_UUID = "U";
static final String KEY_DEVICE_ID_KEY_ID = "DK"; static final String KEY_DEVICE_ID_KEY_ID = "DK";
static final String KEY_PUBLIC_KEY = "P"; static final String KEY_PUBLIC_KEY = "P";
private static final Timer STORE_KEYS_TIMER = Metrics.timer(name(KeysDynamoDb.class, "storeKeys")); 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_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 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 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_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 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 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 DistributionSummary KEY_COUNT_DISTRIBUTION = Metrics.summary(name(KeysDynamoDb.class, "keyCount"));
public KeysDynamoDb(final DynamoDB dynamoDB, final String tableName) { public KeysDynamoDb(final DynamoDbClient dynamoDB, final String tableName) {
super(dynamoDB); super(dynamoDB);
this.tableName = tableName;
}
this.table = dynamoDB.getTable(tableName); public void store(final Account account, final long deviceId, final List<PreKey> keys) {
} STORE_KEYS_TIMER.record(() -> {
delete(account, deviceId);
public void store(final Account account, final long deviceId, final List<PreKey> keys) { writeInBatches(keys, batch -> {
STORE_KEYS_TIMER.record(() -> { List<WriteRequest> items = new ArrayList<>();
delete(account, deviceId); for (final PreKey preKey : batch) {
items.add(WriteRequest.builder()
.putRequest(PutRequest.builder()
.item(getItemFromPreKey(account.getUuid(), deviceId, preKey))
.build())
.build());
}
executeTableWriteItemsUntilComplete(Map.of(tableName, items));
});
});
}
writeInBatches(keys, batch -> { public Optional<PreKey> take(final Account account, final long deviceId) {
final TableWriteItems items = new TableWriteItems(table.getTableName()); return TAKE_KEY_FOR_DEVICE_TIMER.record(() -> {
final AttributeValue partitionKey = getPartitionKey(account.getUuid());
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", partitionKey,
":sortprefix", getSortKeyPrefix(deviceId)))
.projectionExpression(KEY_DEVICE_ID_KEY_ID)
.consistentRead(false)
.build();
for (final PreKey preKey : batch) { int contestedKeys = 0;
items.addItemToPut(getItemFromPreKey(account.getUuid(), deviceId, preKey));
}
executeTableWriteItemsUntilComplete(items); try {
}); QueryResponse response = db().query(queryRequest);
}); for (Map<String, AttributeValue> candidate : response.items()) {
} DeleteItemRequest deleteItemRequest = DeleteItemRequest.builder()
.tableName(tableName)
.key(Map.of(
KEY_ACCOUNT_UUID, partitionKey,
KEY_DEVICE_ID_KEY_ID, candidate.get(KEY_DEVICE_ID_KEY_ID)))
.returnValues(ReturnValue.ALL_OLD)
.build();
DeleteItemResponse deleteItemResponse = db().deleteItem(deleteItemRequest);
if (deleteItemResponse.hasAttributes()) {
return Optional.of(getPreKeyFromItem(deleteItemResponse.attributes()));
}
public Optional<PreKey> take(final Account account, final long deviceId) { contestedKeys++;
return TAKE_KEY_FOR_DEVICE_TIMER.record(() -> { }
final byte[] partitionKey = getPartitionKey(account.getUuid());
final QuerySpec querySpec = new QuerySpec().withKeyConditionExpression("#uuid = :uuid AND begins_with (#sort, :sortprefix)") return Optional.empty();
.withNameMap(Map.of("#uuid", KEY_ACCOUNT_UUID, "#sort", KEY_DEVICE_ID_KEY_ID)) } finally {
.withValueMap(Map.of(":uuid", partitionKey, CONTESTED_KEY_DISTRIBUTION.record(contestedKeys);
":sortprefix", getSortKeyPrefix(deviceId))) }
.withProjectionExpression(KEY_DEVICE_ID_KEY_ID) });
.withConsistentRead(false); }
int contestedKeys = 0; public Map<Long, PreKey> take(final Account account) {
return TAKE_KEYS_FOR_ACCOUNT_TIMER.record(() -> {
final Map<Long, PreKey> preKeysByDeviceId = new HashMap<>();
try { for (final Device device : account.getDevices()) {
for (final Item candidate : table.query(querySpec)) { take(account, device.getId()).ifPresent(preKey -> preKeysByDeviceId.put(device.getId(), preKey));
final DeleteItemSpec deleteItemSpec = new DeleteItemSpec().withPrimaryKey(KEY_ACCOUNT_UUID, partitionKey, KEY_DEVICE_ID_KEY_ID, candidate.getBinary(KEY_DEVICE_ID_KEY_ID)) }
.withReturnValues(ReturnValue.ALL_OLD);
final DeleteItemOutcome outcome = table.deleteItem(deleteItemSpec); return preKeysByDeviceId;
});
}
if (outcome.getItem() != null) { public int getCount(final Account account, final long deviceId) {
return Optional.of(getPreKeyFromItem(outcome.getItem())); 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()),
":sortprefix", getSortKeyPrefix(deviceId)))
.select(Select.COUNT)
.consistentRead(false)
.build();
contestedKeys++; int keyCount = 0;
} // This is very confusing, but does appear to be the intended behavior. See:
//
// - https://github.com/aws/aws-sdk-java/issues/693
// - https://github.com/aws/aws-sdk-java/issues/915
// - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.Count
for (final QueryResponse page : db().queryPaginator(queryRequest)) {
keyCount += page.count();
}
KEY_COUNT_DISTRIBUTION.record(keyCount);
return keyCount;
});
}
return Optional.empty(); public void delete(final Account account) {
} finally { DELETE_KEYS_FOR_ACCOUNT_TIMER.record(() -> {
CONTESTED_KEY_DISTRIBUTION.record(contestedKeys); final QueryRequest queryRequest = QueryRequest.builder()
} .tableName(tableName)
}); .keyConditionExpression("#uuid = :uuid")
} .expressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID))
.expressionAttributeValues(Map.of(
":uuid", getPartitionKey(account.getUuid())))
.projectionExpression(KEY_DEVICE_ID_KEY_ID)
.consistentRead(true)
.build();
public Map<Long, PreKey> take(final Account account) { deleteItemsForAccountMatchingQuery(account, queryRequest);
return TAKE_KEYS_FOR_ACCOUNT_TIMER.record(() -> { });
final Map<Long, PreKey> preKeysByDeviceId = new HashMap<>(); }
for (final Device device : account.getDevices()) { public void delete(final Account account, final long deviceId) {
take(account, device.getId()).ifPresent(preKey -> preKeysByDeviceId.put(device.getId(), preKey)); DELETE_KEYS_FOR_DEVICE_TIMER.record(() -> {
} final 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()),
":sortprefix", getSortKeyPrefix(deviceId)))
.projectionExpression(KEY_DEVICE_ID_KEY_ID)
.consistentRead(true)
.build();
return preKeysByDeviceId; deleteItemsForAccountMatchingQuery(account, queryRequest);
}); });
} }
public int getCount(final Account account, final long deviceId) { private void deleteItemsForAccountMatchingQuery(final Account account, final QueryRequest querySpec) {
return GET_KEY_COUNT_TIMER.record(() -> { final AttributeValue partitionKey = getPartitionKey(account.getUuid());
final QuerySpec querySpec = new QuerySpec().withKeyConditionExpression("#uuid = :uuid AND begins_with (#sort, :sortprefix)")
.withNameMap(Map.of("#uuid", KEY_ACCOUNT_UUID, "#sort", KEY_DEVICE_ID_KEY_ID))
.withValueMap(Map.of(":uuid", getPartitionKey(account.getUuid()),
":sortprefix", getSortKeyPrefix(deviceId)))
.withSelect(Select.COUNT)
.withConsistentRead(false);
final int keyCount = (int)countItemsMatchingQuery(table, querySpec); writeInBatches(db().query(querySpec).items(), batch -> {
List<WriteRequest> deletes = new ArrayList<>();
for (final Map<String, AttributeValue> item : batch) {
deletes.add(WriteRequest.builder()
.deleteRequest(DeleteRequest.builder()
.key(Map.of(
KEY_ACCOUNT_UUID, partitionKey,
KEY_DEVICE_ID_KEY_ID, item.get(KEY_DEVICE_ID_KEY_ID)))
.build())
.build());
}
executeTableWriteItemsUntilComplete(Map.of(tableName, deletes));
});
}
KEY_COUNT_DISTRIBUTION.record(keyCount); private static AttributeValue getPartitionKey(final UUID accountUuid) {
return keyCount; return AttributeValues.fromUUID(accountUuid);
}); }
}
public void delete(final Account account) { private static AttributeValue getSortKey(final long deviceId, final long keyId) {
DELETE_KEYS_FOR_ACCOUNT_TIMER.record(() -> { final ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]);
final QuerySpec querySpec = new QuerySpec().withKeyConditionExpression("#uuid = :uuid") byteBuffer.putLong(deviceId);
.withNameMap(Map.of("#uuid", KEY_ACCOUNT_UUID)) byteBuffer.putLong(keyId);
.withValueMap(Map.of(":uuid", getPartitionKey(account.getUuid()))) return AttributeValues.fromByteBuffer(byteBuffer.flip());
.withProjectionExpression(KEY_DEVICE_ID_KEY_ID) }
.withConsistentRead(true);
deleteItemsForAccountMatchingQuery(account, querySpec); @VisibleForTesting
}); static AttributeValue getSortKeyPrefix(final long deviceId) {
} final ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[8]);
byteBuffer.putLong(deviceId);
return AttributeValues.fromByteBuffer(byteBuffer.flip());
}
@VisibleForTesting private Map<String, AttributeValue> getItemFromPreKey(final UUID accountUuid, final long deviceId, final PreKey preKey) {
void delete(final Account account, final long deviceId) { return Map.of(
DELETE_KEYS_FOR_DEVICE_TIMER.record(() -> { KEY_ACCOUNT_UUID, getPartitionKey(accountUuid),
final QuerySpec querySpec = new QuerySpec().withKeyConditionExpression("#uuid = :uuid AND begins_with (#sort, :sortprefix)") KEY_DEVICE_ID_KEY_ID, getSortKey(deviceId, preKey.getKeyId()),
.withNameMap(Map.of("#uuid", KEY_ACCOUNT_UUID, "#sort", KEY_DEVICE_ID_KEY_ID)) KEY_PUBLIC_KEY, AttributeValues.fromString(preKey.getPublicKey()));
.withValueMap(Map.of(":uuid", getPartitionKey(account.getUuid()), }
":sortprefix", getSortKeyPrefix(deviceId)))
.withProjectionExpression(KEY_DEVICE_ID_KEY_ID)
.withConsistentRead(true);
deleteItemsForAccountMatchingQuery(account, querySpec); private PreKey getPreKeyFromItem(Map<String, AttributeValue> item) {
}); final long keyId = item.get(KEY_DEVICE_ID_KEY_ID).b().asByteBuffer().getLong(8);
} return new PreKey(keyId, item.get(KEY_PUBLIC_KEY).s());
}
private void deleteItemsForAccountMatchingQuery(final Account account, final QuerySpec querySpec) {
final byte[] partitionKey = getPartitionKey(account.getUuid());
writeInBatches(table.query(querySpec), batch -> {
final TableWriteItems writeItems = new TableWriteItems(table.getTableName());
for (final Item item : batch) {
writeItems.addPrimaryKeyToDelete(new PrimaryKey(KEY_ACCOUNT_UUID, partitionKey, KEY_DEVICE_ID_KEY_ID, item.getBinary(KEY_DEVICE_ID_KEY_ID)));
}
executeTableWriteItemsUntilComplete(writeItems);
});
}
private static byte[] getPartitionKey(final UUID accountUuid) {
return UUIDUtil.toBytes(accountUuid);
}
private static byte[] getSortKey(final long deviceId, final long keyId) {
final ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]);
byteBuffer.putLong(deviceId);
byteBuffer.putLong(keyId);
return byteBuffer.array();
}
private static byte[] getSortKeyPrefix(final long deviceId) {
final ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[8]);
byteBuffer.putLong(deviceId);
return byteBuffer.array();
}
private Item getItemFromPreKey(final UUID accountUuid, final long deviceId, final PreKey preKey) {
return new Item().withBinary(KEY_ACCOUNT_UUID, getPartitionKey(accountUuid))
.withBinary(KEY_DEVICE_ID_KEY_ID, getSortKey(deviceId, preKey.getKeyId()))
.withString(KEY_PUBLIC_KEY, preKey.getPublicKey());
}
private PreKey getPreKeyFromItem(final Item item) {
final long keyId = ByteBuffer.wrap(item.getBinary(KEY_DEVICE_ID_KEY_ID)).getLong(8);
return new PreKey(keyId, item.getString(KEY_PUBLIC_KEY));
}
} }

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.lifecycle.Managed;
import io.micrometer.core.instrument.Metrics;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Util;
public abstract class ManagedPeriodicWork implements Managed {
private final Logger logger = LoggerFactory.getLogger(getClass());
private static final String FUTURE_DONE_GAUGE_NAME = "futureDone";
private final ManagedPeriodicWorkLock lock;
private final Duration workerTtl;
private final Duration runInterval;
private final String workerId;
private final ScheduledExecutorService executorService;
private Duration sleepDurationAfterUnexpectedException = Duration.ofSeconds(10);
@Nullable
private ScheduledFuture<?> scheduledFuture;
private AtomicReference<CompletableFuture<Void>> activeExecutionFuture = new AtomicReference<>(CompletableFuture.completedFuture(null));
public ManagedPeriodicWork(final ManagedPeriodicWorkLock lock, final Duration workerTtl, final Duration runInterval, final ScheduledExecutorService scheduledExecutorService) {
this.lock = lock;
this.workerTtl = workerTtl;
this.runInterval = runInterval;
this.workerId = UUID.randomUUID().toString();
this.executorService = scheduledExecutorService;
}
abstract protected void doPeriodicWork() throws Exception;
@Override
public synchronized void start() throws Exception {
if (scheduledFuture != null) {
return;
}
scheduledFuture = executorService.scheduleAtFixedRate(() -> {
try {
execute();
} catch (final Exception e) {
logger.warn("Error in execution", e);
// wait a bit, in case the error is caused by external instability
Util.sleep(sleepDurationAfterUnexpectedException.toMillis());
}
}, 0, runInterval.getSeconds(), TimeUnit.SECONDS);
Metrics.gauge(name(getClass(), FUTURE_DONE_GAUGE_NAME), scheduledFuture, future -> future.isDone() ? 1 : 0);
}
@Override
public synchronized void stop() throws Exception {
if (scheduledFuture != null) {
scheduledFuture.cancel(false);
try {
activeExecutionFuture.get().join();
} catch (final Exception e) {
logger.warn("error while awaiting final execution", e);
}
}
}
public void setSleepDurationAfterUnexpectedException(final Duration sleepDurationAfterUnexpectedException) {
this.sleepDurationAfterUnexpectedException = sleepDurationAfterUnexpectedException;
}
private void execute() {
if (lock.claimActiveWork(workerId, workerTtl)) {
try {
activeExecutionFuture.set(new CompletableFuture<>());
logger.info("Starting execution");
doPeriodicWork();
logger.info("Execution complete");
} catch (final Exception e) {
logger.warn("Periodic work failed", e);
// wait a bit, in case the error is caused by external instability
Util.sleep(sleepDurationAfterUnexpectedException.toMillis());
} finally {
try {
lock.releaseActiveWork(workerId);
} finally {
activeExecutionFuture.get().complete(null);
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import io.lettuce.core.ScriptOutputType;
import io.lettuce.core.SetArgs;
import org.whispersystems.textsecuregcm.redis.ClusterLuaScript;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
public class ManagedPeriodicWorkLock {
private final String activeWorkerKey;
private final FaultTolerantRedisCluster cacheCluster;
private final ClusterLuaScript unlockClusterScript;
public ManagedPeriodicWorkLock(final String activeWorkerKey, final FaultTolerantRedisCluster cacheCluster) throws IOException {
this.activeWorkerKey = activeWorkerKey;
this.cacheCluster = cacheCluster;
this.unlockClusterScript = ClusterLuaScript.fromResource(cacheCluster, "lua/periodic_worker/unlock.lua", ScriptOutputType.INTEGER);
}
public boolean claimActiveWork(String workerId, Duration ttl) {
return "OK".equals(cacheCluster.withCluster(connection -> connection.sync().set(activeWorkerKey, workerId, SetArgs.Builder.nx().px(ttl.toMillis()))));
}
public void releaseActiveWork(String workerId) {
unlockClusterScript.execute(List.of(activeWorkerKey), List.of(workerId));
}
}

View File

@@ -8,17 +8,7 @@ package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import static io.micrometer.core.instrument.Metrics.timer; import static io.micrometer.core.instrument.Metrics.timer;
import com.amazonaws.services.dynamodbv2.document.DeleteItemOutcome; import com.google.common.collect.ImmutableMap;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Index;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.PrimaryKey;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.dynamodbv2.document.TableWriteItems;
import com.amazonaws.services.dynamodbv2.document.api.QueryApi;
import com.amazonaws.services.dynamodbv2.document.spec.DeleteItemSpec;
import com.amazonaws.services.dynamodbv2.document.spec.QuerySpec;
import com.amazonaws.services.dynamodbv2.model.ReturnValue;
import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.Timer;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.time.Duration; import java.time.Duration;
@@ -27,11 +17,22 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.whispersystems.textsecuregcm.entities.MessageProtos; import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.UUIDUtil; import org.whispersystems.textsecuregcm.util.UUIDUtil;
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.DeleteItemResponse;
import software.amazon.awssdk.services.dynamodb.model.DeleteRequest;
import software.amazon.awssdk.services.dynamodb.model.PutRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
import software.amazon.awssdk.services.dynamodb.model.WriteRequest;
public class MessagesDynamoDb extends AbstractDynamoDbStore { public class MessagesDynamoDb extends AbstractDynamoDbStore {
@@ -60,7 +61,7 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
private final String tableName; private final String tableName;
private final Duration timeToLive; private final Duration timeToLive;
public MessagesDynamoDb(DynamoDB dynamoDb, String tableName, Duration timeToLive) { public MessagesDynamoDb(DynamoDbClient dynamoDb, String tableName, Duration timeToLive) {
super(dynamoDb); super(dynamoDb);
this.tableName = tableName; this.tableName = tableName;
@@ -76,54 +77,61 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
throw new IllegalArgumentException("Maximum batch size of " + DYNAMO_DB_MAX_BATCH_SIZE + " execeeded with " + messages.size() + " messages"); throw new IllegalArgumentException("Maximum batch size of " + DYNAMO_DB_MAX_BATCH_SIZE + " execeeded with " + messages.size() + " messages");
} }
final byte[] partitionKey = convertPartitionKey(destinationAccountUuid); final AttributeValue partitionKey = convertPartitionKey(destinationAccountUuid);
TableWriteItems items = new TableWriteItems(tableName); List<WriteRequest> writeItems = new ArrayList<>();
for (MessageProtos.Envelope message : messages) { for (MessageProtos.Envelope message : messages) {
final UUID messageUuid = UUID.fromString(message.getServerGuid()); final UUID messageUuid = UUID.fromString(message.getServerGuid());
final Item item = new Item().withBinary(KEY_PARTITION, partitionKey) final ImmutableMap.Builder<String, AttributeValue> item = ImmutableMap.<String, AttributeValue>builder()
.withBinary(KEY_SORT, convertSortKey(destinationDeviceId, message.getServerTimestamp(), messageUuid)) .put(KEY_PARTITION, partitionKey)
.withBinary(LOCAL_INDEX_MESSAGE_UUID_KEY_SORT, convertLocalIndexMessageUuidSortKey(messageUuid)) .put(KEY_SORT, convertSortKey(destinationDeviceId, message.getServerTimestamp(), messageUuid))
.withInt(KEY_TYPE, message.getType().getNumber()) .put(LOCAL_INDEX_MESSAGE_UUID_KEY_SORT, convertLocalIndexMessageUuidSortKey(messageUuid))
.withLong(KEY_TIMESTAMP, message.getTimestamp()) .put(KEY_TYPE, AttributeValues.fromInt(message.getType().getNumber()))
.withLong(KEY_TTL, getTtlForMessage(message)); .put(KEY_TIMESTAMP, AttributeValues.fromLong(message.getTimestamp()))
.put(KEY_TTL, AttributeValues.fromLong(getTtlForMessage(message)));
if (message.hasRelay() && message.getRelay().length() > 0) { if (message.hasRelay() && message.getRelay().length() > 0) {
item.withString(KEY_RELAY, message.getRelay()); item.put(KEY_RELAY, AttributeValues.fromString(message.getRelay()));
} }
if (message.hasSource()) { if (message.hasSource()) {
item.withString(KEY_SOURCE, message.getSource()); item.put(KEY_SOURCE, AttributeValues.fromString(message.getSource()));
} }
if (message.hasSourceUuid()) { if (message.hasSourceUuid()) {
item.withBinary(KEY_SOURCE_UUID, UUIDUtil.toBytes(UUID.fromString(message.getSourceUuid()))); item.put(KEY_SOURCE_UUID, AttributeValues.fromUUID(UUID.fromString(message.getSourceUuid())));
} }
if (message.hasSourceDevice()) { if (message.hasSourceDevice()) {
item.withInt(KEY_SOURCE_DEVICE, message.getSourceDevice()); item.put(KEY_SOURCE_DEVICE, AttributeValues.fromInt(message.getSourceDevice()));
} }
if (message.hasLegacyMessage()) { if (message.hasLegacyMessage()) {
item.withBinary(KEY_MESSAGE, message.getLegacyMessage().toByteArray()); item.put(KEY_MESSAGE, AttributeValues.fromByteArray(message.getLegacyMessage().toByteArray()));
} }
if (message.hasContent()) { if (message.hasContent()) {
item.withBinary(KEY_CONTENT, message.getContent().toByteArray()); item.put(KEY_CONTENT, AttributeValues.fromByteArray(message.getContent().toByteArray()));
} }
items.addItemToPut(item); writeItems.add(WriteRequest.builder().putRequest(PutRequest.builder()
.item(item.build())
.build()).build());
} }
executeTableWriteItemsUntilComplete(items); executeTableWriteItemsUntilComplete(Map.of(tableName, writeItems));
} }
public List<OutgoingMessageEntity> load(final UUID destinationAccountUuid, final long destinationDeviceId, final int requestedNumberOfMessagesToFetch) { public List<OutgoingMessageEntity> load(final UUID destinationAccountUuid, final long destinationDeviceId, final int requestedNumberOfMessagesToFetch) {
return loadTimer.record(() -> { return loadTimer.record(() -> {
final int numberOfMessagesToFetch = Math.min(requestedNumberOfMessagesToFetch, RESULT_SET_CHUNK_SIZE); final int numberOfMessagesToFetch = Math.min(requestedNumberOfMessagesToFetch, RESULT_SET_CHUNK_SIZE);
final byte[] partitionKey = convertPartitionKey(destinationAccountUuid); final AttributeValue partitionKey = convertPartitionKey(destinationAccountUuid);
final QuerySpec querySpec = new QuerySpec().withConsistentRead(true) final QueryRequest queryRequest = QueryRequest.builder()
.withKeyConditionExpression("#part = :part AND begins_with ( #sort , :sortprefix )") .tableName(tableName)
.withNameMap(Map.of("#part", KEY_PARTITION, .consistentRead(true)
"#sort", KEY_SORT)) .keyConditionExpression("#part = :part AND begins_with ( #sort , :sortprefix )")
.withValueMap(Map.of(":part", partitionKey, .expressionAttributeNames(Map.of(
":sortprefix", convertDestinationDeviceIdToSortKeyPrefix(destinationDeviceId))) "#part", KEY_PARTITION,
.withMaxResultSize(numberOfMessagesToFetch); "#sort", KEY_SORT))
final Table table = getDynamoDb().getTable(tableName); .expressionAttributeValues(Map.of(
":part", partitionKey,
":sortprefix", convertDestinationDeviceIdToSortKeyPrefix(destinationDeviceId)))
.limit(numberOfMessagesToFetch)
.build();
List<OutgoingMessageEntity> messageEntities = new ArrayList<>(numberOfMessagesToFetch); List<OutgoingMessageEntity> messageEntities = new ArrayList<>(numberOfMessagesToFetch);
for (Item message : table.query(querySpec)) { for (Map<String, AttributeValue> message : db().query(queryRequest).items()) {
messageEntities.add(convertItemToOutgoingMessageEntity(message)); messageEntities.add(convertItemToOutgoingMessageEntity(message));
} }
return messageEntities; return messageEntities;
@@ -136,53 +144,63 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
throw new IllegalArgumentException("must specify a source"); throw new IllegalArgumentException("must specify a source");
} }
final byte[] partitionKey = convertPartitionKey(destinationAccountUuid); final AttributeValue partitionKey = convertPartitionKey(destinationAccountUuid);
final QuerySpec querySpec = new QuerySpec().withProjectionExpression(KEY_SORT) final QueryRequest queryRequest = QueryRequest.builder()
.withConsistentRead(true) .tableName(tableName)
.withKeyConditionExpression("#part = :part AND begins_with ( #sort , :sortprefix )") .projectionExpression(KEY_SORT)
.withFilterExpression("#source = :source AND #timestamp = :timestamp") .consistentRead(true)
.withNameMap(Map.of("#part", KEY_PARTITION, .keyConditionExpression("#part = :part AND begins_with ( #sort , :sortprefix )")
"#sort", KEY_SORT, .filterExpression("#source = :source AND #timestamp = :timestamp")
"#source", KEY_SOURCE, .expressionAttributeNames(Map.of(
"#timestamp", KEY_TIMESTAMP)) "#part", KEY_PARTITION,
.withValueMap(Map.of(":part", partitionKey, "#sort", KEY_SORT,
":sortprefix", convertDestinationDeviceIdToSortKeyPrefix(destinationDeviceId), "#source", KEY_SOURCE,
":source", source, "#timestamp", KEY_TIMESTAMP))
":timestamp", timestamp)); .expressionAttributeValues(Map.of(
":part", partitionKey,
":sortprefix", convertDestinationDeviceIdToSortKeyPrefix(destinationDeviceId),
":source", AttributeValues.fromString(source),
":timestamp", AttributeValues.fromLong(timestamp)))
.build();
final Table table = getDynamoDb().getTable(tableName); return deleteItemsMatchingQueryAndReturnFirstOneActuallyDeleted(partitionKey, queryRequest);
return deleteItemsMatchingQueryAndReturnFirstOneActuallyDeleted(table, partitionKey, querySpec, table);
}); });
} }
public Optional<OutgoingMessageEntity> deleteMessageByDestinationAndGuid(final UUID destinationAccountUuid, final long destinationDeviceId, final UUID messageUuid) { public Optional<OutgoingMessageEntity> deleteMessageByDestinationAndGuid(final UUID destinationAccountUuid, final long destinationDeviceId, final UUID messageUuid) {
return deleteByGuid.record(() -> { return deleteByGuid.record(() -> {
final byte[] partitionKey = convertPartitionKey(destinationAccountUuid); final AttributeValue partitionKey = convertPartitionKey(destinationAccountUuid);
final QuerySpec querySpec = new QuerySpec().withProjectionExpression(KEY_SORT) final QueryRequest queryRequest = QueryRequest.builder()
.withConsistentRead(true) .tableName(tableName)
.withKeyConditionExpression("#part = :part AND #uuid = :uuid") .indexName(LOCAL_INDEX_MESSAGE_UUID_NAME)
.withNameMap(Map.of("#part", KEY_PARTITION, .projectionExpression(KEY_SORT)
"#uuid", LOCAL_INDEX_MESSAGE_UUID_KEY_SORT)) .consistentRead(true)
.withValueMap(Map.of(":part", partitionKey, .keyConditionExpression("#part = :part AND #uuid = :uuid")
":uuid", convertLocalIndexMessageUuidSortKey(messageUuid))); .expressionAttributeNames(Map.of(
final Table table = getDynamoDb().getTable(tableName); "#part", KEY_PARTITION,
final Index index = table.getIndex(LOCAL_INDEX_MESSAGE_UUID_NAME); "#uuid", LOCAL_INDEX_MESSAGE_UUID_KEY_SORT))
return deleteItemsMatchingQueryAndReturnFirstOneActuallyDeleted(table, partitionKey, querySpec, index); .expressionAttributeValues(Map.of(
":part", partitionKey,
":uuid", convertLocalIndexMessageUuidSortKey(messageUuid)))
.build();
return deleteItemsMatchingQueryAndReturnFirstOneActuallyDeleted(partitionKey, queryRequest);
}); });
} }
@Nonnull @Nonnull
private Optional<OutgoingMessageEntity> deleteItemsMatchingQueryAndReturnFirstOneActuallyDeleted(Table table, byte[] partitionKey, QuerySpec querySpec, QueryApi queryApi) { private Optional<OutgoingMessageEntity> deleteItemsMatchingQueryAndReturnFirstOneActuallyDeleted(AttributeValue partitionKey, QueryRequest queryRequest) {
Optional<OutgoingMessageEntity> result = Optional.empty(); Optional<OutgoingMessageEntity> result = Optional.empty();
for (Item item : queryApi.query(querySpec)) { for (Map<String, AttributeValue> item : db().query(queryRequest).items()) {
final byte[] rangeKeyValue = item.getBinary(KEY_SORT); final byte[] rangeKeyValue = item.get(KEY_SORT).b().asByteArray();
DeleteItemSpec deleteItemSpec = new DeleteItemSpec().withPrimaryKey(KEY_PARTITION, partitionKey, KEY_SORT, rangeKeyValue); DeleteItemRequest.Builder deleteItemRequest = DeleteItemRequest.builder()
.tableName(tableName)
.key(Map.of(KEY_PARTITION, partitionKey, KEY_SORT, AttributeValues.fromByteArray(rangeKeyValue)));
if (result.isEmpty()) { if (result.isEmpty()) {
deleteItemSpec.withReturnValues(ReturnValue.ALL_OLD); deleteItemRequest.returnValues(ReturnValue.ALL_OLD);
} }
final DeleteItemOutcome deleteItemOutcome = table.deleteItem(deleteItemSpec); final DeleteItemResponse deleteItemResponse = db().deleteItem(deleteItemRequest.build());
if (deleteItemOutcome.getItem() != null && deleteItemOutcome.getItem().hasAttribute(KEY_PARTITION)) { if (deleteItemResponse.attributes() != null && deleteItemResponse.attributes().containsKey(KEY_PARTITION)) {
result = Optional.of(convertItemToOutgoingMessageEntity(deleteItemOutcome.getItem())); result = Optional.of(convertItemToOutgoingMessageEntity(deleteItemResponse.attributes()));
} }
} }
return result; return result;
@@ -190,74 +208,88 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
public void deleteAllMessagesForAccount(final UUID destinationAccountUuid) { public void deleteAllMessagesForAccount(final UUID destinationAccountUuid) {
deleteByAccount.record(() -> { deleteByAccount.record(() -> {
final byte[] partitionKey = convertPartitionKey(destinationAccountUuid); final AttributeValue partitionKey = convertPartitionKey(destinationAccountUuid);
final QuerySpec querySpec = new QuerySpec().withHashKey(KEY_PARTITION, partitionKey) final QueryRequest queryRequest = QueryRequest.builder()
.withProjectionExpression(KEY_SORT) .tableName(tableName)
.withConsistentRead(true); .projectionExpression(KEY_SORT)
deleteRowsMatchingQuery(partitionKey, querySpec); .consistentRead(true)
.keyConditionExpression("#part = :part")
.expressionAttributeNames(Map.of("#part", KEY_PARTITION))
.expressionAttributeValues(Map.of(":part", partitionKey))
.build();
deleteRowsMatchingQuery(partitionKey, queryRequest);
}); });
} }
public void deleteAllMessagesForDevice(final UUID destinationAccountUuid, final long destinationDeviceId) { public void deleteAllMessagesForDevice(final UUID destinationAccountUuid, final long destinationDeviceId) {
deleteByDevice.record(() -> { deleteByDevice.record(() -> {
final byte[] partitionKey = convertPartitionKey(destinationAccountUuid); final AttributeValue partitionKey = convertPartitionKey(destinationAccountUuid);
final QuerySpec querySpec = new QuerySpec().withKeyConditionExpression("#part = :part AND begins_with ( #sort , :sortprefix )") final QueryRequest queryRequest = QueryRequest.builder()
.withNameMap(Map.of("#part", KEY_PARTITION, .tableName(tableName)
"#sort", KEY_SORT)) .keyConditionExpression("#part = :part AND begins_with ( #sort , :sortprefix )")
.withValueMap(Map.of(":part", partitionKey, .expressionAttributeNames(Map.of(
":sortprefix", convertDestinationDeviceIdToSortKeyPrefix(destinationDeviceId))) "#part", KEY_PARTITION,
.withProjectionExpression(KEY_SORT) "#sort", KEY_SORT))
.withConsistentRead(true); .expressionAttributeValues(Map.of(
deleteRowsMatchingQuery(partitionKey, querySpec); ":part", partitionKey,
":sortprefix", convertDestinationDeviceIdToSortKeyPrefix(destinationDeviceId)))
.projectionExpression(KEY_SORT)
.consistentRead(true)
.build();
deleteRowsMatchingQuery(partitionKey, queryRequest);
}); });
} }
private OutgoingMessageEntity convertItemToOutgoingMessageEntity(Item message) { private OutgoingMessageEntity convertItemToOutgoingMessageEntity(Map<String, AttributeValue> message) {
final SortKey sortKey = convertSortKey(message.getBinary(KEY_SORT)); final SortKey sortKey = convertSortKey(message.get(KEY_SORT).b().asByteArray());
final UUID messageUuid = convertLocalIndexMessageUuidSortKey(message.getBinary(LOCAL_INDEX_MESSAGE_UUID_KEY_SORT)); final UUID messageUuid = convertLocalIndexMessageUuidSortKey(message.get(LOCAL_INDEX_MESSAGE_UUID_KEY_SORT).b().asByteArray());
final int type = message.getInt(KEY_TYPE); final int type = AttributeValues.getInt(message, KEY_TYPE, 0);
final String relay = message.getString(KEY_RELAY); final String relay = AttributeValues.getString(message, KEY_RELAY, null);
final long timestamp = message.getLong(KEY_TIMESTAMP); final long timestamp = AttributeValues.getLong(message, KEY_TIMESTAMP, 0L);
final String source = message.getString(KEY_SOURCE); final String source = AttributeValues.getString(message, KEY_SOURCE, null);
final UUID sourceUuid = message.hasAttribute(KEY_SOURCE_UUID) ? convertUuidFromBytes(message.getBinary(KEY_SOURCE_UUID), "message source uuid") : null; final UUID sourceUuid = AttributeValues.getUUID(message, KEY_SOURCE_UUID, null);
final int sourceDevice = message.hasAttribute(KEY_SOURCE_DEVICE) ? message.getInt(KEY_SOURCE_DEVICE) : 0; final int sourceDevice = AttributeValues.getInt(message, KEY_SOURCE_DEVICE, 0);
final byte[] messageBytes = message.getBinary(KEY_MESSAGE); final byte[] messageBytes = AttributeValues.getByteArray(message, KEY_MESSAGE, null);
final byte[] content = message.getBinary(KEY_CONTENT); 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(-1L, false, messageUuid, type, relay, timestamp, source, sourceUuid, sourceDevice, messageBytes, content, sortKey.getServerTimestamp());
} }
private void deleteRowsMatchingQuery(byte[] partitionKey, QuerySpec querySpec) { private void deleteRowsMatchingQuery(AttributeValue partitionKey, QueryRequest querySpec) {
final Table table = getDynamoDb().getTable(tableName); writeInBatches(db().query(querySpec).items(), (itemBatch) -> deleteItems(partitionKey, itemBatch));
writeInBatches(table.query(querySpec), (itemBatch) -> deleteItems(partitionKey, itemBatch));
} }
private void deleteItems(byte[] partitionKey, List<Item> items) { private void deleteItems(AttributeValue partitionKey, List<Map<String, AttributeValue>> items) {
final TableWriteItems tableWriteItems = new TableWriteItems(tableName); List<WriteRequest> deletes = items.stream()
items.stream().map(item -> new PrimaryKey(KEY_PARTITION, partitionKey, KEY_SORT, item.getBinary(KEY_SORT))).forEach(tableWriteItems::addPrimaryKeyToDelete); .map(item -> WriteRequest.builder()
executeTableWriteItemsUntilComplete(tableWriteItems); .deleteRequest(DeleteRequest.builder().key(Map.of(
KEY_PARTITION, partitionKey,
KEY_SORT, item.get(KEY_SORT))).build())
.build())
.collect(Collectors.toList());
executeTableWriteItemsUntilComplete(Map.of(tableName, deletes));
} }
private long getTtlForMessage(MessageProtos.Envelope message) { private long getTtlForMessage(MessageProtos.Envelope message) {
return message.getServerTimestamp() / 1000 + timeToLive.getSeconds(); return message.getServerTimestamp() / 1000 + timeToLive.getSeconds();
} }
private static byte[] convertPartitionKey(final UUID destinationAccountUuid) { private static AttributeValue convertPartitionKey(final UUID destinationAccountUuid) {
return UUIDUtil.toBytes(destinationAccountUuid); return AttributeValues.fromUUID(destinationAccountUuid);
} }
private static byte[] convertSortKey(final long destinationDeviceId, final long serverTimestamp, final UUID messageUuid) { private static AttributeValue convertSortKey(final long destinationDeviceId, final long serverTimestamp, final UUID messageUuid) {
ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[32]); ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[32]);
byteBuffer.putLong(destinationDeviceId); byteBuffer.putLong(destinationDeviceId);
byteBuffer.putLong(serverTimestamp); byteBuffer.putLong(serverTimestamp);
byteBuffer.putLong(messageUuid.getMostSignificantBits()); byteBuffer.putLong(messageUuid.getMostSignificantBits());
byteBuffer.putLong(messageUuid.getLeastSignificantBits()); byteBuffer.putLong(messageUuid.getLeastSignificantBits());
return byteBuffer.array(); return AttributeValues.fromByteBuffer(byteBuffer.flip());
} }
private static byte[] convertDestinationDeviceIdToSortKeyPrefix(final long destinationDeviceId) { private static AttributeValue convertDestinationDeviceIdToSortKeyPrefix(final long destinationDeviceId) {
ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[8]); ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[8]);
byteBuffer.putLong(destinationDeviceId); byteBuffer.putLong(destinationDeviceId);
return byteBuffer.array(); return AttributeValues.fromByteBuffer(byteBuffer.flip());
} }
private static SortKey convertSortKey(final byte[] bytes) { private static SortKey convertSortKey(final byte[] bytes) {
@@ -273,8 +305,8 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
return new SortKey(destinationDeviceId, serverTimestamp, new UUID(mostSigBits, leastSigBits)); return new SortKey(destinationDeviceId, serverTimestamp, new UUID(mostSigBits, leastSigBits));
} }
private static byte[] convertLocalIndexMessageUuidSortKey(final UUID messageUuid) { private static AttributeValue convertLocalIndexMessageUuidSortKey(final UUID messageUuid) {
return UUIDUtil.toBytes(messageUuid); return AttributeValues.fromUUID(messageUuid);
} }
private static UUID convertLocalIndexMessageUuidSortKey(final byte[] bytes) { private static UUID convertLocalIndexMessageUuidSortKey(final byte[] bytes) {

View File

@@ -1,42 +1,52 @@
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.PrimaryKey;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.dynamodbv2.document.TableWriteItems;
import com.amazonaws.services.dynamodbv2.document.spec.ScanSpec;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import org.whispersystems.textsecuregcm.util.UUIDUtil; import java.util.stream.Collectors;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.DeleteRequest;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
import software.amazon.awssdk.services.dynamodb.model.ScanResponse;
import software.amazon.awssdk.services.dynamodb.model.WriteRequest;
public class MigrationDeletedAccounts extends AbstractDynamoDbStore { public class MigrationDeletedAccounts extends AbstractDynamoDbStore {
private final Table table; private final String tableName;
static final String KEY_UUID = "U"; static final String KEY_UUID = "U";
public MigrationDeletedAccounts(DynamoDB dynamoDb, String tableName) { public MigrationDeletedAccounts(DynamoDbClient dynamoDb, String tableName) {
super(dynamoDb); super(dynamoDb);
this.tableName = tableName;
table = dynamoDb.getTable(tableName);
} }
public void put(UUID uuid) { public void put(UUID uuid) {
table.putItem(new Item() db().putItem(PutItemRequest.builder()
.withPrimaryKey(primaryKey(uuid))); .tableName(tableName)
.item(primaryKey(uuid))
.build());
} }
public List<UUID> getRecentlyDeletedUuids() { public List<UUID> getRecentlyDeletedUuids() {
final List<UUID> uuids = new ArrayList<>(); final List<UUID> uuids = new ArrayList<>();
Optional<ScanResponse> firstPage = db().scanPaginator(ScanRequest.builder()
.tableName(tableName)
.build()).stream().findAny(); // get the first available response
for (Item item : table.scan(new ScanSpec()).firstPage()) { if (firstPage.isPresent()) {
// only process one page each time. If we have a significant backlog at the end of the migration for (Map<String, AttributeValue> item : firstPage.get().items()) {
// we can handle it separately // only process one page each time. If we have a significant backlog at the end of the migration
uuids.add(UUIDUtil.fromByteBuffer(item.getByteBuffer(KEY_UUID))); // we can handle it separately
uuids.add(AttributeValues.getUUID(item, KEY_UUID, null));
}
} }
return uuids; return uuids;
@@ -45,20 +55,17 @@ public class MigrationDeletedAccounts extends AbstractDynamoDbStore {
public void delete(List<UUID> uuids) { public void delete(List<UUID> uuids) {
writeInBatches(uuids, (batch) -> { writeInBatches(uuids, (batch) -> {
List<WriteRequest> deletes = batch.stream().map((uuid) -> WriteRequest.builder().deleteRequest(DeleteRequest.builder()
.key(primaryKey(uuid))
.build()).build()).collect(Collectors.toList());
final TableWriteItems deleteItems = new TableWriteItems(table.getTableName()); executeTableWriteItemsUntilComplete(Map.of(tableName, deletes));
for (UUID uuid : batch) {
deleteItems.addPrimaryKeyToDelete(primaryKey(uuid));
}
executeTableWriteItemsUntilComplete(deleteItems);
}); });
} }
@VisibleForTesting @VisibleForTesting
public static PrimaryKey primaryKey(UUID uuid) { public static Map<String, AttributeValue> primaryKey(UUID uuid) {
return new PrimaryKey(KEY_UUID, UUIDUtil.toBytes(uuid)); return Map.of(KEY_UUID, AttributeValues.fromUUID(uuid));
} }
} }

View File

@@ -1,43 +1,47 @@
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.Page;
import com.amazonaws.services.dynamodbv2.document.PrimaryKey;
import com.amazonaws.services.dynamodbv2.document.ScanOutcome;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.dynamodbv2.document.spec.ScanSpec;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import org.whispersystems.textsecuregcm.util.UUIDUtil; import java.util.stream.Collectors;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.DeleteRequest;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
import software.amazon.awssdk.services.dynamodb.model.ScanResponse;
import software.amazon.awssdk.services.dynamodb.model.WriteRequest;
public class MigrationRetryAccounts extends AbstractDynamoDbStore { public class MigrationRetryAccounts extends AbstractDynamoDbStore {
private final Table table; private final String tableName;
static final String KEY_UUID = "U"; static final String KEY_UUID = "U";
public MigrationRetryAccounts(DynamoDB dynamoDb, String tableName) { public MigrationRetryAccounts(DynamoDbClient dynamoDb, String tableName) {
super(dynamoDb); super(dynamoDb);
table = dynamoDb.getTable(tableName); this.tableName = tableName;
} }
public void put(UUID uuid) { public void put(UUID uuid) {
table.putItem(new Item() db().putItem(PutItemRequest.builder()
.withPrimaryKey(primaryKey(uuid))); .tableName(tableName)
.item(primaryKey(uuid))
.build());
} }
public List<UUID> getUuids(int max) { public List<UUID> getUuids(int max) {
final List<UUID> uuids = new ArrayList<>(); final List<UUID> uuids = new ArrayList<>();
for (Page<Item, ScanOutcome> page : table.scan(new ScanSpec()).pages()) { for (ScanResponse response : db().scanPaginator(ScanRequest.builder().tableName(tableName).build())) {
for (Item item : page) { for (Map<String, AttributeValue> item : response.items()) {
uuids.add(UUIDUtil.fromByteBuffer(item.getByteBuffer(KEY_UUID))); uuids.add(AttributeValues.getUUID(item, KEY_UUID, null));
if (uuids.size() >= max) { if (uuids.size() >= max) {
break; break;
@@ -53,8 +57,20 @@ public class MigrationRetryAccounts extends AbstractDynamoDbStore {
} }
@VisibleForTesting @VisibleForTesting
public static PrimaryKey primaryKey(UUID uuid) { public static Map<String, AttributeValue> primaryKey(UUID uuid) {
return new PrimaryKey(KEY_UUID, UUIDUtil.toBytes(uuid)); return Map.of(KEY_UUID, AttributeValues.fromUUID(uuid));
} }
public void delete(final List<UUID> uuidsToDelete) {
writeInBatches(uuidsToDelete, (uuids -> {
final List<WriteRequest> deletes = uuids.stream()
.map(uuid -> WriteRequest.builder().deleteRequest(
DeleteRequest.builder().key(Map.of(KEY_UUID, AttributeValues.fromUUID(uuid))).build()).build())
.collect(Collectors.toList());
executeTableWriteItemsUntilComplete(Map.of(tableName, deletes));
}));
}
} }

View File

@@ -0,0 +1,88 @@
/*
* 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 io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
public class MigrationRetryAccountsTableCrawler extends ManagedPeriodicWork {
private static final Logger logger = LoggerFactory.getLogger(MigrationRetryAccountsTableCrawler.class);
private static final Duration WORKER_TTL = Duration.ofMinutes(2);
private static final Duration RUN_INTERVAL = Duration.ofMinutes(15);
private static final String ACTIVE_WORKER_KEY = "migration_retry_accounts_crawler_cache_active_worker";
private static final int MAX_BATCH_SIZE = 5_000;
private static final Counter MIGRATED_COUNTER = Metrics.counter(name(MigrationRetryAccountsTableCrawler.class, "migrated"));
private static final Counter ERROR_COUNTER = Metrics.counter(name(MigrationRetryAccountsTableCrawler.class, "error"));
private static final Counter TOTAL_COUNTER = Metrics.counter(name(MigrationRetryAccountsTableCrawler.class, "total"));
private final MigrationRetryAccounts retryAccounts;
private final AccountsManager accountsManager;
private final AccountsDynamoDb accountsDynamoDb;
public MigrationRetryAccountsTableCrawler(
final MigrationRetryAccounts retryAccounts,
final AccountsManager accountsManager,
final AccountsDynamoDb accountsDynamoDb,
final FaultTolerantRedisCluster cluster,
final ScheduledExecutorService executorService) throws IOException {
super(new ManagedPeriodicWorkLock(ACTIVE_WORKER_KEY, cluster), WORKER_TTL, RUN_INTERVAL, executorService);
this.retryAccounts = retryAccounts;
this.accountsManager = accountsManager;
this.accountsDynamoDb = accountsDynamoDb;
}
@Override
public void doPeriodicWork() {
final List<UUID> uuids = this.retryAccounts.getUuids(MAX_BATCH_SIZE);
final List<UUID> processedUuids = new ArrayList<>(uuids.size());
try {
for (UUID uuid : uuids) {
try {
final Optional<Account> maybeDynamoAccount = accountsDynamoDb.get(uuid);
if (maybeDynamoAccount.isEmpty()) {
accountsManager.get(uuid).ifPresent(account -> {
accountsDynamoDb.migrate(account);
MIGRATED_COUNTER.increment();
});
}
processedUuids.add(uuid);
TOTAL_COUNTER.increment();
} catch (final Exception e) {
ERROR_COUNTER.increment();
logger.warn("Failed to migrate account");
}
}
} finally {
this.retryAccounts.delete(processedUuids);
}
}
}

View File

@@ -0,0 +1,10 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
public class OptimisticLockRetryLimitExceededException extends RuntimeException {
}

View File

@@ -1,86 +0,0 @@
/*
* Copyright 2013-2020 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 java.util.Optional;
import com.google.common.annotations.VisibleForTesting;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.storage.mappers.StoredVerificationCodeRowMapper;
import org.whispersystems.textsecuregcm.util.Constants;
public class PendingAccounts {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Timer insertTimer = metricRegistry.timer(name(PendingAccounts.class, "insert" ));
private final Timer getCodeForNumberTimer = metricRegistry.timer(name(PendingAccounts.class, "getCodeForNumber"));
private final Timer removeTimer = metricRegistry.timer(name(PendingAccounts.class, "remove" ));
private final Timer vacuumTimer = metricRegistry.timer(name(PendingAccounts.class, "vacuum" ));
private final FaultTolerantDatabase database;
public PendingAccounts(FaultTolerantDatabase database) {
this.database = database;
this.database.getDatabase().registerRowMapper(new StoredVerificationCodeRowMapper());
}
@VisibleForTesting
public void insert (String number, String verificationCode, long timestamp, String pushCode) {
insert(number, verificationCode, timestamp, pushCode, null);
}
public void insert(String number, String verificationCode, long timestamp, String pushCode, String twilioVerificationSid) {
database.use(jdbi -> jdbi.useHandle(handle -> {
try (Timer.Context ignored = insertTimer.time()) {
handle.createUpdate("INSERT INTO pending_accounts (number, verification_code, timestamp, push_code, twilio_verification_sid) " +
"VALUES (:number, :verification_code, :timestamp, :push_code, :twilio_verification_sid) " +
"ON CONFLICT(number) DO UPDATE " +
"SET verification_code = EXCLUDED.verification_code, timestamp = EXCLUDED.timestamp, push_code = EXCLUDED.push_code, twilio_verification_sid = EXCLUDED.twilio_verification_sid")
.bind("verification_code", verificationCode)
.bind("timestamp", timestamp)
.bind("number", number)
.bind("push_code", pushCode)
.bind("twilio_verification_sid", twilioVerificationSid)
.execute();
}
}));
}
public Optional<StoredVerificationCode> getCodeForNumber(String number) {
return database.with(jdbi ->jdbi.withHandle(handle -> {
try (Timer.Context ignored = getCodeForNumberTimer.time()) {
return handle.createQuery("SELECT verification_code, timestamp, push_code, twilio_verification_sid FROM pending_accounts WHERE number = :number")
.bind("number", number)
.mapTo(StoredVerificationCode.class)
.findFirst();
}
}));
}
public void remove(String number) {
database.use(jdbi-> jdbi.useHandle(handle -> {
try (Timer.Context ignored = removeTimer.time()) {
handle.createUpdate("DELETE FROM pending_accounts WHERE number = :number")
.bind("number", number)
.execute();
}
}));
}
public void vacuum() {
database.use(jdbi -> jdbi.useHandle(handle -> {
try (Timer.Context ignored = vacuumTimer.time()) {
handle.execute("VACUUM pending_accounts");
}
}));
}
}

View File

@@ -1,82 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import java.io.IOException;
import java.util.Optional;
public class PendingAccountsManager {
private final Logger logger = LoggerFactory.getLogger(PendingAccountsManager.class);
private static final String CACHE_PREFIX = "pending_account2::";
private final PendingAccounts pendingAccounts;
private final FaultTolerantRedisCluster cacheCluster;
private final ObjectMapper mapper;
public PendingAccountsManager(PendingAccounts pendingAccounts, FaultTolerantRedisCluster cacheCluster)
{
this.pendingAccounts = pendingAccounts;
this.cacheCluster = cacheCluster;
this.mapper = SystemMapper.getMapper();
}
public void store(String number, StoredVerificationCode code) {
memcacheSet(number, code);
pendingAccounts.insert(number, code.getCode(), code.getTimestamp(), code.getPushCode(),
code.getTwilioVerificationSid().orElse(null));
}
public void remove(String number) {
memcacheDelete(number);
pendingAccounts.remove(number);
}
public Optional<StoredVerificationCode> getCodeForNumber(String number) {
Optional<StoredVerificationCode> code = memcacheGet(number);
if (!code.isPresent()) {
code = pendingAccounts.getCodeForNumber(number);
code.ifPresent(storedVerificationCode -> memcacheSet(number, storedVerificationCode));
}
return code;
}
private void memcacheSet(String number, StoredVerificationCode code) {
try {
final String verificationCodeJson = mapper.writeValueAsString(code);
cacheCluster.useCluster(connection -> connection.sync().set(CACHE_PREFIX + number, verificationCodeJson));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
}
private Optional<StoredVerificationCode> memcacheGet(String number) {
try {
final String json = cacheCluster.withCluster(connection -> connection.sync().get(CACHE_PREFIX + number));
if (json == null) return Optional.empty();
else return Optional.of(mapper.readValue(json, StoredVerificationCode.class));
} catch (IOException e) {
logger.warn("Error deserializing value...", e);
return Optional.empty();
}
}
private void memcacheDelete(String number) {
cacheCluster.useCluster(connection -> connection.sync().del(CACHE_PREFIX + number));
}
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2013-2020 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 java.util.Optional;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.storage.mappers.StoredVerificationCodeRowMapper;
import org.whispersystems.textsecuregcm.util.Constants;
public class PendingDevices {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Timer insertTimer = metricRegistry.timer(name(PendingDevices.class, "insert" ));
private final Timer getCodeForNumberTimer = metricRegistry.timer(name(PendingDevices.class, "getcodeForNumber"));
private final Timer removeTimer = metricRegistry.timer(name(PendingDevices.class, "remove" ));
private final FaultTolerantDatabase database;
public PendingDevices(FaultTolerantDatabase database) {
this.database = database;
this.database.getDatabase().registerRowMapper(new StoredVerificationCodeRowMapper());
}
public void insert(String number, String verificationCode, long timestamp) {
database.use(jdbi ->jdbi.useHandle(handle -> {
try (Timer.Context timer = insertTimer.time()) {
handle.createUpdate("WITH upsert AS (UPDATE pending_devices SET verification_code = :verification_code, timestamp = :timestamp WHERE number = :number RETURNING *) " +
"INSERT INTO pending_devices (number, verification_code, timestamp) SELECT :number, :verification_code, :timestamp WHERE NOT EXISTS (SELECT * FROM upsert)")
.bind("number", number)
.bind("verification_code", verificationCode)
.bind("timestamp", timestamp)
.execute();
}
}));
}
public Optional<StoredVerificationCode> getCodeForNumber(String number) {
return database.with(jdbi -> jdbi.withHandle(handle -> {
try (Timer.Context timer = getCodeForNumberTimer.time()) {
return handle.createQuery("SELECT verification_code, timestamp, NULL as push_code, NULL as twilio_verification_sid FROM pending_devices WHERE number = :number")
.bind("number", number)
.mapTo(StoredVerificationCode.class)
.findFirst();
}
}));
}
public void remove(String number) {
database.use(jdbi -> jdbi.useHandle(handle -> {
try (Timer.Context timer = removeTimer.time()) {
handle.createUpdate("DELETE FROM pending_devices WHERE number = :number")
.bind("number", number)
.execute();
}
}));
}
}

View File

@@ -1,81 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import java.io.IOException;
import java.util.Optional;
public class PendingDevicesManager {
private final Logger logger = LoggerFactory.getLogger(PendingDevicesManager.class);
private static final String CACHE_PREFIX = "pending_devices2::";
private final PendingDevices pendingDevices;
private final FaultTolerantRedisCluster cacheCluster;
private final ObjectMapper mapper;
public PendingDevicesManager(PendingDevices pendingDevices, FaultTolerantRedisCluster cacheCluster) {
this.pendingDevices = pendingDevices;
this.cacheCluster = cacheCluster;
this.mapper = SystemMapper.getMapper();
}
public void store(String number, StoredVerificationCode code) {
memcacheSet(number, code);
pendingDevices.insert(number, code.getCode(), code.getTimestamp());
}
public void remove(String number) {
memcacheDelete(number);
pendingDevices.remove(number);
}
public Optional<StoredVerificationCode> getCodeForNumber(String number) {
Optional<StoredVerificationCode> code = memcacheGet(number);
if (!code.isPresent()) {
code = pendingDevices.getCodeForNumber(number);
code.ifPresent(storedVerificationCode -> memcacheSet(number, storedVerificationCode));
}
return code;
}
private void memcacheSet(String number, StoredVerificationCode code) {
try {
final String verificationCodeJson = mapper.writeValueAsString(code);
cacheCluster.useCluster(connection -> connection.sync().set(CACHE_PREFIX + number, verificationCodeJson));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
}
private Optional<StoredVerificationCode> memcacheGet(String number) {
try {
final String json = cacheCluster.withCluster(connection -> connection.sync().get(CACHE_PREFIX + number));
if (json == null) return Optional.empty();
else return Optional.of(mapper.readValue(json, StoredVerificationCode.class));
} catch (IOException e) {
logger.warn("Could not parse pending device stored verification json");
return Optional.empty();
}
}
private void memcacheDelete(String number) {
cacheCluster.useCluster(connection -> connection.sync().del(CACHE_PREFIX + number));
}
}

View File

@@ -1,666 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: PubSubMessage.proto
package org.whispersystems.textsecuregcm.storage;
public final class PubSubProtos {
private PubSubProtos() {}
public static void registerAllExtensions(
com.google.protobuf.ExtensionRegistry registry) {
}
public interface PubSubMessageOrBuilder
extends com.google.protobuf.MessageOrBuilder {
// optional .textsecure.PubSubMessage.Type type = 1;
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
boolean hasType();
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type getType();
// optional bytes content = 2;
/**
* <code>optional bytes content = 2;</code>
*/
boolean hasContent();
/**
* <code>optional bytes content = 2;</code>
*/
com.google.protobuf.ByteString getContent();
}
/**
* Protobuf type {@code textsecure.PubSubMessage}
*/
public static final class PubSubMessage extends
com.google.protobuf.GeneratedMessage
implements PubSubMessageOrBuilder {
// Use PubSubMessage.newBuilder() to construct.
private PubSubMessage(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
super(builder);
this.unknownFields = builder.getUnknownFields();
}
private PubSubMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
private static final PubSubMessage defaultInstance;
public static PubSubMessage getDefaultInstance() {
return defaultInstance;
}
public PubSubMessage getDefaultInstanceForType() {
return defaultInstance;
}
private final com.google.protobuf.UnknownFieldSet unknownFields;
@java.lang.Override
public final com.google.protobuf.UnknownFieldSet
getUnknownFields() {
return this.unknownFields;
}
private PubSubMessage(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
initFields();
int mutable_bitField0_ = 0;
com.google.protobuf.UnknownFieldSet.Builder unknownFields =
com.google.protobuf.UnknownFieldSet.newBuilder();
try {
boolean done = false;
while (!done) {
int tag = input.readTag();
switch (tag) {
case 0:
done = true;
break;
default: {
if (!parseUnknownField(input, unknownFields,
extensionRegistry, tag)) {
done = true;
}
break;
}
case 8: {
int rawValue = input.readEnum();
org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type value = org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type.valueOf(rawValue);
if (value == null) {
unknownFields.mergeVarintField(1, rawValue);
} else {
bitField0_ |= 0x00000001;
type_ = value;
}
break;
}
case 18: {
bitField0_ |= 0x00000002;
content_ = input.readBytes();
break;
}
}
}
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
throw e.setUnfinishedMessage(this);
} catch (java.io.IOException e) {
throw new com.google.protobuf.InvalidProtocolBufferException(
e.getMessage()).setUnfinishedMessage(this);
} finally {
this.unknownFields = unknownFields.build();
makeExtensionsImmutable();
}
}
public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() {
return org.whispersystems.textsecuregcm.storage.PubSubProtos.internal_static_textsecure_PubSubMessage_descriptor;
}
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
internalGetFieldAccessorTable() {
return org.whispersystems.textsecuregcm.storage.PubSubProtos.internal_static_textsecure_PubSubMessage_fieldAccessorTable
.ensureFieldAccessorsInitialized(
org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.class, org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Builder.class);
}
public static com.google.protobuf.Parser<PubSubMessage> PARSER =
new com.google.protobuf.AbstractParser<PubSubMessage>() {
public PubSubMessage parsePartialFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return new PubSubMessage(input, extensionRegistry);
}
};
@java.lang.Override
public com.google.protobuf.Parser<PubSubMessage> getParserForType() {
return PARSER;
}
/**
* Protobuf enum {@code textsecure.PubSubMessage.Type}
*/
public enum Type
implements com.google.protobuf.ProtocolMessageEnum {
/**
* <code>UNKNOWN = 0;</code>
*/
UNKNOWN(0, 0),
/**
* <code>QUERY_DB = 1;</code>
*/
QUERY_DB(1, 1),
/**
* <code>DELIVER = 2;</code>
*/
DELIVER(2, 2),
/**
* <code>KEEPALIVE = 3;</code>
*/
KEEPALIVE(3, 3),
/**
* <code>CLOSE = 4;</code>
*/
CLOSE(4, 4),
/**
* <code>CONNECTED = 5;</code>
*/
CONNECTED(5, 5),
;
/**
* <code>UNKNOWN = 0;</code>
*/
public static final int UNKNOWN_VALUE = 0;
/**
* <code>QUERY_DB = 1;</code>
*/
public static final int QUERY_DB_VALUE = 1;
/**
* <code>DELIVER = 2;</code>
*/
public static final int DELIVER_VALUE = 2;
/**
* <code>KEEPALIVE = 3;</code>
*/
public static final int KEEPALIVE_VALUE = 3;
/**
* <code>CLOSE = 4;</code>
*/
public static final int CLOSE_VALUE = 4;
/**
* <code>CONNECTED = 5;</code>
*/
public static final int CONNECTED_VALUE = 5;
public final int getNumber() { return value; }
public static Type valueOf(int value) {
switch (value) {
case 0: return UNKNOWN;
case 1: return QUERY_DB;
case 2: return DELIVER;
case 3: return KEEPALIVE;
case 4: return CLOSE;
case 5: return CONNECTED;
default: return null;
}
}
public static com.google.protobuf.Internal.EnumLiteMap<Type>
internalGetValueMap() {
return internalValueMap;
}
private static com.google.protobuf.Internal.EnumLiteMap<Type>
internalValueMap =
new com.google.protobuf.Internal.EnumLiteMap<Type>() {
public Type findValueByNumber(int number) {
return Type.valueOf(number);
}
};
public final com.google.protobuf.Descriptors.EnumValueDescriptor
getValueDescriptor() {
return getDescriptor().getValues().get(index);
}
public final com.google.protobuf.Descriptors.EnumDescriptor
getDescriptorForType() {
return getDescriptor();
}
public static final com.google.protobuf.Descriptors.EnumDescriptor
getDescriptor() {
return org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.getDescriptor().getEnumTypes().get(0);
}
private static final Type[] VALUES = values();
public static Type valueOf(
com.google.protobuf.Descriptors.EnumValueDescriptor desc) {
if (desc.getType() != getDescriptor()) {
throw new java.lang.IllegalArgumentException(
"EnumValueDescriptor is not for this type.");
}
return VALUES[desc.getIndex()];
}
private final int index;
private final int value;
private Type(int index, int value) {
this.index = index;
this.value = value;
}
// @@protoc_insertion_point(enum_scope:textsecure.PubSubMessage.Type)
}
private int bitField0_;
// optional .textsecure.PubSubMessage.Type type = 1;
public static final int TYPE_FIELD_NUMBER = 1;
private org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type type_;
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
public boolean hasType() {
return ((bitField0_ & 0x00000001) == 0x00000001);
}
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
public org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type getType() {
return type_;
}
// optional bytes content = 2;
public static final int CONTENT_FIELD_NUMBER = 2;
private com.google.protobuf.ByteString content_;
/**
* <code>optional bytes content = 2;</code>
*/
public boolean hasContent() {
return ((bitField0_ & 0x00000002) == 0x00000002);
}
/**
* <code>optional bytes content = 2;</code>
*/
public com.google.protobuf.ByteString getContent() {
return content_;
}
private void initFields() {
type_ = org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type.UNKNOWN;
content_ = com.google.protobuf.ByteString.EMPTY;
}
private byte memoizedIsInitialized = -1;
public final boolean isInitialized() {
byte isInitialized = memoizedIsInitialized;
if (isInitialized != -1) return isInitialized == 1;
memoizedIsInitialized = 1;
return true;
}
public void writeTo(com.google.protobuf.CodedOutputStream output)
throws java.io.IOException {
getSerializedSize();
if (((bitField0_ & 0x00000001) == 0x00000001)) {
output.writeEnum(1, type_.getNumber());
}
if (((bitField0_ & 0x00000002) == 0x00000002)) {
output.writeBytes(2, content_);
}
getUnknownFields().writeTo(output);
}
private int memoizedSerializedSize = -1;
public int getSerializedSize() {
int size = memoizedSerializedSize;
if (size != -1) return size;
size = 0;
if (((bitField0_ & 0x00000001) == 0x00000001)) {
size += com.google.protobuf.CodedOutputStream
.computeEnumSize(1, type_.getNumber());
}
if (((bitField0_ & 0x00000002) == 0x00000002)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(2, content_);
}
size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size;
return size;
}
private static final long serialVersionUID = 0L;
@java.lang.Override
protected java.lang.Object writeReplace()
throws java.io.ObjectStreamException {
return super.writeReplace();
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(
com.google.protobuf.ByteString data)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(
com.google.protobuf.ByteString data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(byte[] data)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(
byte[] data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(java.io.InputStream input)
throws java.io.IOException {
return PARSER.parseFrom(input);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(
java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return PARSER.parseFrom(input, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseDelimitedFrom(java.io.InputStream input)
throws java.io.IOException {
return PARSER.parseDelimitedFrom(input);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseDelimitedFrom(
java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return PARSER.parseDelimitedFrom(input, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(
com.google.protobuf.CodedInputStream input)
throws java.io.IOException {
return PARSER.parseFrom(input);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return PARSER.parseFrom(input, extensionRegistry);
}
public static Builder newBuilder() { return Builder.create(); }
public Builder newBuilderForType() { return newBuilder(); }
public static Builder newBuilder(org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage prototype) {
return newBuilder().mergeFrom(prototype);
}
public Builder toBuilder() { return newBuilder(this); }
@java.lang.Override
protected Builder newBuilderForType(
com.google.protobuf.GeneratedMessage.BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
/**
* Protobuf type {@code textsecure.PubSubMessage}
*/
public static final class Builder extends
com.google.protobuf.GeneratedMessage.Builder<Builder>
implements org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessageOrBuilder {
public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() {
return org.whispersystems.textsecuregcm.storage.PubSubProtos.internal_static_textsecure_PubSubMessage_descriptor;
}
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
internalGetFieldAccessorTable() {
return org.whispersystems.textsecuregcm.storage.PubSubProtos.internal_static_textsecure_PubSubMessage_fieldAccessorTable
.ensureFieldAccessorsInitialized(
org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.class, org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Builder.class);
}
// Construct using org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.newBuilder()
private Builder() {
maybeForceBuilderInitialization();
}
private Builder(
com.google.protobuf.GeneratedMessage.BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
private void maybeForceBuilderInitialization() {
if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
}
}
private static Builder create() {
return new Builder();
}
public Builder clear() {
super.clear();
type_ = org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type.UNKNOWN;
bitField0_ = (bitField0_ & ~0x00000001);
content_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000002);
return this;
}
public Builder clone() {
return create().mergeFrom(buildPartial());
}
public com.google.protobuf.Descriptors.Descriptor
getDescriptorForType() {
return org.whispersystems.textsecuregcm.storage.PubSubProtos.internal_static_textsecure_PubSubMessage_descriptor;
}
public org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage getDefaultInstanceForType() {
return org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.getDefaultInstance();
}
public org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage build() {
org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage result = buildPartial();
if (!result.isInitialized()) {
throw newUninitializedMessageException(result);
}
return result;
}
public org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage buildPartial() {
org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage result = new org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage(this);
int from_bitField0_ = bitField0_;
int to_bitField0_ = 0;
if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
to_bitField0_ |= 0x00000001;
}
result.type_ = type_;
if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
to_bitField0_ |= 0x00000002;
}
result.content_ = content_;
result.bitField0_ = to_bitField0_;
onBuilt();
return result;
}
public Builder mergeFrom(com.google.protobuf.Message other) {
if (other instanceof org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage) {
return mergeFrom((org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage)other);
} else {
super.mergeFrom(other);
return this;
}
}
public Builder mergeFrom(org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage other) {
if (other == org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.getDefaultInstance()) return this;
if (other.hasType()) {
setType(other.getType());
}
if (other.hasContent()) {
setContent(other.getContent());
}
this.mergeUnknownFields(other.getUnknownFields());
return this;
}
public final boolean isInitialized() {
return true;
}
public Builder mergeFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parsedMessage = null;
try {
parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
parsedMessage = (org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage) e.getUnfinishedMessage();
throw e;
} finally {
if (parsedMessage != null) {
mergeFrom(parsedMessage);
}
}
return this;
}
private int bitField0_;
// optional .textsecure.PubSubMessage.Type type = 1;
private org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type type_ = org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type.UNKNOWN;
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
public boolean hasType() {
return ((bitField0_ & 0x00000001) == 0x00000001);
}
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
public org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type getType() {
return type_;
}
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
public Builder setType(org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000001;
type_ = value;
onChanged();
return this;
}
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
public Builder clearType() {
bitField0_ = (bitField0_ & ~0x00000001);
type_ = org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type.UNKNOWN;
onChanged();
return this;
}
// optional bytes content = 2;
private com.google.protobuf.ByteString content_ = com.google.protobuf.ByteString.EMPTY;
/**
* <code>optional bytes content = 2;</code>
*/
public boolean hasContent() {
return ((bitField0_ & 0x00000002) == 0x00000002);
}
/**
* <code>optional bytes content = 2;</code>
*/
public com.google.protobuf.ByteString getContent() {
return content_;
}
/**
* <code>optional bytes content = 2;</code>
*/
public Builder setContent(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000002;
content_ = value;
onChanged();
return this;
}
/**
* <code>optional bytes content = 2;</code>
*/
public Builder clearContent() {
bitField0_ = (bitField0_ & ~0x00000002);
content_ = getDefaultInstance().getContent();
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:textsecure.PubSubMessage)
}
static {
defaultInstance = new PubSubMessage(true);
defaultInstance.initFields();
}
// @@protoc_insertion_point(class_scope:textsecure.PubSubMessage)
}
private static com.google.protobuf.Descriptors.Descriptor
internal_static_textsecure_PubSubMessage_descriptor;
private static
com.google.protobuf.GeneratedMessage.FieldAccessorTable
internal_static_textsecure_PubSubMessage_fieldAccessorTable;
public static com.google.protobuf.Descriptors.FileDescriptor
getDescriptor() {
return descriptor;
}
private static com.google.protobuf.Descriptors.FileDescriptor
descriptor;
static {
java.lang.String[] descriptorData = {
"\n\023PubSubMessage.proto\022\ntextsecure\"\247\001\n\rPu" +
"bSubMessage\022,\n\004type\030\001 \001(\0162\036.textsecure.P" +
"ubSubMessage.Type\022\017\n\007content\030\002 \001(\014\"W\n\004Ty" +
"pe\022\013\n\007UNKNOWN\020\000\022\014\n\010QUERY_DB\020\001\022\013\n\007DELIVER" +
"\020\002\022\r\n\tKEEPALIVE\020\003\022\t\n\005CLOSE\020\004\022\r\n\tCONNECTE" +
"D\020\005B8\n(org.whispersystems.textsecuregcm." +
"storageB\014PubSubProtos"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
public com.google.protobuf.ExtensionRegistry assignDescriptors(
com.google.protobuf.Descriptors.FileDescriptor root) {
descriptor = root;
internal_static_textsecure_PubSubMessage_descriptor =
getDescriptor().getMessageTypes().get(0);
internal_static_textsecure_PubSubMessage_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_textsecure_PubSubMessage_descriptor,
new java.lang.String[] { "Type", "Content", });
return null;
}
};
com.google.protobuf.Descriptors.FileDescriptor
.internalBuildGeneratedFileFrom(descriptorData,
new com.google.protobuf.Descriptors.FileDescriptor[] {
}, assigner);
}
// @@protoc_insertion_point(outer_class_scope)
}

View File

@@ -5,25 +5,23 @@
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.dynamodbv2.document.spec.DeleteItemSpec;
import com.amazonaws.services.dynamodbv2.document.spec.PutItemSpec;
import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;
import java.time.Clock; import java.time.Clock;
import java.time.Duration; import java.time.Duration;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.whispersystems.textsecuregcm.util.UUIDUtil; import org.whispersystems.textsecuregcm.util.AttributeValues;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
/** /**
* Stores push challenge tokens. Users may have at most one outstanding push challenge token at a time. * Stores push challenge tokens. Users may have at most one outstanding push challenge token at a time.
*/ */
public class PushChallengeDynamoDb extends AbstractDynamoDbStore { public class PushChallengeDynamoDb extends AbstractDynamoDbStore {
private final Table table; private final String tableName;
private final Clock clock; private final Clock clock;
static final String KEY_ACCOUNT_UUID = "U"; static final String KEY_ACCOUNT_UUID = "U";
@@ -33,15 +31,15 @@ public class PushChallengeDynamoDb extends AbstractDynamoDbStore {
private static final Map<String, String> UUID_NAME_MAP = Map.of("#uuid", KEY_ACCOUNT_UUID); private static final Map<String, String> UUID_NAME_MAP = Map.of("#uuid", KEY_ACCOUNT_UUID);
private static final Map<String, String> CHALLENGE_TOKEN_NAME_MAP = Map.of("#challenge", ATTR_CHALLENGE_TOKEN); private static final Map<String, String> CHALLENGE_TOKEN_NAME_MAP = Map.of("#challenge", ATTR_CHALLENGE_TOKEN);
public PushChallengeDynamoDb(final DynamoDB dynamoDB, final String tableName) { public PushChallengeDynamoDb(final DynamoDbClient dynamoDB, final String tableName) {
this(dynamoDB, tableName, Clock.systemUTC()); this(dynamoDB, tableName, Clock.systemUTC());
} }
@VisibleForTesting @VisibleForTesting
PushChallengeDynamoDb(final DynamoDB dynamoDB, final String tableName, final Clock clock) { PushChallengeDynamoDb(final DynamoDbClient dynamoDB, final String tableName, final Clock clock) {
super(dynamoDB); super(dynamoDB);
this.table = dynamoDB.getTable(tableName); this.tableName = tableName;
this.clock = clock; this.clock = clock;
} }
@@ -57,13 +55,15 @@ public class PushChallengeDynamoDb extends AbstractDynamoDbStore {
*/ */
public boolean add(final UUID accountUuid, final byte[] challengeToken, final Duration ttl) { public boolean add(final UUID accountUuid, final byte[] challengeToken, final Duration ttl) {
try { try {
table.putItem( new PutItemSpec() db().putItem(PutItemRequest.builder()
.withItem(new Item() .tableName(tableName)
.withBinary(KEY_ACCOUNT_UUID, UUIDUtil.toByteBuffer(accountUuid)) .item(Map.of(
.withBinary(ATTR_CHALLENGE_TOKEN, challengeToken) KEY_ACCOUNT_UUID, AttributeValues.fromUUID(accountUuid),
.withNumber(ATTR_TTL, getExpirationTimestamp(ttl))) ATTR_CHALLENGE_TOKEN, AttributeValues.fromByteArray(challengeToken),
.withConditionExpression("attribute_not_exists(#uuid)") ATTR_TTL, AttributeValues.fromLong(getExpirationTimestamp(ttl))))
.withNameMap(UUID_NAME_MAP)); .conditionExpression("attribute_not_exists(#uuid)")
.expressionAttributeNames(UUID_NAME_MAP)
.build());
return true; return true;
} catch (final ConditionalCheckFailedException e) { } catch (final ConditionalCheckFailedException e) {
return false; return false;
@@ -84,11 +84,13 @@ public class PushChallengeDynamoDb extends AbstractDynamoDbStore {
*/ */
public boolean remove(final UUID accountUuid, final byte[] challengeToken) { public boolean remove(final UUID accountUuid, final byte[] challengeToken) {
try { try {
table.deleteItem(new DeleteItemSpec() db().deleteItem(DeleteItemRequest.builder()
.withPrimaryKey(KEY_ACCOUNT_UUID, UUIDUtil.toByteBuffer(accountUuid)) .tableName(tableName)
.withConditionExpression("#challenge = :challenge") .key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(accountUuid)))
.withNameMap(CHALLENGE_TOKEN_NAME_MAP) .conditionExpression("#challenge = :challenge")
.withValueMap(Map.of(":challenge", challengeToken))); .expressionAttributeNames(CHALLENGE_TOKEN_NAME_MAP)
.expressionAttributeValues(Map.of(":challenge", AttributeValues.fromByteArray(challengeToken)))
.build());
return true; return true;
} catch (final ConditionalCheckFailedException e) { } catch (final ConditionalCheckFailedException e) {
return false; return false;

View File

@@ -5,20 +5,20 @@
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import static com.codahale.metrics.MetricRegistry.name; import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
public class PushFeedbackProcessor extends AccountDatabaseCrawlerListener { public class PushFeedbackProcessor extends AccountDatabaseCrawlerListener {
@@ -27,11 +27,9 @@ public class PushFeedbackProcessor extends AccountDatabaseCrawlerListener {
private final Meter recovered = metricRegistry.meter(name(getClass(), "unregistered", "recovered")); private final Meter recovered = metricRegistry.meter(name(getClass(), "unregistered", "recovered"));
private final AccountsManager accountsManager; private final AccountsManager accountsManager;
private final DirectoryQueue directoryQueue;
public PushFeedbackProcessor(AccountsManager accountsManager, DirectoryQueue directoryQueue) { public PushFeedbackProcessor(AccountsManager accountsManager) {
this.accountsManager = accountsManager; this.accountsManager = accountsManager;
this.directoryQueue = directoryQueue;
} }
@Override @Override
@@ -42,47 +40,58 @@ public class PushFeedbackProcessor extends AccountDatabaseCrawlerListener {
@Override @Override
protected void onCrawlChunk(Optional<UUID> fromUuid, List<Account> chunkAccounts) { protected void onCrawlChunk(Optional<UUID> fromUuid, List<Account> chunkAccounts) {
final List<Account> directoryUpdateAccounts = new ArrayList<>();
for (Account account : chunkAccounts) { for (Account account : chunkAccounts) {
boolean update = false; boolean update = false;
for (Device device : account.getDevices()) { final Set<Device> devices = account.getDevices();
if (device.getUninstalledFeedbackTimestamp() != 0 && for (Device device : devices) {
device.getUninstalledFeedbackTimestamp() + TimeUnit.DAYS.toMillis(2) <= Util.todayInMillis()) if (deviceNeedsUpdate(device)) {
{ if (deviceExpired(device)) {
if (device.getLastSeen() + TimeUnit.DAYS.toMillis(2) <= Util.todayInMillis()) {
if (!Util.isEmpty(device.getApnId())) {
if (device.getId() == 1) {
device.setUserAgent("OWI");
} else {
device.setUserAgent("OWP");
}
} else if (!Util.isEmpty(device.getGcmId())) {
device.setUserAgent("OWA");
}
device.setGcmId(null);
device.setApnId(null);
device.setVoipApnId(null);
device.setFetchesMessages(false);
expired.mark(); expired.mark();
} else { } else {
device.setUninstalledFeedbackTimestamp(0);
recovered.mark(); recovered.mark();
} }
update = true; update = true;
} }
} }
if (update) { if (update) {
accountsManager.update(account); // fetch a new version, since the chunk is shared and implicitly read-only
directoryUpdateAccounts.add(account); accountsManager.get(account.getUuid()).ifPresent(accountToUpdate -> {
accountsManager.update(accountToUpdate, a -> {
for (Device device : a.getDevices()) {
if (deviceNeedsUpdate(device)) {
if (deviceExpired(device)) {
if (!Util.isEmpty(device.getApnId())) {
if (device.getId() == 1) {
device.setUserAgent("OWI");
} else {
device.setUserAgent("OWP");
}
} else if (!Util.isEmpty(device.getGcmId())) {
device.setUserAgent("OWA");
}
device.setGcmId(null);
device.setApnId(null);
device.setVoipApnId(null);
device.setFetchesMessages(false);
} else {
device.setUninstalledFeedbackTimestamp(0);
}
}
}
});
});
} }
} }
}
if (!directoryUpdateAccounts.isEmpty()) { private boolean deviceNeedsUpdate(final Device device) {
directoryQueue.refreshRegisteredUsers(directoryUpdateAccounts); return device.getUninstalledFeedbackTimestamp() != 0 &&
} device.getUninstalledFeedbackTimestamp() + TimeUnit.DAYS.toMillis(2) <= Util.todayInMillis();
}
private boolean deviceExpired(final Device device) {
return device.getLastSeen() + TimeUnit.DAYS.toMillis(2) <= Util.todayInMillis();
} }
} }

View File

@@ -1,13 +1,14 @@
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import com.amazonaws.services.dynamodbv2.document.DeleteItemOutcome; import org.whispersystems.textsecuregcm.util.AttributeValues;
import com.amazonaws.services.dynamodbv2.document.DynamoDB; import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import com.amazonaws.services.dynamodbv2.document.Item; import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
import com.amazonaws.services.dynamodbv2.document.Table; import software.amazon.awssdk.services.dynamodb.model.DeleteItemResponse;
import com.amazonaws.services.dynamodbv2.document.spec.DeleteItemSpec; import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import com.amazonaws.services.dynamodbv2.model.ReturnValue; import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Map;
public class ReportMessageDynamoDb { public class ReportMessageDynamoDb {
@@ -16,33 +17,30 @@ public class ReportMessageDynamoDb {
static final Duration TIME_TO_LIVE = Duration.ofDays(7); static final Duration TIME_TO_LIVE = Duration.ofDays(7);
private final Table table; private final DynamoDbClient db;
private final String tableName;
public ReportMessageDynamoDb(final DynamoDB dynamoDB, final String tableName) { public ReportMessageDynamoDb(final DynamoDbClient dynamoDB, final String tableName) {
this.db = dynamoDB;
this.table = dynamoDB.getTable(tableName); this.tableName = tableName;
} }
public void store(byte[] hash) { public void store(byte[] hash) {
db.putItem(PutItemRequest.builder()
table.putItem(buildItemForHash(hash)); .tableName(tableName)
} .item(Map.of(
KEY_HASH, AttributeValues.fromByteArray(hash),
private Item buildItemForHash(byte[] hash) { ATTR_TTL, AttributeValues.fromLong(Instant.now().plus(TIME_TO_LIVE).getEpochSecond())
return new Item() ))
.withBinary(KEY_HASH, hash) .build());
.withLong(ATTR_TTL, Instant.now().plus(TIME_TO_LIVE).getEpochSecond());
} }
public boolean remove(byte[] hash) { public boolean remove(byte[] hash) {
final DeleteItemResponse deleteItemResponse = db.deleteItem(DeleteItemRequest.builder()
final DeleteItemSpec deleteItemSpec = new DeleteItemSpec() .tableName(tableName)
.withPrimaryKey(KEY_HASH, hash) .key(Map.of(KEY_HASH, AttributeValues.fromByteArray(hash)))
.withReturnValues(ReturnValue.ALL_OLD); .returnValues(ReturnValue.ALL_OLD)
.build());
final DeleteItemOutcome outcome = table.deleteItem(deleteItemSpec); return !deleteItemResponse.attributes().isEmpty();
return outcome.getItem() != null;
} }
} }

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import java.util.Optional;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
public class StoredVerificationCodeManager {
private final VerificationCodeStore verificationCodeStore;
public StoredVerificationCodeManager(final VerificationCodeStore verificationCodeStore) {
this.verificationCodeStore = verificationCodeStore;
}
public void store(String number, StoredVerificationCode code) {
verificationCodeStore.insert(number, code);
}
public void remove(String number) {
verificationCodeStore.remove(number);
}
public Optional<StoredVerificationCode> getCodeForNumber(String number) {
return verificationCodeStore.findForNumber(number);
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.core.JsonProcessingException;
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 org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
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.PutItemRequest;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import static com.codahale.metrics.MetricRegistry.name;
public class VerificationCodeStore {
private final DynamoDbClient dynamoDbClient;
private final String tableName;
private final Timer insertTimer;
private final Timer getTimer;
private final Timer removeTimer;
@VisibleForTesting
static final String KEY_E164 = "P";
private static final String ATTR_STORED_CODE = "C";
private static final String ATTR_TTL = "E";
private static final Logger log = LoggerFactory.getLogger(VerificationCodeStore.class);
public VerificationCodeStore(final DynamoDbClient dynamoDbClient, final String tableName) {
this.dynamoDbClient = dynamoDbClient;
this.tableName = tableName;
this.insertTimer = Metrics.timer(name(getClass(), "insert"), "table", tableName);
this.getTimer = Metrics.timer(name(getClass(), "get"), "table", tableName);
this.removeTimer = Metrics.timer(name(getClass(), "remove"), "table", tableName);
}
public void insert(final String number, final StoredVerificationCode verificationCode) {
insertTimer.record(() -> {
try {
dynamoDbClient.putItem(PutItemRequest.builder()
.tableName(tableName)
.item(Map.of(
KEY_E164, AttributeValues.fromString(number),
ATTR_STORED_CODE, AttributeValues.fromString(SystemMapper.getMapper().writeValueAsString(verificationCode)),
ATTR_TTL, AttributeValues.fromLong(getExpirationTimestamp(verificationCode))))
.build());
} catch (final JsonProcessingException e) {
// This should never happen when writing directly to a string except in cases of serious misconfiguration, which
// would be caught by tests.
throw new AssertionError(e);
}
});
}
private long getExpirationTimestamp(final StoredVerificationCode storedVerificationCode) {
return Instant.ofEpochMilli(storedVerificationCode.getTimestamp()).plus(StoredVerificationCode.EXPIRATION).getEpochSecond();
}
public Optional<StoredVerificationCode> findForNumber(final String number) {
return getTimer.record(() -> {
final GetItemResponse response = dynamoDbClient.getItem(GetItemRequest.builder()
.tableName(tableName)
.consistentRead(true)
.key(Map.of(KEY_E164, AttributeValues.fromString(number)))
.build());
try {
return response.hasItem()
? Optional.of(SystemMapper.getMapper().readValue(response.item().get(ATTR_STORED_CODE).s(), StoredVerificationCode.class))
: Optional.empty();
} catch (final JsonProcessingException e) {
log.error("Failed to parse stored verification code", e);
return Optional.empty();
}
});
}
public void remove(final String number) {
removeTimer.record(() -> {
dynamoDbClient.deleteItem(DeleteItemRequest.builder()
.tableName(tableName)
.key(Map.of(KEY_E164, AttributeValues.fromString(number)))
.build());
});
}
}

View File

@@ -27,6 +27,7 @@ public class AccountRowMapper implements RowMapper<Account> {
Account account = mapper.readValue(resultSet.getString(Accounts.DATA), Account.class); Account account = mapper.readValue(resultSet.getString(Accounts.DATA), Account.class);
account.setNumber(resultSet.getString(Accounts.NUMBER)); account.setNumber(resultSet.getString(Accounts.NUMBER));
account.setUuid(UUID.fromString(resultSet.getString(Accounts.UID))); account.setUuid(UUID.fromString(resultSet.getString(Accounts.UID)));
account.setVersion(resultSet.getInt(Accounts.VERSION));
return account; return account;
} catch (IOException e) { } catch (IOException e) {
throw new SQLException(e); throw new SQLException(e);

View File

@@ -1,24 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage.mappers;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import java.sql.ResultSet;
import java.sql.SQLException;
public class StoredVerificationCodeRowMapper implements RowMapper<StoredVerificationCode> {
@Override
public StoredVerificationCode map(ResultSet resultSet, StatementContext ctx) throws SQLException {
return new StoredVerificationCode(resultSet.getString("verification_code"),
resultSet.getLong("timestamp"),
resultSet.getString("push_code"),
resultSet.getString("twilio_verification_sid"));
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.util;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
/** AwsAV provides static helper methods for working with AWS AttributeValues. */
public class AttributeValues {
public static AttributeValue fromString(String value) {
return AttributeValue.builder().s(value).build();
}
public static AttributeValue fromLong(long value) {
return AttributeValue.builder().n(Long.toString(value)).build();
}
public static AttributeValue fromInt(int value) {
return AttributeValue.builder().n(Integer.toString(value)).build();
}
public static AttributeValue fromByteArray(byte[] value) {
return AttributeValues.fromSdkBytes(SdkBytes.fromByteArray(value));
}
public static AttributeValue fromByteBuffer(ByteBuffer value) {
return AttributeValues.fromSdkBytes(SdkBytes.fromByteBuffer(value));
}
public static AttributeValue fromUUID(UUID uuid) {
return AttributeValues.fromSdkBytes(SdkBytes.fromByteArrayUnsafe(UUIDUtil.toBytes(uuid)));
}
public static AttributeValue fromSdkBytes(SdkBytes value) {
return AttributeValue.builder().b(value).build();
}
private static int toInt(AttributeValue av) {
return Integer.parseInt(av.n());
}
private static long toLong(AttributeValue av) {
return Long.parseLong(av.n());
}
private static UUID toUUID(AttributeValue av) {
return UUIDUtil.fromBytes(av.b().asByteArrayUnsafe()); // We're guaranteed not to modify the byte array
}
private static byte[] toByteArray(AttributeValue av) {
return av.b().asByteArray();
}
private static String toString(AttributeValue av) {
return av.s();
}
public static Optional<AttributeValue> get(Map<String, AttributeValue> item, String key) {
return Optional.ofNullable(item.get(key));
}
public static int getInt(Map<String, AttributeValue> item, String key, int defaultValue) {
return AttributeValues.get(item, key).map(AttributeValues::toInt).orElse(defaultValue);
}
public static String getString(Map<String, AttributeValue> item, String key, String defaultValue) {
return AttributeValues.get(item, key).map(AttributeValues::toString).orElse(defaultValue);
}
public static long getLong(Map<String, AttributeValue> item, String key, long defaultValue) {
return AttributeValues.get(item, key).map(AttributeValues::toLong).orElse(defaultValue);
}
public static byte[] getByteArray(Map<String, AttributeValue> item, String key, byte[] defaultValue) {
return AttributeValues.get(item, key).map(AttributeValues::toByteArray).orElse(defaultValue);
}
public static UUID getUUID(Map<String, AttributeValue> item, String key, UUID defaultValue) {
return AttributeValues.get(item, key).map(AttributeValues::toUUID).orElse(defaultValue);
}
}

View File

@@ -11,19 +11,59 @@ import com.codahale.metrics.MetricRegistry;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import io.github.resilience4j.circuitbreaker.CircuitBreaker; import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.retry.Retry; import io.github.resilience4j.retry.Retry;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
public class CircuitBreakerUtil { public class CircuitBreakerUtil {
private static final String CIRCUIT_BREAKER_CALL_COUNTER_NAME = name(CircuitBreakerUtil.class, "breaker", "call");
private static final String CIRCUIT_BREAKER_STATE_GAUGE_NAME = name(CircuitBreakerUtil.class, "breaker", "state");
private static final String RETRY_CALL_COUNTER_NAME = name(CircuitBreakerUtil.class, "retry", "call");
private static final String NAME_TAG_NAME = "name";
private static final String OUTCOME_TAG_NAME = "outcome";
public static void registerMetrics(MetricRegistry metricRegistry, CircuitBreaker circuitBreaker, Class<?> clazz) { public static void registerMetrics(MetricRegistry metricRegistry, CircuitBreaker circuitBreaker, Class<?> clazz) {
Meter successMeter = metricRegistry.meter(name(clazz, circuitBreaker.getName(), "success" )); Meter successMeter = metricRegistry.meter(name(clazz, circuitBreaker.getName(), "success" ));
Meter failureMeter = metricRegistry.meter(name(clazz, circuitBreaker.getName(), "failure" )); Meter failureMeter = metricRegistry.meter(name(clazz, circuitBreaker.getName(), "failure" ));
Meter unpermittedMeter = metricRegistry.meter(name(clazz, circuitBreaker.getName(), "unpermitted")); Meter unpermittedMeter = metricRegistry.meter(name(clazz, circuitBreaker.getName(), "unpermitted"));
final String breakerName = clazz.getSimpleName() + "/" + circuitBreaker.getName();
final Counter successCounter = Metrics.counter(CIRCUIT_BREAKER_CALL_COUNTER_NAME,
NAME_TAG_NAME, breakerName,
OUTCOME_TAG_NAME, "success");
final Counter failureCounter = Metrics.counter(CIRCUIT_BREAKER_CALL_COUNTER_NAME,
NAME_TAG_NAME, breakerName,
OUTCOME_TAG_NAME, "failure");
final Counter unpermittedCounter = Metrics.counter(CIRCUIT_BREAKER_CALL_COUNTER_NAME,
NAME_TAG_NAME, breakerName,
OUTCOME_TAG_NAME, "unpermitted");
circuitBreaker.getEventPublisher().onSuccess(event -> {
successMeter.mark();
successCounter.increment();
});
circuitBreaker.getEventPublisher().onError(event -> {
failureMeter.mark();
failureCounter.increment();
});
circuitBreaker.getEventPublisher().onCallNotPermitted(event -> {
unpermittedMeter.mark();
unpermittedCounter.increment();
});
metricRegistry.gauge(name(clazz, circuitBreaker.getName(), "state"), () -> ()-> circuitBreaker.getState().getOrder()); metricRegistry.gauge(name(clazz, circuitBreaker.getName(), "state"), () -> ()-> circuitBreaker.getState().getOrder());
circuitBreaker.getEventPublisher().onSuccess(event -> successMeter.mark()); Metrics.gauge(CIRCUIT_BREAKER_STATE_GAUGE_NAME,
circuitBreaker.getEventPublisher().onError(event -> failureMeter.mark()); Tags.of(Tag.of(NAME_TAG_NAME, circuitBreaker.getName())),
circuitBreaker.getEventPublisher().onCallNotPermitted(event -> unpermittedMeter.mark()); circuitBreaker, breaker -> breaker.getState().getOrder());
} }
public static void registerMetrics(MetricRegistry metricRegistry, Retry retry, Class<?> clazz) { public static void registerMetrics(MetricRegistry metricRegistry, Retry retry, Class<?> clazz) {
@@ -32,10 +72,43 @@ public class CircuitBreakerUtil {
Meter errorMeter = metricRegistry.meter(name(clazz, retry.getName(), "error" )); Meter errorMeter = metricRegistry.meter(name(clazz, retry.getName(), "error" ));
Meter ignoredErrorMeter = metricRegistry.meter(name(clazz, retry.getName(), "ignored_error")); Meter ignoredErrorMeter = metricRegistry.meter(name(clazz, retry.getName(), "ignored_error"));
retry.getEventPublisher().onSuccess(event -> successMeter.mark()); final String retryName = clazz.getSimpleName() + "/" + retry.getName();
retry.getEventPublisher().onRetry(event -> retryMeter.mark());
retry.getEventPublisher().onError(event -> errorMeter.mark()); final Counter successCounter = Metrics.counter(RETRY_CALL_COUNTER_NAME,
retry.getEventPublisher().onIgnoredError(event -> ignoredErrorMeter.mark()); NAME_TAG_NAME, retryName,
OUTCOME_TAG_NAME, "success");
final Counter retryCounter = Metrics.counter(RETRY_CALL_COUNTER_NAME,
NAME_TAG_NAME, retryName,
OUTCOME_TAG_NAME, "retry");
final Counter errorCounter = Metrics.counter(RETRY_CALL_COUNTER_NAME,
NAME_TAG_NAME, retryName,
OUTCOME_TAG_NAME, "error");
final Counter ignoredErrorCounter = Metrics.counter(RETRY_CALL_COUNTER_NAME,
NAME_TAG_NAME, retryName,
OUTCOME_TAG_NAME, "ignored_error");
retry.getEventPublisher().onSuccess(event -> {
successMeter.mark();
successCounter.increment();
});
retry.getEventPublisher().onRetry(event -> {
retryMeter.mark();
retryCounter.increment();
});
retry.getEventPublisher().onError(event -> {
errorMeter.mark();
errorCounter.increment();
});
retry.getEventPublisher().onIgnoredError(event -> {
ignoredErrorMeter.mark();
ignoredErrorCounter.increment();
});
} }
} }

View File

@@ -0,0 +1,41 @@
package org.whispersystems.textsecuregcm.util;
import org.whispersystems.textsecuregcm.configuration.DynamoDbConfiguration;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.core.client.config.ClientAsyncConfiguration;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClientBuilder;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import java.util.concurrent.Executor;
public class DynamoDbFromConfig {
private static ClientOverrideConfiguration clientOverrideConfiguration(DynamoDbConfiguration config) {
return ClientOverrideConfiguration.builder()
.apiCallTimeout(config.getClientExecutionTimeout())
.apiCallAttemptTimeout(config.getClientRequestTimeout())
.build();
}
public static DynamoDbClient client(DynamoDbConfiguration config, AwsCredentialsProvider credentialsProvider) {
return DynamoDbClient.builder()
.region(Region.of(config.getRegion()))
.credentialsProvider(credentialsProvider)
.overrideConfiguration(clientOverrideConfiguration(config))
.build();
}
public static DynamoDbAsyncClient asyncClient(DynamoDbConfiguration config, AwsCredentialsProvider credentialsProvider, Executor executor) {
DynamoDbAsyncClientBuilder builder = DynamoDbAsyncClient.builder()
.region(Region.of(config.getRegion()))
.credentialsProvider(credentialsProvider)
.overrideConfiguration(clientOverrideConfiguration(config));
if (executor != null) {
builder.asyncConfiguration(ClientAsyncConfiguration.builder()
.advancedOption(SdkAdvancedAsyncClientOption.FUTURE_COMPLETION_EXECUTOR,
executor)
.build());
}
return builder.build();
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Locale;
public class HostnameUtil {
private static final Logger log = LoggerFactory.getLogger(HostnameUtil.class);
public static String getLocalHostname() {
try {
return InetAddress.getLocalHost().getHostName().toLowerCase(Locale.US);
} catch (final UnknownHostException e) {
log.warn("Failed to get hostname", e);
return "unknown";
}
}
}

View File

@@ -5,6 +5,7 @@
package org.whispersystems.textsecuregcm.util; package org.whispersystems.textsecuregcm.util;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.UUID; import java.util.UUID;
@@ -26,12 +27,15 @@ public class UUIDUtil {
} }
public static UUID fromByteBuffer(final ByteBuffer byteBuffer) { public static UUID fromByteBuffer(final ByteBuffer byteBuffer) {
if (byteBuffer.array().length != 16) { try {
throw new IllegalArgumentException("unexpected byte array length; was " + byteBuffer.array().length + " but expected 16"); final long mostSigBits = byteBuffer.getLong();
final long leastSigBits = byteBuffer.getLong();
if (byteBuffer.hasRemaining()) {
throw new IllegalArgumentException("unexpected byte array length; was greater than 16");
}
return new UUID(mostSigBits, leastSigBits);
} catch (BufferUnderflowException e) {
throw new IllegalArgumentException("unexpected byte array length; was less than 16");
} }
final long mostSigBits = byteBuffer.getLong();
final long leastSigBits = byteBuffer.getLong();
return new UUID(mostSigBits, leastSigBits);
} }
} }

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.util.logging;
import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.jersey.errors.LoggingExceptionMapper;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Context;
import org.glassfish.jersey.server.ExtendedUriInfo;
import org.slf4j.Logger;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
public class LoggingUnhandledExceptionMapper extends LoggingExceptionMapper<Throwable> {
@Context
private HttpServletRequest request;
@Context
private ExtendedUriInfo uriInfo;
public LoggingUnhandledExceptionMapper() {
super();
}
@VisibleForTesting
LoggingUnhandledExceptionMapper(final Logger logger) {
super(logger);
}
@Override
protected String formatLogMessage(final long id, final Throwable exception) {
String requestMethod = "unknown method";
String userAgent = "missing";
String requestPath = "/{unknown path}";
try {
// request and uriInfo shouldnt be `null`, but it is technically possible
requestMethod = request.getMethod();
requestPath = UriInfoUtil.getPathTemplate(uriInfo);
userAgent = request.getHeader("user-agent");
// streamline the user-agent if it is recognized
final UserAgent ua = UserAgentUtil.parseUserAgentString(userAgent);
userAgent = String.format("%s %s", ua.getPlatform(), ua.getVersion());
} catch (final UnrecognizedUserAgentException ignored) {
} catch (final Exception e) {
logger.warn("Unexpected exception getting request details", e);
}
return String.format("%s at %s %s (%s)",
super.formatLogMessage(id, exception),
requestMethod,
requestPath,
userAgent) ;
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.util.logging;
import org.glassfish.jersey.server.ExtendedUriInfo;
public class UriInfoUtil {
public static String getPathTemplate(final ExtendedUriInfo uriInfo) {
final StringBuilder pathBuilder = new StringBuilder();
for (int i = uriInfo.getMatchedTemplates().size() - 1; i >= 0; i--) {
pathBuilder.append(uriInfo.getMatchedTemplates().get(i).getTemplate());
}
return pathBuilder.toString();
}
}

View File

@@ -68,6 +68,11 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
context.getClient(), context.getClient(),
retrySchedulingExecutor); retrySchedulingExecutor);
// TODO Remove once PIN-based reglocks have been deprecated
if (account.getRegistrationLock().requiresClientRegistrationLock() && account.getRegistrationLock().hasDeprecatedPin()) {
log.info("User-Agent with deprecated PIN-based registration lock: {}", context.getClient().getUserAgent());
}
openWebsocketCounter.inc(); openWebsocketCounter.inc();
RedisOperation.unchecked(() -> apnFallbackManager.cancel(account, device)); RedisOperation.unchecked(() -> apnFallbackManager.cancel(account, device));

View File

@@ -171,7 +171,7 @@ public class WebSocketConnection implements MessageAvailabilityListener, Displac
messagesManager.delete(account.getUuid(), device.getId(), storedMessageInfo.get().getGuid()); messagesManager.delete(account.getUuid(), device.getId(), storedMessageInfo.get().getGuid());
} }
if (message.getType() != Envelope.Type.RECEIPT) { if (message.getType() != Envelope.Type.SERVER_DELIVERY_RECEIPT) {
recordMessageDeliveryDuration(message.getTimestamp(), device); recordMessageDeliveryDuration(message.getTimestamp(), device);
sendDeliveryReceiptFor(message); sendDeliveryReceiptFor(message);
} }

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.workers;
import io.dropwizard.cli.Command;
import io.dropwizard.setup.Bootstrap;
import java.nio.file.Files;
import java.nio.file.Path;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
public class CheckDynamicConfigurationCommand extends Command {
public CheckDynamicConfigurationCommand() {
super("check-dynamic-config", "Check validity of a dynamic configuration file");
}
@Override
public void configure(final Subparser subparser) {
subparser.addArgument("file")
.type(String.class)
.required(true)
.help("Dynamic configuration file to check");
}
@Override
public void run(final Bootstrap<?> bootstrap, final Namespace namespace) throws Exception {
final Path path = Path.of(namespace.getString("file"));
if (DynamicConfigurationManager.parseConfiguration(Files.readString(path)).isPresent()) {
System.out.println("Dynamic configuration file at " + path + " is valid");
} else {
System.err.println("Dynamic configuration file at " + path + " is not valid");
}
}
}

View File

@@ -10,10 +10,7 @@ import static com.codahale.metrics.MetricRegistry.name;
import com.amazonaws.ClientConfiguration; import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.InstanceProfileCredentialsProvider; import com.amazonaws.auth.InstanceProfileCredentialsProvider;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsync;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsyncClientBuilder;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import io.dropwizard.Application; import io.dropwizard.Application;
import io.dropwizard.cli.EnvironmentCommand; import io.dropwizard.cli.EnvironmentCommand;
@@ -44,6 +41,8 @@ import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsDynamoDb; import org.whispersystems.textsecuregcm.storage.AccountsDynamoDb;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.AccountsManager.DeletionReason; import org.whispersystems.textsecuregcm.storage.AccountsManager.DeletionReason;
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase; import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb; import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
@@ -57,8 +56,13 @@ import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb; import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
import org.whispersystems.textsecuregcm.storage.ReportMessageManager; import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
import org.whispersystems.textsecuregcm.storage.ReservedUsernames; import org.whispersystems.textsecuregcm.storage.ReservedUsernames;
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
import org.whispersystems.textsecuregcm.storage.Usernames; import org.whispersystems.textsecuregcm.storage.Usernames;
import org.whispersystems.textsecuregcm.storage.UsernamesManager; import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.storage.VerificationCodeStore;
import org.whispersystems.textsecuregcm.util.DynamoDbFromConfig;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfiguration> { public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfiguration> {
@@ -100,64 +104,22 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
FaultTolerantDatabase accountDatabase = new FaultTolerantDatabase("account_database_delete_user", accountJdbi, configuration.getAccountsDatabaseConfiguration().getCircuitBreakerConfiguration()); FaultTolerantDatabase accountDatabase = new FaultTolerantDatabase("account_database_delete_user", accountJdbi, configuration.getAccountsDatabaseConfiguration().getCircuitBreakerConfiguration());
ClientResources redisClusterClientResources = ClientResources.builder().build(); ClientResources redisClusterClientResources = ClientResources.builder().build();
AmazonDynamoDBClientBuilder clientBuilder = AmazonDynamoDBClientBuilder
.standard()
.withRegion(configuration.getMessageDynamoDbConfiguration().getRegion())
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) configuration.getMessageDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
.withRequestTimeout((int) configuration.getMessageDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
.withCredentials(InstanceProfileCredentialsProvider.getInstance());
AmazonDynamoDBClientBuilder keysDynamoDbClientBuilder = AmazonDynamoDBClientBuilder
.standard()
.withRegion(configuration.getKeysDynamoDbConfiguration().getRegion())
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) configuration.getKeysDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
.withRequestTimeout((int) configuration.getKeysDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
.withCredentials(InstanceProfileCredentialsProvider.getInstance());
AmazonDynamoDBClientBuilder accountsDynamoDbClientBuilder = AmazonDynamoDBClientBuilder
.standard()
.withRegion(configuration.getAccountsDynamoDbConfiguration().getRegion())
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) configuration.getAccountsDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
.withRequestTimeout((int) configuration.getAccountsDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
.withCredentials(InstanceProfileCredentialsProvider.getInstance());
ThreadPoolExecutor accountsDynamoDbMigrationThreadPool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, ThreadPoolExecutor accountsDynamoDbMigrationThreadPool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS,
new LinkedBlockingDeque<>()); new LinkedBlockingDeque<>());
AmazonDynamoDBAsyncClientBuilder accountsDynamoDbAsyncClientBuilder = AmazonDynamoDBAsyncClientBuilder DynamoDbClient reportMessagesDynamoDb = DynamoDbFromConfig.client(configuration.getReportMessageDynamoDbConfiguration(),
.standard() software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
.withRegion(accountsDynamoDbClientBuilder.getRegion()) DynamoDbClient messageDynamoDb = DynamoDbFromConfig.client(configuration.getMessageDynamoDbConfiguration(),
.withClientConfiguration(accountsDynamoDbClientBuilder.getClientConfiguration()) software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
.withCredentials(accountsDynamoDbClientBuilder.getCredentials()) DynamoDbClient preKeysDynamoDb = DynamoDbFromConfig.client(configuration.getKeysDynamoDbConfiguration(),
.withExecutorFactory(() -> accountsDynamoDbMigrationThreadPool); software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
DynamoDbClient accountsDynamoDbClient = DynamoDbFromConfig.client(configuration.getAccountsDynamoDbConfiguration(),
AmazonDynamoDBClientBuilder migrationDeletedAccountsDynamoDbClientBuilder = AmazonDynamoDBClientBuilder software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
.standard() DynamoDbAsyncClient accountsDynamoDbAsyncClient = DynamoDbFromConfig.asyncClient(configuration.getAccountsDynamoDbConfiguration(),
.withRegion(configuration.getMigrationDeletedAccountsDynamoDbConfiguration().getRegion()) software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create(),
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) configuration.getMigrationDeletedAccountsDynamoDbConfiguration().getClientExecutionTimeout().toMillis())) accountsDynamoDbMigrationThreadPool);
.withRequestTimeout((int) configuration.getMigrationDeletedAccountsDynamoDbConfiguration().getClientRequestTimeout().toMillis())) DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client(configuration.getDeletedAccountsDynamoDbConfiguration(),
.withCredentials(InstanceProfileCredentialsProvider.getInstance()); software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
AmazonDynamoDBClientBuilder migrationRetryAccountsDynamoDbClientBuilder = AmazonDynamoDBClientBuilder
.standard()
.withRegion(configuration.getMigrationRetryAccountsDynamoDbConfiguration().getRegion())
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) configuration.getMigrationRetryAccountsDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
.withRequestTimeout((int) configuration.getMigrationRetryAccountsDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
.withCredentials(InstanceProfileCredentialsProvider.getInstance());
AmazonDynamoDBClientBuilder reportMessageDynamoDbClientBuilder = AmazonDynamoDBClientBuilder
.standard()
.withRegion(configuration.getReportMessageDynamoDbConfiguration().getRegion())
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) configuration.getReportMessageDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
.withRequestTimeout((int) configuration.getReportMessageDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
.withCredentials(InstanceProfileCredentialsProvider.getInstance());
DynamoDB messageDynamoDb = new DynamoDB(clientBuilder.build());
DynamoDB preKeysDynamoDb = new DynamoDB(keysDynamoDbClientBuilder.build());
DynamoDB reportMessagesDynamoDb = new DynamoDB(reportMessageDynamoDbClientBuilder.build());
AmazonDynamoDB accountsDynamoDbClient = accountsDynamoDbClientBuilder.build();
AmazonDynamoDBAsync accountsDynamoDbAsyncClient = accountsDynamoDbAsyncClientBuilder.build();
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", configuration.getCacheClusterConfiguration(), redisClusterClientResources); FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", configuration.getCacheClusterConfiguration(), redisClusterClientResources);
@@ -173,14 +135,27 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager(dynamicConfigurationManager); ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager(dynamicConfigurationManager);
DynamoDB migrationDeletedAccountsDynamoDb = new DynamoDB(migrationDeletedAccountsDynamoDbClientBuilder.build()); DynamoDbClient migrationDeletedAccountsDynamoDb = DynamoDbFromConfig.client(configuration.getMigrationDeletedAccountsDynamoDbConfiguration(),
DynamoDB migrationRetryAccountsDynamoDb = new DynamoDB(migrationRetryAccountsDynamoDbClientBuilder.build()); software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
DynamoDbClient migrationRetryAccountsDynamoDb = DynamoDbFromConfig.client(configuration.getMigrationRetryAccountsDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
DynamoDbClient pendingAccountsDynamoDbClient = DynamoDbFromConfig.client(configuration.getPendingAccountsDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
AmazonDynamoDB deletedAccountsLockDynamoDbClient = AmazonDynamoDBClientBuilder.standard()
.withRegion(configuration.getDeletedAccountsLockDynamoDbConfiguration().getRegion())
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) configuration.getDeletedAccountsLockDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
.withRequestTimeout((int) configuration.getDeletedAccountsLockDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
.withCredentials(InstanceProfileCredentialsProvider.getInstance())
.build();
DeletedAccounts deletedAccounts = new DeletedAccounts(deletedAccountsDynamoDbClient, configuration.getDeletedAccountsDynamoDbConfiguration().getTableName(), configuration.getDeletedAccountsDynamoDbConfiguration().getNeedsReconciliationIndexName());
MigrationDeletedAccounts migrationDeletedAccounts = new MigrationDeletedAccounts(migrationDeletedAccountsDynamoDb, configuration.getMigrationDeletedAccountsDynamoDbConfiguration().getTableName()); MigrationDeletedAccounts migrationDeletedAccounts = new MigrationDeletedAccounts(migrationDeletedAccountsDynamoDb, configuration.getMigrationDeletedAccountsDynamoDbConfiguration().getTableName());
MigrationRetryAccounts migrationRetryAccounts = new MigrationRetryAccounts(migrationRetryAccountsDynamoDb, configuration.getMigrationRetryAccountsDynamoDbConfiguration().getTableName()); MigrationRetryAccounts migrationRetryAccounts = new MigrationRetryAccounts(migrationRetryAccountsDynamoDb, configuration.getMigrationRetryAccountsDynamoDbConfiguration().getTableName());
VerificationCodeStore pendingAccounts = new VerificationCodeStore(pendingAccountsDynamoDbClient, configuration.getPendingAccountsDynamoDbConfiguration().getTableName());
Accounts accounts = new Accounts(accountDatabase); Accounts accounts = new Accounts(accountDatabase);
AccountsDynamoDb accountsDynamoDb = new AccountsDynamoDb(accountsDynamoDbClient, accountsDynamoDbAsyncClient, accountsDynamoDbMigrationThreadPool, new DynamoDB(accountsDynamoDbClient), configuration.getAccountsDynamoDbConfiguration().getTableName(), configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(), migrationDeletedAccounts, migrationRetryAccounts); AccountsDynamoDb accountsDynamoDb = new AccountsDynamoDb(accountsDynamoDbClient, accountsDynamoDbAsyncClient, accountsDynamoDbMigrationThreadPool, configuration.getAccountsDynamoDbConfiguration().getTableName(), configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(), migrationDeletedAccounts, migrationRetryAccounts);
Usernames usernames = new Usernames(accountDatabase); Usernames usernames = new Usernames(accountDatabase);
Profiles profiles = new Profiles(accountDatabase); Profiles profiles = new Profiles(accountDatabase);
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase); ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
@@ -199,7 +174,9 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(reportMessagesDynamoDb, configuration.getReportMessageDynamoDbConfiguration().getTableName()); ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(reportMessagesDynamoDb, configuration.getReportMessageDynamoDbConfiguration().getTableName());
ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, Metrics.globalRegistry); ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, Metrics.globalRegistry);
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, pushLatencyManager, reportMessageManager); MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, pushLatencyManager, reportMessageManager);
AccountsManager accountsManager = new AccountsManager(accounts, accountsDynamoDb, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager, secureStorageClient, secureBackupClient, experimentEnrollmentManager, dynamicConfigurationManager); DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts, deletedAccountsLockDynamoDbClient, configuration.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
AccountsManager accountsManager = new AccountsManager(accounts, accountsDynamoDb, cacheCluster, deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager, pendingAccountsManager, secureStorageClient, secureBackupClient, experimentEnrollmentManager, dynamicConfigurationManager);
for (String user: users) { for (String user: users) {
Optional<Account> account = accountsManager.get(user); Optional<Account> account = accountsManager.get(user);

View File

@@ -1,43 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.workers;
import io.dropwizard.cli.ConfiguredCommand;
import io.dropwizard.setup.Bootstrap;
import io.lettuce.core.resource.ClientResources;
import net.sourceforge.argparse4j.inf.Namespace;
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import java.util.List;
public class GetRedisCommandStatsCommand extends ConfiguredCommand<WhisperServerConfiguration> {
public GetRedisCommandStatsCommand() {
super("rediscommandstats", "Dump Redis command stats");
}
@Override
protected void run(final Bootstrap<WhisperServerConfiguration> bootstrap, final Namespace namespace, final WhisperServerConfiguration config) {
final ClientResources redisClusterClientResources = ClientResources.builder().build();
final FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", config.getCacheClusterConfiguration(), redisClusterClientResources);
final FaultTolerantRedisCluster messagesCacheCluster = new FaultTolerantRedisCluster("messages_cluster", config.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClusterClientResources);
final FaultTolerantRedisCluster metricsCluster = new FaultTolerantRedisCluster("metrics_cluster", config.getMetricsClusterConfiguration(), redisClusterClientResources);
for (final FaultTolerantRedisCluster cluster : List.of(cacheCluster, messagesCacheCluster, metricsCluster)) {
cluster.useCluster(connection -> connection.sync()
.upstream()
.commands()
.info("commandstats")
.asMap()
.forEach((node, commandStats) -> {
System.out.format("# %s - %s\n\n", cluster.getName(), node.getUri());
System.out.println(commandStats);
}));
}
}
}

View File

@@ -1,62 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.workers;
import io.dropwizard.cli.ConfiguredCommand;
import io.dropwizard.setup.Bootstrap;
import io.lettuce.core.resource.ClientResources;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class GetRedisSlowlogCommand extends ConfiguredCommand<WhisperServerConfiguration> {
public GetRedisSlowlogCommand() {
super("redisslowlog", "Dump a JSON blob describing slow Redis operations");
}
@Override
public void configure(final Subparser subparser) {
super.configure(subparser);
subparser.addArgument("-n", "--entries")
.dest("entries")
.type(Integer.class)
.required(false)
.setDefault(128)
.help("The maximum number of SLOWLOG entries to retrieve per cluster node");
}
@Override
protected void run(final Bootstrap<WhisperServerConfiguration> bootstrap, final Namespace namespace, final WhisperServerConfiguration config) throws Exception {
final int entries = namespace.getInt("entries");
final ClientResources redisClusterClientResources = ClientResources.builder().build();
final FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", config.getCacheClusterConfiguration(), redisClusterClientResources);
final FaultTolerantRedisCluster messagesCacheCluster = new FaultTolerantRedisCluster("messages_cluster", config.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClusterClientResources);
final FaultTolerantRedisCluster metricsCluster = new FaultTolerantRedisCluster("metrics_cluster", config.getMetricsClusterConfiguration(), redisClusterClientResources);
final Map<String, List<Object>> slowlogsByUri = new HashMap<>();
for (final FaultTolerantRedisCluster cluster : List.of(cacheCluster, messagesCacheCluster, metricsCluster)) {
cluster.useCluster(connection -> connection.sync()
.upstream()
.commands()
.slowlogGet(entries)
.asMap()
.forEach((node, slowlogs) -> slowlogsByUri.put(node.getUri().toString(), slowlogs)));
}
SystemMapper.getMapper().writeValue(System.out, slowlogsByUri);
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.workers;
import io.dropwizard.cli.Command;
import io.dropwizard.setup.Bootstrap;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.whispersystems.textsecuregcm.WhisperServerVersion;
public class ServerVersionCommand extends Command {
public ServerVersionCommand() {
super("version", "Print the version of the service");
}
@Override
public void configure(final Subparser subparser) {
}
@Override
public void run(final Bootstrap<?> bootstrap, final Namespace namespace) throws Exception {
System.out.println(WhisperServerVersion.getServerVersion());
}
}

Some files were not shown because too many files have changed in this diff Show More