Compare commits

...

486 Commits

Author SHA1 Message Date
Jon Chambers
a87b84fbe2 Return an empty response if somebody requests a profile key credential with a non-existent version 2021-12-16 10:30:55 -05:00
Chris Eager
b2f0ace9db Update dropwizard to 2.0.26 2021-12-15 16:34:52 -08:00
Jon Chambers
20c95e2606 Register ApplicationShutdownMonitor last
This will make it start last and shut down first, which is pretty much what we want for shutdown state monitoring
2021-12-15 19:27:23 -05:00
Jon Chambers
22dccaeddb Count cases where we can find a given account, but not the given profile version 2021-12-15 15:53:47 -05:00
Jon Chambers
e611a70ba4 Remove usernames from profile responses 2021-12-15 15:53:47 -05:00
Jon Chambers
66845d7080 Refactor: separate the various types of profile responses 2021-12-15 15:53:47 -05:00
Jon Chambers
4ea7278c6f Remove unversioned profile properties from Account entities 2021-12-15 15:53:47 -05:00
Jon Chambers
2b2e26f14b Remove deprecated, unversioned profile setters 2021-12-15 15:53:47 -05:00
Jon Chambers
b496ef8d6f Fix an issue where the deleted accounts lock client was trying to talk to the deleted accounts table 2021-12-15 13:16:32 -05:00
Jon Chambers
7f5e83141d Consolidate DynamoDB clients/configuration 2021-12-15 12:43:57 -05:00
Jon Chambers
2d1ca98605 Consolidate DynamoDB clients/configuration 2021-12-15 12:42:41 -05:00
Jon Chambers
eaa4c318e3 Add usernames to whoami and account creation responses 2021-12-15 11:47:10 -05:00
Jon Chambers
31373fd1ba Add a command for assigning usernames to individual users 2021-12-15 11:46:33 -05:00
Ehren Kret
9086246947 Require any transitive dependencies on log4j to use 2.16.0 2021-12-15 01:16:10 -06:00
Ehren Kret
7855b70682 Require any transitive dependencies on log4j to use 2.15.0 2021-12-10 01:16:48 -06:00
Chris Eager
0ce87153e5 Remove second database for AbusiveHostRules 2021-12-08 16:03:02 -08:00
Chris Eager
dba1711e8d Convert AbusiveHostRule to a record 2021-12-08 14:43:21 -08:00
Chris Eager
a70b057e1c Add second (migration) database to AbusiveHostRules 2021-12-08 12:46:05 -08:00
Chris Eager
9a5ffea0ad Move ossrh-snapshots to project aggregate pom 2021-12-07 15:14:47 -08:00
Chris Eager
96f4b771ea Update wiremock to 2.32.0 2021-12-07 10:34:49 -08:00
Ehren Kret
3df143dd3d Switch to zonky test embedded postgres and use postgres 11.13.0 2021-12-03 16:07:07 -05:00
Jon Chambers
d78d7c726e Fix a UUID transposition when requesting profile key credentials 2021-12-03 16:06:26 -05:00
Jon Chambers
d0ad580c7d Add (failing!) tests for getting a profile key credentials for somebody else 2021-12-03 16:06:26 -05:00
Ehren Kret
4a8a2a70b5 Return 400 instead of 500 when amount is too small 2021-12-03 12:24:16 -06:00
Jon Chambers
20a71b7df2 Add tests for generating profile key credentials 2021-12-03 12:16:13 -05:00
Jon Chambers
68412b3901 Allow the "get profile" endpoint to include a PNI credential 2021-12-03 12:16:13 -05:00
Jon Chambers
93a7c60a15 Update to zkgroup 0.9.0 2021-12-03 12:16:13 -05:00
Chris Eager
31e5058b15 Make temporary iOS envelope type adaptation a little wider 2021-12-03 12:06:31 -05:00
Jon Chambers
14cff958e9 Migrate challenge-issuing rate limiters to the abusive message filter 2021-12-03 11:52:58 -05:00
Jon Chambers
9628f147f1 Separate statically- and dynamically-configured rate limiters 2021-12-03 11:01:15 -05:00
Chris Eager
13e346d4eb Distinguish local vs remote in ClientPresenceManager#disconnectPresence 2021-12-02 14:32:42 -08:00
Fedor Indutny
e507ce2f26 Include ACI and PNI in DeviceResponse 2021-12-02 17:21:05 -05:00
Jon Chambers
9c62622733 Handle cases where a message might be missing a destination UUID 2021-12-02 14:06:49 -05:00
Jon Chambers
62aa0cef39 Set destination UUIDs for receipts 2021-12-02 14:06:49 -05:00
Jon Chambers
401953313a Remove all vestiges of the relational account database 2021-12-02 12:46:43 -05:00
Chris Eager
4d2403d619 Use assertTimeoutPreemptively instead of assertTimeout 2021-12-02 12:17:08 -05:00
Jon Chambers
c5f261305d Store destination UUIDs when persisting messages to DynamoDB 2021-12-02 12:17:08 -05:00
Jon Chambers
394f58f6cc Add a (failing!) check for destination UUIDs 2021-12-02 12:17:08 -05:00
Jon Chambers
674bf1b0e0 Drop a myserious empty test 2021-12-02 12:17:08 -05:00
Jon Chambers
606ddd8a9b Populate destination UUID for messages delivered via websocket 2021-12-02 12:17:08 -05:00
Jon Chambers
e23a1fac50 Remove old Postgres-backed remote config machinery 2021-12-02 12:16:43 -05:00
Jon Chambers
342323a7e6 Use canonical usernames throughout AccountsManager 2021-12-02 12:02:29 -05:00
Jon Chambers
efb410444b Introduce a username validator 2021-12-02 12:02:29 -05:00
Jon Chambers
17c9b4c5d3 Transition from Postgres-backed remote config store to Dynamo-backed store 2021-12-02 11:13:07 -05:00
Jon Chambers
706de8e2f1 Add a command to migrate remote configuration entries from Postgres to DynamoDB 2021-12-02 11:13:07 -05:00
Jon Chambers
23bc11f3b6 Introduce a DynamoDB-backed remote config store 2021-12-02 11:13:07 -05:00
Jon Chambers
4eb7dde1c8 Migrate RemoteConfigsTest to Junit 5 2021-12-02 11:13:07 -05:00
Jon Chambers
064861b930 Consolidate profiles store tests and discard ProfilesStore interface 2021-12-02 10:49:54 -05:00
Jon Chambers
afa910bbd7 Drop relational profiles store 2021-12-02 10:49:54 -05:00
Jon Chambers
6aceb24fd2 Drop profile migration tools 2021-12-02 10:49:54 -05:00
Jon Chambers
d94e86781f Migrate username storage from a relational database to DynamoDB 2021-12-01 16:50:18 -05:00
Daniel Gospodinow
0d4a3b1ad4 Fix typo in .gitmodules 2021-12-01 13:26:32 -05:00
Jon Chambers
acfcb18f29 Allow overwriting of previously-delted profiles 2021-12-01 11:59:18 -05:00
Jon Chambers
f7ff8e3837 Add a (failing!) test for deleting, then resetting profiles 2021-12-01 11:59:18 -05:00
Jon Chambers
048e17c62b Use a memoizing supplier instead of a looping thread to cache remote config entries 2021-11-30 16:35:42 -07:00
Jon Chambers
d89b4f7e95 Compare versioned profiles synchronously; log a subset of mismatches for further investigation 2021-11-30 16:35:29 -07:00
Chris Eager
795b226b90 Mark methods that update SignedPreKeys as @ChangesDeviceEnabledState 2021-11-30 10:40:12 -07:00
Jon Chambers
e485c380e0 Change the name of the CSV file argument to avoid upstream naming collisions 2021-11-30 11:31:12 -05:00
Jon Chambers
bb4f4bc441 Modify the "migrate profiles" command to accept a list of UUIDs/versions from a CSV file 2021-11-30 11:22:51 -05:00
Jon Chambers
65b49b2d9c Use a "for each" strategy in profile migration methods 2021-11-24 16:54:30 -05:00
Jon Chambers
9e7010f185 Migrate profiles from a relational database to DynamoDB 2021-11-24 14:48:41 -05:00
Jon Chambers
3bb8e5bb00 Set a TTL for Account entries in the general cache 2021-11-24 14:47:36 -05:00
Jon Chambers
2a4d1da2ca Delete accounts from Redis after they've been deleted from Dynamo 2021-11-24 13:47:53 -05:00
Jon Chambers
6b71b66bd2 Take no action if badge list is omitted entirely 2021-11-24 13:47:39 -05:00
Jon Chambers
ebf24fb125 Add a (failing!) test for clients omitting badges in profile update requests 2021-11-24 13:47:39 -05:00
Jon Chambers
46d64b949e Don't read "soft-deleted" profiles
Nothing is actually "soft-deleting" profiles yet, and this
is a first step toward migrating profiles to a new data
store.
2021-11-24 12:02:07 -05:00
Jon Chambers
6919354520 Fix a counting bug with reported messages 2021-11-23 17:28:39 -05:00
Jon Chambers
a42fe9bfb0 Add crawler names to log messages 2021-11-23 16:22:09 -05:00
Jon Chambers
ee1f8b34ea Add a command for reserving usernames 2021-11-23 16:21:03 -05:00
Jon Chambers
c910fa406d Migrate reserved usernames from a relational database to DynamoDB 2021-11-23 16:21:03 -05:00
Jon Chambers
559205e33f Log cases where accounts are missing or have inconsistent PNIs 2021-11-23 15:40:31 -05:00
Chris Eager
c0756e9c60 Attempt an orderly websocket close on displacement before a hard disconnect 2021-11-23 11:36:32 -07:00
Ehren Kret
bf1190696e Add badge workaround for old Android builds 2021-11-23 09:58:06 -06:00
Jon Chambers
71dd0890de Restore an accidentally-removed PNI consistency check 2021-11-23 10:53:40 -05:00
Jon Chambers
e5acdf1402 Don't update the PNI attribute during general account updates 2021-11-23 10:53:40 -05:00
Jon Chambers
0f08b6bb59 Drop "got successful captcha" messages from INFO to DEBUG 2021-11-22 17:06:34 -05:00
Jon Chambers
6198a7b69a Remove spurious @JsonProperty annotations 2021-11-22 15:43:09 -05:00
Jon Chambers
067aee6664 Remove unused properties from OutgoingMessageEntity 2021-11-22 15:43:09 -05:00
Jon Chambers
138a2ebbd0 Drop transactional logic from phone number identifier migration 2021-11-22 15:32:24 -05:00
Jon Chambers
296f6a7a88 Make phone number identifiers non-optional 2021-11-22 15:32:24 -05:00
Jon Chambers
069ffa9921 Drop PNI migration tools 2021-11-22 15:32:24 -05:00
Jon Chambers
f42fd8a840 Retire unused diagnostic metrics 2021-11-22 15:31:16 -05:00
Jon Chambers
10f27af6f2 Retire old unsealed-sender meters 2021-11-22 15:31:16 -05:00
Jon Chambers
0bbd34d060 Use text blocks where possible 2021-11-22 15:30:31 -05:00
Jon Chambers
282daeb0dc Add a command to assign PNIs to accounts that don't already have one 2021-11-22 15:03:19 -05:00
Jon Chambers
d33b313c11 Break down legacy "get profile" requests by platform 2021-11-19 12:37:39 -05:00
Ehren Kret
fb7316c9ae Return subscription status string in GET 2021-11-19 11:36:01 -06:00
Ehren Kret
279b0a51d9 Use latest invoice on subscription to generate receipts 2021-11-19 11:25:38 -06:00
Ehren Kret
6547d5ebf3 More consistent naming of receipt credentials endpoints 2021-11-19 10:14:00 -06:00
Ehren Kret
4f1ef9a039 Add additional http status codes to /v1/subscription/boost/receipt_credentials 2021-11-19 10:11:33 -06:00
Ehren Kret
4c80714d19 Update sample.yml 2021-11-18 10:49:30 -06:00
Jon Chambers
077ead71a5 Rename legacy profile methods to separate them in metrics 2021-11-18 11:31:15 -05:00
Ehren Kret
caba110266 Revert "Revert "Remove transparent SVG for badging""
This reverts commit 0fdb23c1e9.
2021-11-18 10:23:09 -06:00
Ehren Kret
0fdb23c1e9 Revert "Remove transparent SVG for badging"
This reverts commit 13a84f0c72.
2021-11-18 10:09:34 -06:00
Ehren Kret
13a84f0c72 Remove transparent SVG for badging 2021-11-18 10:04:42 -06:00
Jon Chambers
669bd58e33 Drop the unused Key utility class 2021-11-17 10:57:29 -05:00
Chris Eager
6e82740a9b Update sample.yml 2021-11-16 17:25:45 -07:00
Ehren Kret
7ea43a728d Set boost description from configuration 2021-11-16 17:21:57 -06:00
Chris Eager
71b38356b1 Update to Mockito 4.0.0 2021-11-16 15:56:35 -07:00
Chris Eager
5a99708f56 Update some deprecated usages 2021-11-16 15:56:13 -07:00
Chris Eager
24191d9599 Update Dropwizard to 2.0.25 2021-11-16 15:56:00 -07:00
Chris Eager
482ea8eb40 Update minimum required maven to 3.8.3 2021-11-16 15:55:52 -07:00
Jon Chambers
1dae05651f Add PNIs to account creation and whoami responses 2021-11-16 15:08:10 -05:00
Jon Chambers
5164e92538 Shorten metric names 2021-11-16 15:08:10 -05:00
Jon Chambers
f89a20dbc7 Allow callers to set/retrieve keys by ACI or PNI 2021-11-16 15:08:10 -05:00
Jon Chambers
3a4c5a2bfb Store and retrieve one-time pre-keys by UUID 2021-11-16 15:08:10 -05:00
Jon Chambers
5e1334e8de s/KeysDynamoDb/Keys/ 2021-11-16 15:08:10 -05:00
Jon Chambers
fa6e3d3690 Allow clients to request PNI-based group credentials 2021-11-16 15:08:10 -05:00
Jon Chambers
9383e7716b Resolve CertificateControllerTest warnings and recommendations 2021-11-16 15:08:10 -05:00
Jon Chambers
cfe34fbf0f Allow unsealed-sender messages to be addressed by PNI 2021-11-16 15:08:10 -05:00
Jon Chambers
9fe110625c Add a destinationUuid field to envelopes 2021-11-16 15:08:10 -05:00
Jon Chambers
975f753c2b Add an endpoint for testing whether an account with a given ACI or PNI exists 2021-11-16 15:08:10 -05:00
Jon Chambers
e6237480f8 Require that unidentified access keys be exactly 16 bytes 2021-11-16 15:08:10 -05:00
Chris Eager
966d4e29d4 Update sample.yml config to pass mvn verify 2021-11-16 11:43:07 -07:00
Chris Eager
26f876a2cb Check service configurations in verify phase 2021-11-16 11:43:07 -07:00
Jon Chambers
ab9e6ac48a Revert "Replace zkgroup with libsignal-client"
This reverts commit 73ea6e4251.
2021-11-16 11:35:10 -05:00
Jon Chambers
c1d6c04ab2 Revert "Replace curve25519-java with libsignal-client"
This reverts commit 0011b8925b.
2021-11-16 11:35:10 -05:00
Jon Chambers
888cec3d56 Introduce a filter for correcting numeric "online" flags 2021-11-16 10:15:14 -05:00
Jon Chambers
1461bcc2c2 Correct envelope types for certain iOS builds 2021-11-16 10:15:14 -05:00
Jon Chambers
11f1cf80bd Move MessageControllerTest out of the tests sub-package to expose package-private elements from the class under test 2021-11-16 10:15:14 -05:00
Jordan Rose
c675cc8b26 Test the response code for invalid serialized zkgroup objects
Test by Jon, making sure this is consistent even without up-front size
checking.
2021-11-16 09:52:38 -05:00
Jordan Rose
0011b8925b Replace curve25519-java with libsignal-client
These APIs stemmed from a common source long ago, so there's not much
to change!
2021-11-16 09:52:38 -05:00
Jordan Rose
73ea6e4251 Replace zkgroup with libsignal-client 2021-11-16 09:52:38 -05:00
Jon Chambers
e4441dddbb Consolidate Redis client resources 2021-11-16 09:52:12 -05:00
Chris Eager
8d1d56f694 Update to Java 17 2021-11-15 16:42:43 -07:00
Jon Chambers
2015ba77ca Switch to a disallowed prefix model instead of a disallowed country code model 2021-11-15 15:44:55 -05:00
Chris Eager
7033a0f68f Set checkStaleness to true for protoc 2021-11-12 13:34:15 -07:00
Jon Chambers
6ada76da7f Parallelize assignment of phone number identifiers 2021-11-12 11:03:46 -05:00
Jon Chambers
cbdec0cb22 Remove legacy push latency measurement pathways 2021-11-11 15:44:07 -05:00
Chris Eager
de6e9d31c9 Add dedicated crawler for directory reconciler 2021-11-11 13:38:13 -07:00
Jon Chambers
f0a6be32fc Add a crawler to assign PNIs to existing accounts 2021-11-10 11:15:05 -05:00
Ehren Kret
5c4855cca6 Remove trailing space 2021-11-10 10:11:49 -06:00
Ehren Kret
2e1e380418 Also update description text of boost badge 2021-11-10 10:10:53 -06:00
Ehren Kret
d07f0b4f71 Update badge description text 2021-11-10 09:51:51 -06:00
Jon Chambers
aaa2a6eef1 Break down push latency metrics by VOIP/not-VOIP and optionally by client version 2021-11-10 10:35:41 -05:00
Jon Chambers
b1f56c3324 Resolve formatting complaints 2021-11-10 10:15:14 -05:00
Jon Chambers
da5c0ae4b6 Enable Payments Beta for more country codes 2021-11-10 10:15:14 -05:00
Jon Chambers
1e1394560d Check length of cancellation reason list before getting reason codes 2021-11-09 11:42:44 -05:00
Jon Chambers
bae0196bcf Tolerate null UUID attribute values 2021-11-09 11:00:27 -05:00
Jon Chambers
3398955c1a Add basic support for phone number identifiers 2021-11-09 10:23:08 -05:00
Chris Eager
a1b925d1e0 Reduce visiblity on one constructor 2021-11-08 14:30:52 -07:00
Chris Eager
31c0c3275f Use the latest and in config, @JsonCreator 2021-11-08 14:30:52 -07:00
Chris Eager
0a4392f700 Streamline ExternalServiceCredentialsGenerator construction 2021-11-08 14:30:52 -07:00
Chris Eager
eb86986cf4 Add /v2/directory/auth endpoint 2021-11-08 14:30:52 -07:00
Ehren Kret
1053a47e42 Add an exception mapper for CompletionExceptions 2021-11-04 19:12:31 -05:00
Ehren Kret
99b1f48e0e Copy badges from existing account on re-reg 2021-11-04 18:00:47 -05:00
Jon Chambers
c21eb6aa50 Update to the latest abusive message filter 2021-11-01 15:27:14 -04:00
Jon Chambers
6dddf54222 Consolidate rate-limit counters 2021-11-01 14:33:05 -04:00
Jon Chambers
9e3eb2319e Update to the latest abusive message filter 2021-11-01 13:12:23 -04:00
Jon Chambers
1d8dcda815 Update to the latest abusive message filter 2021-11-01 12:45:50 -04:00
Jon Chambers
ee52a84262 Update to the latest abusive message filter 2021-11-01 12:19:21 -04:00
Jon Chambers
eb51e81faa Configuration-only change 2021-11-01 11:41:26 -04:00
Jon Chambers
d41ef1df18 Configuration-only change 2021-11-01 11:27:02 -04:00
Jon Chambers
66d47aff2c Update deployment configuration 2021-11-01 11:10:19 -04:00
Jon Chambers
c931103712 Remove unused utility classes 2021-11-01 10:51:47 -04:00
Jon Chambers
ad1aeea74b Add an abusive message filter interface and submodule 2021-11-01 10:51:47 -04:00
Ehren Kret
ae7f8af03e Mark boost and subscription configuration as not null 2021-10-28 16:12:23 -07:00
Ehren Kret
a52c91a665 Add names to subscription levels 2021-10-28 14:48:44 -07:00
Ehren Kret
94bf3a3902 Extract logic for created header controlled resource bundles 2021-10-28 14:26:53 -07:00
Ehren Kret
f5a539e128 Add subscriptions resource bundle 2021-10-28 14:15:20 -07:00
Ehren Kret
24480b2090 Add endpoint to fetch information on boost badges 2021-10-26 14:05:59 -07:00
Ehren Kret
a124b3abe9 Fix encoding of boost amounts 2021-10-26 08:08:09 -07:00
Ehren Kret
090d722b61 Add method to retrieve receipt credentials for a boost payment 2021-10-25 14:54:40 -07:00
Ehren Kret
d27ec6fe8d Create boost create endpoint 2021-10-25 12:58:32 -07:00
Jon Chambers
8d34f3447b Drop an unused registration meter 2021-10-25 14:52:21 -04:00
Ehren Kret
72b52965b9 Expand definition of badge SVGs 2021-10-25 10:28:02 -07:00
Jon Chambers
ae7077c643 Refresh accounts from storage when checking for device state changes after requests 2021-10-22 14:02:28 -04:00
Jon Chambers
11598e855f Count non-normalized or impossible numbers 2021-10-22 14:01:54 -04:00
Jon Chambers
534c577f59 Enforce phone number normalization when creating accounts or changing numbers 2021-10-22 14:01:54 -04:00
Jon Chambers
7762afc497 Add a method for verifying that numbers are normalized in addition to being dialable 2021-10-22 14:01:54 -04:00
Jon Chambers
a3fe4b9980 Update/parameterize ValidNumberTest 2021-10-22 14:01:54 -04:00
Jon Chambers
598599cd14 Use a default reportMessage configuration if not specified 2021-10-21 15:34:57 -05:00
Ehren Kret
07cd69ab34 Add endpoint for fetching boost amounts 2021-10-21 13:56:35 -05:00
Jon Chambers
3b764bed7a Make DynamicConfigurationManager generic 2021-10-19 11:52:29 -04:00
Jon Chambers
c91d5c2fdb Count reported messages per sender 2021-10-19 11:47:54 -04:00
Jon Chambers
40f7e6e994 Remove unused imports 2021-10-19 11:47:54 -04:00
Ehren Kret
ee9aa9ce12 Round up by days on receipt expiration 2021-10-18 12:11:27 -05:00
Ehren Kret
08304bf375 Validate GetReceiptCredentialsRequest 2021-10-18 11:57:18 -05:00
Ehren Kret
8b8c6237be Use last subscription created at time as a subscription generation number 2021-10-14 12:06:19 -05:00
Ehren Kret
c0837104cd Bring badge configuration into levels information 2021-10-14 11:35:18 -05:00
Jon Chambers
fe21d014f7 Remove legacy rate-limiting tools 2021-10-14 11:43:18 -04:00
Ehren Kret
75c5032cd3 Add method to set default payment method 2021-10-14 10:37:12 -05:00
Jon Chambers
f84e7aebd0 Count numbers that can't be normalized because another account has the normalized form of the number 2021-10-14 11:10:12 -04:00
Jon Chambers
c379a3d297 Remove deprecated counters 2021-10-14 10:52:20 -04:00
Jon Chambers
eedeaaecee Update rate-limiting for requests matching specific criteria 2021-10-14 10:42:16 -04:00
Ehren Kret
64eeb1e361 Move to using collections for sprites and svgs for badges 2021-10-13 23:25:39 -05:00
Ehren Kret
e07597eba7 Add initial text for donation badges 2021-10-13 23:08:02 -05:00
Ehren Kret
5f2656710c Update badge configuration to new style 2021-10-13 22:58:57 -05:00
Ehren Kret
1af53f2612 Simplify getLevels API 2021-10-13 13:28:31 -05:00
Ehren Kret
c89cfa4927 Move property setting above servlet construction 2021-10-13 13:17:07 -05:00
Ehren Kret
bbde93a3c7 Enable unwrapping of CompletionStage 2021-10-13 11:46:20 -05:00
Ehren Kret
b01b76d78f First pass at subscriptions API
This is an incomplete first pass at building the subscriptions API. More API endpoints are still to be added along with controller tests.
2021-10-12 21:23:20 -05:00
Ehren Kret
75c22038eb Create empty Badges_en.properties file 2021-10-11 22:27:30 -05:00
Jon Chambers
3c1705994d Count accounts with non-normalized phone numbers 2021-10-11 10:13:08 -04:00
Jon Chambers
439d2f5df8 Update to libphonenumber 8.12.33 2021-10-05 15:19:59 -04:00
Chris Eager
d2bc3c7360 Add dynamic configuration to disable directory reconciler 2021-10-04 14:38:47 -07:00
Jon Chambers
9734433f00 Use the default SecureRandom algorithm for tests 2021-10-04 11:37:32 -04:00
Ehren Kret
5bd08800bb Remove public modifier from test class 2021-10-01 14:27:07 -05:00
Ehren Kret
3032415141 Add receipt redemption API to chat server 2021-10-01 12:44:47 -05:00
Jon Chambers
ba58a95a0f Add support for changing phone numbers 2021-10-01 10:15:33 -04:00
Jon Chambers
aa4bd92fee Lazy-load scripts; fall back to eval if evalsha returns NOSCRIPT 2021-09-29 16:08:17 -04:00
Jon Chambers
f37c76dab1 Drop LuaScript 2021-09-29 16:08:17 -04:00
Jon Chambers
863969c77c Resolve ClusterLuaScript warnings/suggestions 2021-09-29 16:08:17 -04:00
Jon Chambers
2383aaaa3d Update ClusterLuaScript formatting 2021-09-29 16:08:17 -04:00
Jon Chambers
715d1157ad Reject Redis commands when disconnected 2021-09-29 16:07:26 -04:00
Jon Chambers
4aaae3f445 Use a single configuration URI for Redis clusters 2021-09-29 14:46:09 -04:00
Jon Chambers
8359ef73f4 Cycle all connected websockets on any device or account enabled state change 2021-09-29 14:18:35 -04:00
Jon Chambers
c6bb649adb Simplify map-building logic 2021-09-29 14:18:35 -04:00
Jon Chambers
e333cbd94d Close websockets on account deletion 2021-09-29 14:11:16 -04:00
Ehren Kret
cc9a825279 Remove checked in generated code 2021-09-29 13:10:27 -05:00
Chris Eager
5189cbe5c7 apply editorconfig formatting 2021-09-29 10:31:39 -07:00
Chris Eager
d1d6e5c652 Filter stale ephemeral messages from cache 2021-09-29 10:31:39 -07:00
Chris Eager
3e5087e60b Remove obsolete ephemeral queue handling 2021-09-29 10:31:39 -07:00
Chris Eager
93c3cea912 Clean up old "ephemeral" metrics 2021-09-29 10:31:39 -07:00
Chris Eager
e824b861d4 Drop accounts table 2021-09-29 10:31:20 -07:00
Chris Eager
e8dd1e0bf2 Fixup formatting 2021-09-29 10:31:20 -07:00
Ehren Kret
533afa4c6e Upgrade to zkgroup 0.8.1 2021-09-28 09:28:28 -05:00
Ehren Kret
559026933d Add low and high detail svgs to badges 2021-09-27 17:00:09 -05:00
Ehren Kret
7864405efd Remove single URL in favor of density based sprite sheets 2021-09-27 16:50:18 -05:00
Jon Chambers
a5575902de Pause if we bump into an exception while trying to persist messages 2021-09-27 13:38:17 -04:00
Ehren Kret
5b9bce59e1 Upgrade to zkgroup 0.8.0 2021-09-24 15:56:28 -05:00
Ehren Kret
041aed2d72 Remove zkgroup enabled flag
The last remnants of a time before zkgroup have been swept away.
2021-09-23 09:24:06 -05:00
Jon Chambers
02a296e500 Use a MIME Base64 decoder for provisioning messages 2021-09-22 16:30:44 -04:00
Jon Chambers
98e41f9a37 Improve Redis exception handling 2021-09-22 10:31:39 -04:00
Chris Eager
6a71d369e2 More Accounts cleanup
* Remove `AccountStore`
* Clean up `AccountsDynamoDb#delete`
* Rename `AccountsDynamoDb` → `Accounts`
* Remove unused configuration
* Move Accounts scan page size to static configuration
* Remove disabled tests and related methods
2021-09-21 15:25:16 -07:00
Ehren Kret
75661fa800 Add JavaTimeModule 2021-09-20 16:42:38 -05:00
Ehren Kret
df5498e1c0 Add test for self badge fetching 2021-09-20 16:42:38 -05:00
Ehren Kret
c0af911197 Show invisible badges to query for self 2021-09-20 16:42:38 -05:00
Ehren Kret
44bc90e5ab Return a badge with additional properties when fetching your own profile 2021-09-20 16:42:38 -05:00
Ehren Kret
5c1cde1b28 Add visibility modifying helper method in AccountBadge 2021-09-20 16:41:09 -05:00
Ehren Kret
3172b571c6 Move "testing" string into BadgeConfiguration 2021-09-20 16:41:09 -05:00
Ehren Kret
17e8b77e88 Add unit test for setting badges 2021-09-20 16:41:09 -05:00
Ehren Kret
8011935a3b Fix compilation issues created by constructor changes 2021-09-20 16:41:09 -05:00
Ehren Kret
3f3052c23c Remove isZkEnabled 2021-09-20 16:41:09 -05:00
Ehren Kret
8f17f45339 Reorder creating of testing badges
Need their expiration time to always get refreshed on set so reorder
how they're created to ensure that's the case.
2021-09-20 16:41:09 -05:00
Ehren Kret
009e2eeb97 Enable editing of badges on profiles 2021-09-20 16:41:09 -05:00
Ehren Kret
c70fa48835 Fix indentation of ProfileController#setProfile 2021-09-20 16:41:09 -05:00
Ehren Kret
bd5f5c407b Add method to merge badge ids list into a profile 2021-09-20 16:41:09 -05:00
Ehren Kret
2bc573a53d Add additional test badges for translation 2021-09-20 16:41:09 -05:00
Ehren Kret
537d61d5bd Add badges to CreateProfileRequest
This will permit users to set the order and visibility of badges on
their profile.
2021-09-20 16:41:09 -05:00
Jon Chambers
09519ae942 Only retry websocket sending if the client is still connected 2021-09-20 14:24:07 -04:00
Chris Eager
2a67b2e610 Remove Accounts Postgres 2021-09-20 11:10:24 -07:00
Chris Eager
8161f55a82 Add dynamic configuration for setting Dynamo as primary 2021-09-17 13:28:45 -07:00
Chris Eager
ecee189ad8 Add AccountDatabaseCrawler.dedicatedDynamoMigrationCrawler 2021-09-17 11:27:20 -07:00
Jon Chambers
ef0900f3ac Add .tx/ to .gitignore 2021-09-17 13:43:52 -04:00
Fedor Indutny
383d744bd8 Log the error message when retrying queue send 2021-09-16 18:03:42 -04:00
Jon Chambers
c2ba8ab562 Identify receipt destinations by UUID instead of e164 2021-09-16 10:47:03 -04:00
Chris Eager
cd49ea43c0 Use queryPaginator when loading messages 2021-09-16 10:46:37 -04:00
Chris Eager
53aa45a2bb Use queryPaginator when deleting messages 2021-09-16 10:46:37 -04:00
Chris Eager
83e0a19561 Migrate MessagesDynamoDbRule to MessagesDynamoDbExtension 2021-09-16 10:46:37 -04:00
Jon Chambers
6a5d475198 Add a "refresh websocket on number change" provider 2021-09-16 10:37:34 -04:00
Jon Chambers
49ccbba2e3 Generalize the "watch for websockets that need to be refreshed" listener 2021-09-16 10:37:34 -04:00
Fedor Indutnyy
41735ed40e Introduce queueDrainRetry counter metric 2021-09-16 10:30:19 -04:00
Ehren Kret
2d11a433c9 Wrap all calls to getAcceptableLanguages
ContainerRequestContext#getAcceptableLanguages throws a
ProcessingException if the header has invalid values in it. Rather than
error out of the request entirely with the exception handler for that
exception, we just treat it as though no Accept-Languages header was
specified.
2021-09-16 09:28:21 -05:00
Ehren Kret
e79ab2521f Rename field in ConfiguredProfileBadgeConverter 2021-09-16 09:28:21 -05:00
Ehren Kret
fb1f99da87 Add a method to enable a badge for all accounts 2021-09-16 09:28:21 -05:00
Ehren Kret
08c6a8c2e5 Add category to badges 2021-09-16 09:28:21 -05:00
Ehren Kret
ce3835e176 Rename id to name in the configuration 2021-09-16 09:28:21 -05:00
Ehren Kret
39f6eadbb9 Add test for add and remove badges 2021-09-16 09:28:21 -05:00
Ehren Kret
16dba09b61 Handle merging badges when adding to account 2021-09-16 09:28:21 -05:00
Ehren Kret
d5ebf2f2ed Rename name to id in Account#removeBadge 2021-09-16 09:28:21 -05:00
Ehren Kret
8a8e6e7b49 Rename name to id in the stored badge information and expose id in the profile endpoint 2021-09-16 09:28:21 -05:00
Ehren Kret
34e21b9f7b Change name to id on AccountBadge
This makes it distinct from the localized name field on the Badge
entity that is returned.
2021-09-16 09:28:21 -05:00
Ehren Kret
98a31d1474 Switch ProfileController to the actual badge converter 2021-09-16 09:28:21 -05:00
Chris Eager
72a0c1be0f Tune mismatch logging 2021-09-15 16:46:10 -07:00
Ehren Kret
5b25e38e41 Ensure badges are in ordered collections 2021-09-15 16:20:15 -05:00
Chris Eager
2fb400280b Remove unused parameter from deleteMessageByDestinationAndGuid 2021-09-15 10:14:08 -07:00
Ehren Kret
79ad09524e Implement the ProfileBadgeConverter interface 2021-09-15 10:32:20 -05:00
Chris Eager
5f8accb492 Add acceptable languages from request to variable 2021-09-14 17:43:39 -07:00
Chris Eager
6fcadc2297 Handle exception reading Accept-Language header 2021-09-13 18:07:16 -07:00
Chris Eager
3f4e1522eb Only put accounts that exhaust optimistic lock retries in migration retry table 2021-09-13 15:00:01 -07:00
Graeme Connell
6304c84cdb Add ContactDiscoveryWriterTest based on mock. 2021-09-13 15:20:21 -06:00
Chris Eager
894297efa9 Add dynamic configuration for doing a mismatch post-check 2021-09-13 13:54:19 -07:00
Chris Eager
a51a7a0901 Add MigrationMismatchedAccounts to AccountsManager 2021-09-13 13:54:19 -07:00
Chris Eager
372e131e25 Update PaymentsControllerTest 2021-09-13 09:58:42 -07:00
Chris Eager
6c6e6a4975 Switch to actions/setup-java’s built-in caching 2021-09-13 09:40:44 -07:00
Sophiah Ho
cd66a1ceb7 fix merge issue after 2021 Aug 15 commit d1735c7e57 (#137) 2021-09-13 09:39:11 -07:00
Sophiah Ho
feb59deb28 Use BigDecimal instead of Double for currency rate calculations (#134)
use BigDecimal instead of double for accuracy
2021-09-10 16:15:57 -05:00
Nicolas Remond
489519a982 Use Map.of() for statically defined map 2021-09-10 14:27:18 -05:00
Dambar Pun
a96865d0f5 Update RedisInputStream.java
Fix code style
2021-09-10 14:24:52 -05:00
Blake Irvin
12e11609a9 pin 3rd-party Actions dep by full SHA
This change follows GitHub's security-hardening guidance. By pinning to a full SHA, we reduce our exposure to supply-chain attacks where a malicious party could compromise the 3rd-party Actions repo, commit malicious code, and then mutate an existing git tag to redirect to a SHA containing the malicious commit.

See https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions for more.
2021-09-10 14:22:11 -05:00
F2theK
5b404095b0 Added missing config entries
Starting server with sample.yml throws errors because of missing elements in config - not just empty ones
2021-09-10 14:19:10 -05:00
Chris Eager
6a6555e2d5 Add metrics for AuthEnablementRequestEventListener displacements 2021-09-10 12:01:05 -07:00
Chris Eager
49489a6021 Re-check mismatched accounts after a delay, to avoid false positives from concurrent requests 2021-09-10 11:31:44 -07:00
Chris Eager
8cd93d68e4 Add MetricsUtil 2021-09-10 11:31:44 -07:00
Chris Eager
f3b9a8d97f Add account to migration retry table on transient dynamo failure 2021-09-10 11:30:49 -07:00
Chris Eager
b91a69d8b3 Add asynchronous chunk pre-read to AccountDatabaseCrawler 2021-09-10 11:14:11 -07:00
Chris Eager
624e40e3b7 Add separate AccountsDatabaseCrawler for DynamoDB migration 2021-09-10 11:14:11 -07:00
Chris Eager
23a076a204 Update Account#getNextDeviceId to not reuse disable device’s IDs 2021-09-10 10:48:48 -07:00
Chris Eager
016141a05d Add DevicesHelper 2021-09-10 10:48:48 -07:00
Graeme Connell
a064b25a14 Fix CDS writer to use AccountsManager. 2021-09-10 11:36:06 -06:00
Ehren Kret
bd40e32f3b Send acceptable languages instead of request into the profile badge converter 2021-09-10 10:53:04 -05:00
Ehren Kret
81a21c0d5f Use @NotNull since @NotEmpty doesn't support URL 2021-09-10 10:49:31 -05:00
Ehren Kret
6478210330 Update configuration for badges to use URL instead of String 2021-09-10 10:49:31 -05:00
Ehren Kret
aa1c37fe26 Create configuration for badges 2021-09-10 10:14:16 -05:00
Ehren Kret
6ee23b0186 Create resource bundle for badges 2021-09-10 10:11:56 -05:00
Jon Chambers
40eb445592 Add a command to set a user's discoverability in CDS 2021-09-10 10:34:20 -04:00
Jon Chambers
ce7d687205 Add a shutdown monitor that publishes shutdown state as a metric 2021-09-08 16:37:05 -04:00
Chris Eager
758900b7a8 Register AuthEnablementApplicationEventListener 2021-09-08 13:11:09 -07:00
Chris Eager
539b62a829 Add request event listener that handles device.isEnabled changes 2021-09-08 13:11:09 -07:00
Jon Chambers
2866f1b213 Include e164 in account creation (whoami) responses 2021-09-07 16:52:32 -04:00
Ehren Kret
fc1465c05d Wire up stored account badges to the profile endpoints 2021-09-07 15:51:29 -05:00
Ehren Kret
bc887ec6fa Add visibility flag to badge storage 2021-09-07 15:50:29 -05:00
Ehren Kret
84b3d324bb Creates a storage object for badges 2021-09-07 15:49:41 -05:00
Ehren Kret
fc10108788 Make fields final in Badge entity 2021-09-07 15:39:48 -05:00
Ehren Kret
fbbc1bec58 Add badge entity to profile 2021-09-07 15:39:48 -05:00
Graeme Connell
2059bb5ef8 Update test to handle read-then-write in ContactDiscoveryWriter. 2021-09-07 13:41:47 -06:00
gram-signal
b080a5db4d Get-and-set accounts, since other updates may have made them stale.
Co-authored-by: Chris Eager <79161849+eager-signal@users.noreply.github.com>
2021-09-07 13:41:47 -06:00
Graeme Connell
b4aabd799b Canonical discoverability writing. 2021-09-07 13:41:47 -06:00
Jon Chambers
92f035bc2a Add a "change number" device/account capability 2021-09-07 15:07:30 -04:00
Chris Eager
18a6df34bd Add timers to processChunk and deleteRecentlyDeletedUuids 2021-09-03 14:54:51 -07:00
Chris Eager
b1274125c9 Add start/chunk/sleep logging to crawler 2021-09-03 14:54:51 -07:00
Chris Eager
dceebc1c8d Consistently use whenCompleteAsync(…, migrationThreadPool) 2021-09-03 14:02:51 -07:00
Chris Eager
6aadb4b458 Parameterize registration lock constructor when updating account attributes 2021-09-03 14:02:27 -07:00
Fedor Indutnyy
703405b874 Start WebSocket before registering its presence 2021-08-27 16:41:07 -04:00
Jon Chambers
d1735c7e57 Retire AmbiguousIdentifier 2021-08-27 13:40:46 -04:00
Jon Chambers
1f815b49dd Measure APNs rejections by reason 2021-08-27 11:52:29 -04:00
Jon Chambers
a9339b7037 Update to Pushy 0.15.0 2021-08-27 11:52:29 -04:00
Jon Chambers
f2c6ca182d Include the current server version in the tag list for Dropwizard metrics 2021-08-27 11:52:11 -04:00
Jon Chambers
b946c27a20 Remove a metric aggregator 2021-08-27 11:52:11 -04:00
Ehren Kret
9fd6358518 Add missing section to end of LICENSE file 2021-08-26 12:55:56 -05:00
Jon Chambers
8a8a848fac Record error metrics from Twilio Verify 2021-08-26 12:22:17 -04:00
Chris Eager
aeb9f67266 Migrate MessageSenderTest to JUnit 5 2021-08-25 12:25:10 -05:00
Chris Eager
e08c5a412e Insert ephemeral messages in the standard cache queue 2021-08-25 12:25:10 -05:00
Chris Eager
a7443a9ece Don’t persist ephemeral messages; clear ephemeral field when sending to clients 2021-08-25 11:17:00 -05:00
Chris Eager
54fe3b9a43 Update TextSecure.proto 2021-08-25 11:17:00 -05:00
Ehren Kret
ba522b1691 Clean redis message cache structure 2021-08-24 10:30:52 -05:00
Jon Chambers
739c5bf22c Add a counter to estimate announcement group adoption 2021-08-23 17:31:34 -04:00
Chris Eager
7cdadeb791 Register circuit breaker metrics for FaultTolerantPubSubConnection 2021-08-23 15:49:19 -05:00
Chris Eager
dadf43b93e Consolidate directory reconciliation on v3 endpoints 2021-08-19 14:18:38 -05:00
Chris Eager
bd820e6d2e Migrate websocket-resources test to JUnit 5 and .editorconfig 2021-08-19 14:09:57 -05:00
Chris Eager
19f7b207b7 Extract configuration for WebSocket max message sizes 2021-08-19 14:09:57 -05:00
Chris Eager
a398e2269c Update AccountsManager mismatch comparison 2021-08-19 14:08:48 -05:00
Chris Eager
2e28fb97a4 Delete DynamoDB accounts with invalid UUIDs in AccountsManager#create 2021-08-19 14:05:21 -05:00
Chris Eager
5c68d83a93 Add integration test for re-registration with and without Dynamo DB 2021-08-19 14:05:21 -05:00
Chris Eager
0b7c3ad745 .editorconfig formatting 2021-08-16 16:32:26 -05:00
Chris Eager
0cde06557d Catch and log unexpected exceptions keyspace notification executor service 2021-08-16 16:32:26 -05:00
Chris Eager
27844fe692 Add JUnit 5 RedisClusterExtension 2021-08-13 12:07:04 -05:00
Ehren Kret
779051ef9f Add minThreads(64) to multiRecipientMessageExecutor 2021-08-12 13:03:40 -05:00
Ehren Kret
d13741fbd5 Change from using parallel streams to using an ExecutorService 2021-08-12 12:05:01 -05:00
Ehren Kret
f7f870fe62 Execute send multi-recipient message loop in parallel 2021-08-12 12:05:01 -05:00
Chris Eager
de59aa099d Add uncaught exception handler 2021-08-12 11:10:05 -05:00
Ehren Kret
57a478b898 Remove unused local variable 2021-08-12 10:26:23 -05:00
Ehren Kret
3e8d79e147 Remove unused lua script to delete by sender and timestamp 2021-08-11 17:38:55 -05:00
Ehren Kret
a46045d987 Remove unused methods that delete messages by sender and timestamp 2021-08-11 17:30:39 -05:00
Ehren Kret
662c905b80 Remove deprecated delete messages endpoint
DELETE /v1/messages/{source}/{timestamp} has been deprecated a long
time and has minimal usage each day at this point. Dropping support
for this endpoint to improve message cache storage flexibility.
2021-08-11 16:17:44 -05:00
Chris Eager
31022aeb79 Use refreshing AuthenticatedAccount for @Auth 2021-08-11 14:52:25 -05:00
Chris Eager
b3e6a50dee Send 508 status code for legacy clients that produce rate limit challenges 2021-08-11 11:57:30 -05:00
Chris Eager
d29764d11f Only process updates for enabled devices in PushFeedbackProcessor 2021-08-11 11:54:42 -05:00
Chris Eager
f8e4f6727a Reorder crawler listeners so updates happen after read-only processing 2021-08-11 11:54:20 -05:00
Ehren Kret
63d05df8a3 Fix indentation 2021-08-10 10:02:04 -05:00
Ehren Kret
52d13d1d62 Remove unused lua script 2021-08-10 10:02:04 -05:00
Ehren Kret
f58a320223 Remove unused method from MessagesCache 2021-08-10 10:02:04 -05:00
Chris Eager
3e01bc1174 Add metric for content-length header distribution 2021-08-06 14:41:16 -05:00
Jon Chambers
d1ada7f998 Revert "Continue to verify rate limiters by e164 during UUID migration period"
This reverts commit ce5edbb7fc.
2021-08-06 14:33:59 -05:00
Chris Eager
095fc8140e Increase from default binary message size 2021-08-06 12:56:34 -05:00
Jon Chambers
73c368ea86 Use UUIDs instead of e164s to associate accounts with push notifications. 2021-08-04 14:38:28 -04:00
Jon Chambers
ce5edbb7fc Continue to verify rate limiters by e164 during UUID migration period 2021-08-04 14:15:21 -04:00
Jon Chambers
a680639718 Use UUIDs as rate limiter keys. 2021-08-04 14:15:21 -04:00
Ehren Kret
becf6afbdd Block until all UUID bytes are read or EOF 2021-08-03 17:59:48 -05:00
Ehren Kret
1dda015c6a Update multi-recipient message sending to handle unrestricted destinations 2021-08-03 17:31:39 -05:00
Chris Eager
a0427ecf8c Update s3-upload-maven-plugin to 1.6-SNAPSHOT 2021-08-03 11:04:29 -05:00
Chris Eager
cfd31e98ff Move version comparison to after more meaningful checks 2021-08-03 11:03:41 -05:00
Jon Chambers
bcb89924b4 Simplify optimistic write logic 2021-08-03 11:54:26 -04:00
Ehren Kret
23f9199439 Fix dependency resolution error for commons-logging 2021-08-02 13:14:44 -05:00
Ehren Kret
1f6318a919 Rename constant 2021-08-02 13:14:44 -05:00
Ehren Kret
b0667b258b Implement EnterpriseRecaptchaClient 2021-08-02 13:14:44 -05:00
Ehren Kret
4c3a48f5be Use more specific prefix for recaptcha transition 2021-08-02 13:14:44 -05:00
Ehren Kret
33fb7a72de Use RecaptchaClient interface 2021-08-02 13:14:44 -05:00
Ehren Kret
2c808e369c Create a transitional recaptcha client for upgrading 2021-08-02 13:14:44 -05:00
Ehren Kret
906d0be382 Setup recaptcha client interface 2021-08-02 13:14:44 -05:00
Ehren Kret
1c9a3c6105 Bringing in Google Cloud Recaptcha Enterprise libraries 2021-08-02 13:14:44 -05:00
Ehren Kret
2aaddd721f Rename existing captcha client 2021-08-02 13:14:44 -05:00
Jon Chambers
4e2284b83f Retire old GV2 adoption metrics. 2021-08-02 12:51:49 -05:00
Chris Eager
d5d9978e48 Use non-stale account in mutator when adding a new device 2021-08-02 11:38:03 -05:00
Chris Eager
d45659ac76 Reduce contention when updating device.lastSeen 2021-08-02 11:26:15 -05:00
Jon Chambers
13a07dc6cd Drop the active user counter. 2021-07-29 15:40:27 -04:00
Chris Eager
51b7a8d868 Add excluded E164s configuration to pre-registration experiment 2021-07-29 14:16:40 -05:00
Chris Eager
df9c0051c9 Reconcile inactive and undiscoverable accounts when using v3 endpoints 2021-07-29 10:56:44 -05:00
Jon Chambers
331ff83cd5 Drop legacy PIN-based registration lock plumbing 2021-07-29 11:51:14 -04:00
Jon Chambers
44838d6238 Verify that nobody's addressing API calls by e164 any more. 2021-07-29 11:50:36 -04:00
Chris Eager
5400abb065 Better support unhandled exception logging on websocket requests 2021-07-28 14:06:09 -05:00
Jon Chambers
f47fefb73e Lock accounts for the duration of deletion operations. 2021-07-27 13:12:39 -04:00
Jon Chambers
cdef745a7a Drop a not-very-helpful metric (logging works better in this case). 2021-07-27 13:12:39 -04:00
Jon Chambers
1a1eab4ec0 Also clear profiles on re-registration. 2021-07-27 13:05:54 -04:00
Jon Chambers
3a966ef345 Reuse account UUIDs when registering an account with a recently-deleted e164. 2021-07-27 13:05:54 -04:00
Jon Chambers
be20c04cd8 Identify accounts for which to delete keys by UUID. 2021-07-27 13:05:54 -04:00
Jon Chambers
d09dcc90fe Add methods for getting, clearing, locking recently-deleted account records. 2021-07-27 13:05:54 -04:00
Sophiah Ho
1fd1207bf6 Prevent unit tests from failing for machines with a non-US default Locale 2021-07-27 13:01:48 -04:00
Jon Chambers
0117fc12c7 Actually increment the moved "new user" counter. 2021-07-27 12:09:51 -04:00
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
451 changed files with 26752 additions and 20440 deletions

View File

@@ -9,16 +9,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
uses: actions/setup-java@3bc31aaf88e8fc94dc1e632d48af61be5ca8721c
with:
distribution: 'adopt'
java-version: 11
- name: Cache local Maven repository
uses: actions/cache@v2
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
distribution: 'temurin'
java-version: 17
cache: 'maven'
- name: Build with Maven
run: mvn -e -B verify

5
.gitignore vendored
View File

@@ -21,3 +21,8 @@ put.sh
deployer-staging.properties
deployer-production.properties
deployer.log
/service/src/main/resources/org/signal/badges/Badges_*.properties
!/service/src/main/resources/org/signal/badges/Badges_en.properties
/service/src/main/resources/org/signal/subscriptions/Subscriptions_*.properties
!/service/src/main/resources/org/signal/subscriptions/Subscriptions_en.properties
/.tx/config

11
.gitmodules vendored Normal file
View File

@@ -0,0 +1,11 @@
# Note that the implementation of the abusive message filter is private; internal
# developers will need to override this URL with:
#
# ```
# git config submodule.abusive-message-filter.url PRIVATE_URL
# ```
#
# External developers may safely ignore this submodule.
[submodule "abusive-message-filter"]
path = abusive-message-filter
url = REDACTED

44
LICENSE
View File

@@ -615,3 +615,47 @@ reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

110
pom.xml
View File

@@ -21,6 +21,19 @@
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>ossrh-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<modules>
<module>redis-dispatch</module>
<module>websocket-resources</module>
@@ -34,24 +47,27 @@
<commons-codec.version>1.15</commons-codec.version>
<commons-csv.version>1.8</commons-csv.version>
<commons-io.version>2.9.0</commons-io.version>
<dropwizard.version>2.0.22</dropwizard.version>
<dropwizard.version>2.0.26</dropwizard.version>
<dropwizard-metrics-datadog.version>1.1.13</dropwizard-metrics-datadog.version>
<gson.version>2.8.8</gson.version>
<guava.version>30.1.1-jre</guava.version>
<jaxb.version>2.3.1</jaxb.version>
<jedis.version>2.9.0</jedis.version>
<lettuce.version>6.0.4.RELEASE</lettuce.version>
<libphonenumber.version>8.12.23</libphonenumber.version>
<libphonenumber.version>8.12.33</libphonenumber.version>
<logstash.logback.version>6.6</logstash.logback.version>
<micrometer.version>1.5.3</micrometer.version>
<mockito.version>2.25.1</mockito.version>
<mockito.version>4.0.0</mockito.version>
<netty.version>4.1.65.Final</netty.version>
<netty.tcnative-boringssl-static.version>2.0.39.Final</netty.tcnative-boringssl-static.version>
<opentest4j.version>1.2.0</opentest4j.version>
<postgresql.version>9.4-1201-jdbc41</postgresql.version>
<protobuf.version>3.17.1</protobuf.version>
<pushy.version>0.14.2</pushy.version>
<pushy.version>0.15.0</pushy.version>
<resilience4j.version>1.5.0</resilience4j.version>
<semver4j.version>3.1.0</semver4j.version>
<slf4j.version>1.7.30</slf4j.version>
<stripe.version>20.79.0</stripe.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@@ -90,6 +106,13 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>20.9.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bom</artifactId>
@@ -171,6 +194,11 @@
<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>
@@ -217,6 +245,47 @@
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
<version>${stripe.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<dependency>
<groupId>org.signal</groupId>
<artifactId>embedded-redis</artifactId>
<version>0.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.zonky.test.postgres</groupId>
<artifactId>embedded-postgres-binaries-bom</artifactId>
<version>11.13.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-bom</artifactId>
<version>2.16.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
@@ -230,7 +299,7 @@
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>2.27.2</version>
<version>2.32.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
@@ -262,6 +331,29 @@
</dependencies>
<profiles>
<profile>
<id>include-abusive-message-filter</id>
<activation>
<file>
<exists>abusive-message-filter/pom.xml</exists>
</file>
</activation>
<modules>
<module>abusive-message-filter</module>
</modules>
</profile>
<profile>
<id>exclude-abusive-message-filter</id>
<activation>
<file>
<missing>abusive-message-filter/pom.xml</missing>
</file>
</activation>
</profile>
</profiles>
<build>
<extensions>
<extension>
@@ -277,7 +369,8 @@
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.17.2:exe:${os.detected.classifier}</protocArtifact>
<protocArtifact>com.google.protobuf:protoc:3.18.0:exe:${os.detected.classifier}</protocArtifact>
<checkStaleness>true</checkStaleness>
</configuration>
<executions>
<execution>
@@ -294,8 +387,7 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
<release>17</release>
</configuration>
</plugin>
@@ -359,7 +451,7 @@
<rules>
<dependencyConvergence/>
<requireMavenVersion>
<version>3.0.0</version>
<version>3.8.3</version>
</requireMavenVersion>
</rules>
</configuration>

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ public class RedisInputStream {
}
public String readLine() throws IOException {
ByteArrayOutputStream boas = new ByteArrayOutputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
boolean foundCr = false;
@@ -31,14 +31,14 @@ public class RedisInputStream {
throw new IOException("Stream closed!");
}
boas.write(character);
baos.write(character);
if (foundCr && character == LF) break;
else if (character == CR) foundCr = true;
else if (foundCr) foundCr = false;
}
byte[] data = boas.toByteArray();
byte[] data = baos.toByteArray();
return new String(data, 0, data.length-2);
}

View File

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

View File

@@ -1,114 +1,316 @@
# Example, relatively minimal, configuration that passes validation (see `io.dropwizard.cli.CheckCommand`)
#
# `unset` values will need to be set to work properly.
# Most other values are technically valid for a local/demonstration environment, but are probably not production-ready.
stripe:
apiKey: unset
idempotencyKeyGenerator: abcdefg12345678= # base64 for creating request idempotency hash
boostDescription: >
Example
dynamoDbClientConfiguration:
region: us-west-2 # AWS Region
dynamoDbTables:
accounts:
tableName: Example_Accounts
phoneNumberTableName: Example_Accounts_PhoneNumbers
phoneNumberIdentifierTableName: Example_Accounts_PhoneNumberIdentifiers
usernamesTableName: Example_Accounts_Usernames
scanPageSize: 100
deletedAccounts:
tableName: Example_DeletedAccounts
needsReconciliationIndexName: NeedsReconciliation
deletedAccountsLock:
tableName: Example_DeletedAccountsLock
issuedReceipts:
tableName: Example_IssuedReceipts
expiration: P30D # Duration of time until rows expire
generator: abcdefg12345678= # random base64-encoded binary sequence
keys:
tableName: Example_Keys
messages:
tableName: Example_Messages
expiration: P30D # Duration of time until rows expire
pendingAccounts:
tableName: Example_PendingAccounts
pendingDevices:
tableName: Example_PendingDevices
phoneNumberIdentifiers:
tableName: Example_PhoneNumberIdentifiers
profiles:
tableName: Example_Profiles
pushChallenge:
tableName: Example_PushChallenge
redeemedReceipts:
tableName: Example_RedeemedReceipts
expiration: P30D # Duration of time until rows expire
remoteConfig:
tableName: Example_RemoteConfig
reportMessage:
tableName: Example_ReportMessage
reservedUsernames:
tableName: Example_ReservedUsernames
subscriptions:
tableName: Example_Subscriptions
twilio: # Twilio gateway configuration
accountId:
accountToken:
nanpaMessagingServiceSid: # Twilio SID for the messaging service to use for NANPA.
messagingServiceSid: # Twilio SID for the message service to use for non-NANPA.
verifyServiceSid: # Twilio SID for a Verify service
localDomain: # Domain Twilio can connect back to for calls. Should be domain of your service.
accountId: unset
accountToken: unset
nanpaMessagingServiceSid: unset # Twilio SID for the messaging service to use for NANPA.
messagingServiceSid: unset # Twilio SID for the message service to use for non-NANPA.
verifyServiceSid: unset # Twilio SID for a Verify service
localDomain: example.com # Domain Twilio can connect back to for calls. Should be domain of your service.
defaultClientVerificationTexts:
ios: # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1.
androidNg: # Text to use for the verification message on android-ng client types. Will be passed to String.format with the verification code as argument 1.
android202001: # Text to use for the verification message on android-2020-01 client types. Will be passed to String.format with the verification code as argument 1.
android202103: # Text to use for the verification message on android-2021-03 client types. Will be passed to String.format with the verification code as argument 1.
generic: # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1.
ios: example %1$s # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1.
androidNg: example %1$s # Text to use for the verification message on android-ng client types. Will be passed to String.format with the verification code as argument 1.
android202001: example %1$s # Text to use for the verification message on android-2020-01 client types. Will be passed to String.format with the verification code as argument 1.
android202103: example %1$s # Text to use for the verification message on android-2021-03 client types. Will be passed to String.format with the verification code as argument 1.
generic: example %1$s # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1.
regionalClientVerificationTexts: # Map of country codes to custom texts
999: # example country code
ios:
# … all keys from defaultClientVerificationTexts are required
androidAppHash: # Hash appended to Android
verifyServiceFriendlyName: # Service name used in template. Requires Twilio account rep to enable
ios: example %1$s # all keys from defaultClientVerificationTexts are required
androidNg: example %1$s
android202001: example %1$s
android202103: example %1$s
generic: example %1$s
androidAppHash: example # Hash appended to Android
verifyServiceFriendlyName: example # Service name used in template. Requires Twilio account rep to enable
push:
queueSize: # Size of push pending queue
redphone:
authKey: # Deprecated
queueSize: 1000 # Size of push pending queue
turn: # TURN server configuration
secret: # TURN server secret
secret: example # TURN server secret
uris:
- stun:yourdomain:80
- stun:yourdomain.com:443
- turn:yourdomain:443?transport=udp
- turn:etc.com:80?transport=udp
- stun:example.com:80
- stun:another.example.com:443
- turn:example.com:443?transport=udp
- turn:ya.example.com:80?transport=udp
cacheCluster: # Redis server configuration for cache cluster
urls:
configurationUri: redis://redis.example.com:6379/
clientPresenceCluster: # Redis server configuration for client presence cluster
configurationUri: 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
configurationUri: redis://redis.example.com:6379/
rateLimitersCluster: # Redis server configuration for rate limiters cluster
configurationUri: redis://redis.example.com:6379/
directory:
client: # Configuration for interfacing with Contact Discovery Service cluster
userAuthenticationTokenSharedSecret: # hex-encoded secret shared with CDS used to generate auth tokens for Signal users
userAuthenticationTokenUserIdSecret: # hex-encoded secret shared among Signal-Servers to obscure user phone numbers from CDS
userAuthenticationTokenSharedSecret: 00000f # hex-encoded secret shared with CDS used to generate auth tokens for Signal users
userAuthenticationTokenUserIdSecret: 00000f # hex-encoded secret shared among Signal-Servers to obscure user phone numbers from CDS
sqs:
accessKey: # AWS SQS accessKey
accessSecret: # AWS SQS accessSecret
queueUrl: # AWS SQS queue url
server:
replicationUrl: # CDS replication endpoint base url
replicationPassword: # CDS replication endpoint password
replicationCaCertificate: # CDS replication endpoint TLS certificate trust root
reconciliationChunkSize: # CDS reconciliation chunk size
reconciliationChunkIntervalMs: # CDS reconciliation chunk interval, in milliseconds
accessKey: test # AWS SQS accessKey
accessSecret: test # AWS SQS accessSecret
queueUrls: # AWS SQS queue urls
- https://sqs.example.com/directory.fifo
server: # One or more CDS servers
- replicationName: example # CDS replication name
replicationUrl: cds.example.com # CDS replication endpoint base url
replicationPassword: example # CDS replication endpoint password
replicationCaCertificate: | # CDS replication endpoint TLS certificate trust root
-----BEGIN CERTIFICATE-----
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
AAAAAAAAAAAAAAAAAAAA
-----END CERTIFICATE-----
directoryV2:
client: # Configuration for interfacing with Contact Discovery Service v2 cluster
userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth tokens for Signal users
messageCache: # Redis server configuration for message store cache
persistDelayMinutes:
persistDelayMinutes: 1
cluster:
urls:
- redis://redis.example.com:6379/
messageStore: # Postgresql database configuration for message store
driverClass: org.postgresql.Driver
user:
password:
url:
configurationUri: redis://redis.example.com:6379/
metricsCluster:
urls:
- redis://redis.example.com:6379/
configurationUri: redis://redis.example.com:6379/
awsAttachments: # AWS S3 configuration
accessKey:
accessSecret:
bucket:
region:
accessKey: test
accessSecret: test
bucket: aws-attachments
region: us-west-2
gcpAttachments: # GCP Storage configuration
domain:
email:
maxSizeInBytes:
domain: example.com
email: user@example.cocm
maxSizeInBytes: 1024
pathPrefix:
rsaSigningKey:
rsaSigningKey: |
-----BEGIN PRIVATE KEY-----
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
AAAAAAAA
-----END PRIVATE KEY-----
profiles: # AWS S3 configuration
accessKey:
accessSecret:
bucket:
region:
database: # Postgresql database configuration
abuseDatabase: # Postgresql database configuration
driverClass: org.postgresql.Driver
user:
password:
url:
user: example
password: password
url: jdbc:postgresql://example.com:5432/abusedb
accountDatabaseCrawler:
chunkSize: 10 # accounts per run
chunkIntervalMs: 60000 # time per run
apn: # Apple Push Notifications configuration
sandbox: true
bundleId:
keyId:
teamId:
signingKey:
bundleId: com.example.textsecuregcm
keyId: unset
teamId: unset
signingKey: |
-----BEGIN PRIVATE KEY-----
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
AAAAAAAA
-----END PRIVATE KEY-----
gcm: # GCM Configuration
senderId:
apiKey:
senderId: 123456789
apiKey: unset
micrometer: # Micrometer metrics config
- name: "example"
- uri: "https://metrics.example.com/"
- apiKey:
- accountId:
cdn:
accessKey: test # AWS Access Key ID
accessSecret: test # AWS Access Secret
bucket: cdn # S3 Bucket name
region: us-west-2 # AWS region
datadog:
apiKey: unset
environment: dev
unidentifiedDelivery:
certificate: ABCD1234
privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA
expiresDays: 7
voiceVerification:
url: https://cdn-ca.signal.org/verification/
locales:
- en
recaptcha:
secret: unset
recaptchaV2:
siteKey: unset
scoreFloor: 1.0
projectPath: projects/example
credentialConfigurationJson: "{ }" # service account configuration for backend authentication
storageService:
uri: storage.example.com
userAuthenticationTokenSharedSecret: 00000f
storageCaCertificate: |
-----BEGIN CERTIFICATE-----
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
AAAAAAAAAAAAAAAAAAAA
-----END CERTIFICATE-----
backupService:
uri: backup.example.com
userAuthenticationTokenSharedSecret: 00000f
backupCaCertificate: |
-----BEGIN CERTIFICATE-----
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
AAAAAAAAAAAAAAAAAAAA
-----END CERTIFICATE-----
zkConfig:
serverPublic: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
appConfig:
application: example
environment: example
configuration: example
remoteConfig:
authorizedTokens:
@@ -117,23 +319,64 @@ remoteConfig:
- # ...
- # Nth authorized token
globalConfig: # keys and values that are given to clients on GET /v1/config
EXAMPLE_KEY: VALUE
paymentService:
userAuthenticationTokenSharedSecret: # hex-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
paymentsService:
userAuthenticationTokenSharedSecret: 0000000f0000000f0000000f0000000f0000000f0000000f0000000f0000000f # hex-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
fixerApiKey: unset
paymentCurrencies:
# list of symbols for supported currencies
- MOB
donation:
uri: # value
apiKey: # value
uri: donation.example.com # value
supportedCurrencies:
- # 1st supported currency
- # 2nd supported currency
- # ...
- # Nth supported currency
circuitBreaker:
failureRateThreshold: # value
ringBufferSizeInHalfOpenState: # value
ringBufferSizeInClosedState: # value
waitDurationInOpenStateInSeconds: # value
retry:
maxAttempts: # value
waitDuration: # value
badges:
badges:
- id: TEST
category: other
sprites: # exactly 6
- sprite-1.png
- sprite-2.png
- sprite-3.png
- sprite-4.png
- sprite-5.png
- sprite-6.png
svg: example.svg
svgs:
- light: example-light.svg
dark: example-dark.svg
badgeIdsEnabledForAll:
- TEST
receiptLevels:
'1': TEST
subscription: # configuration for Stripe subscriptions
badgeGracePeriod: P15D
levels:
500:
badge: EXAMPLE
prices:
# list of ISO 4217 currency codes and amounts for the given badge level
xts:
amount: '10'
id: price_example # stripe ID
boost:
level: 1
expiration: P90D
badge: EXAMPLE
currencies:
# ISO 4217 currency codes and amounts in those currencies
xts:
- '1'
- '2'
- '4'
- '8'
- '20'
- '40'

View File

@@ -42,7 +42,7 @@
<dependency>
<groupId>org.signal</groupId>
<artifactId>zkgroup-java</artifactId>
<version>0.7.0</version>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.whispersystems</groupId>
@@ -218,15 +218,14 @@
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-wavefront</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-datadog</artifactId>
</dependency>
<dependency>
<groupId>org.coursera</groupId>
<artifactId>dropwizard-metrics-datadog</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
@@ -252,14 +251,26 @@
<artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sts</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sqs</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>appconfig</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-core</artifactId>
@@ -270,11 +281,14 @@
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sqs</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-appconfig</artifactId>
<artifactId>dynamodb-lock-client</artifactId>
<version>1.1.0</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
@@ -355,9 +369,9 @@
</dependency>
<dependency>
<groupId>com.opentable.components</groupId>
<artifactId>otj-pg-embedded</artifactId>
<version>0.13.3</version>
<groupId>io.zonky.test</groupId>
<artifactId>embedded-postgres</artifactId>
<version>1.3.1</version>
<scope>test</scope>
</dependency>
@@ -393,7 +407,6 @@
<dependency>
<groupId>org.signal</groupId>
<artifactId>embedded-redis</artifactId>
<version>0.8.1</version>
<scope>test</scope>
</dependency>
@@ -407,7 +420,7 @@
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>DynamoDBLocal</artifactId>
<version>1.13.6</version>
<version>1.16.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
@@ -417,6 +430,16 @@
</exclusions>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-recaptchaenterprise</artifactId>
</dependency>
<dependency>
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
</dependency>
<dependency>
<groupId>pl.pragmatists</groupId>
<artifactId>JUnitParams</artifactId>
@@ -425,103 +448,112 @@
</dependency>
</dependencies>
<profiles>
<profile>
<id>exclude-abusive-message-filter</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.whispersystems.textsecuregcm.WhisperServerService</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>properties-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<id>read-deploy-configuration</id>
<phase>deploy</phase>
<goals>
<goal>read-project-properties</goal>
</goals>
<configuration>
<files>${project.basedir}/config/deploy.properties</files>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.signal</groupId>
<artifactId>s3-upload-maven-plugin</artifactId>
<version>1.6-SNAPSHOT</version>
<configuration>
<source>${project.build.directory}/${project.build.finalName}-bin.tar.gz</source>
<bucketName>${deploy.bucketName}</bucketName>
<region>${deploy.bucketRegion}</region>
<destination>${project.build.finalName}-bin.tar.gz</destination>
</configuration>
<executions>
<execution>
<id>deploy-to-s3</id>
<phase>deploy</phase>
<goals>
<goal>s3-upload</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<build>
<finalName>${project.parent.artifactId}-${project.version}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.whispersystems.textsecuregcm.WhisperServerService</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>properties-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<id>read-deploy-configuration</id>
<phase>deploy</phase>
<goals>
<goal>read-project-properties</goal>
</goals>
<configuration>
<files>${project.basedir}/config/deploy.properties</files>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.bazaarvoice.maven.plugins</groupId>
<artifactId>s3-upload-maven-plugin</artifactId>
<version>1.5</version>
<configuration>
<source>${project.build.directory}/${project.build.finalName}-bin.tar.gz</source>
<bucketName>${deploy.bucketName}</bucketName>
<destination>${project.build.finalName}-bin.tar.gz</destination>
</configuration>
<executions>
<execution>
<id>deploy-to-s3</id>
<phase>deploy</phase>
<goals>
<goal>s3-upload</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>templating-maven-plugin</artifactId>
@@ -535,6 +567,40 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>check-all-service-config</id>
<phase>verify</phase>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>org.whispersystems.textsecuregcm.CheckServiceConfigurations</mainClass>
<classpathScope>test</classpathScope>
<arguments>
<argument>${project.basedir}/config</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.i18n;
import com.google.common.annotations.VisibleForTesting;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
public class HeaderControlledResourceBundleLookup {
private static final int MAX_LOCALES = 15;
private final ResourceBundleFactory resourceBundleFactory;
public HeaderControlledResourceBundleLookup() {
this(ResourceBundle::getBundle);
}
@VisibleForTesting
public HeaderControlledResourceBundleLookup(
@Nonnull final ResourceBundleFactory resourceBundleFactory) {
this.resourceBundleFactory = Objects.requireNonNull(resourceBundleFactory);
}
@Nonnull
private List<Locale> getAcceptableLocales(final List<Locale> acceptableLanguages) {
return acceptableLanguages.stream().limit(MAX_LOCALES).distinct().collect(Collectors.toList());
}
@Nonnull
public ResourceBundle getResourceBundle(final String baseName, final List<Locale> acceptableLocales) {
final List<Locale> deduplicatedLocales = getAcceptableLocales(acceptableLocales);
final Locale desiredLocale = deduplicatedLocales.isEmpty() ? Locale.getDefault() : deduplicatedLocales.get(0);
// define a control with a fallback order as specified in the header
Control control = new Control() {
@Override
public List<String> getFormats(final String baseName) {
Objects.requireNonNull(baseName);
return Control.FORMAT_PROPERTIES;
}
@Override
public Locale getFallbackLocale(final String baseName, final Locale locale) {
Objects.requireNonNull(baseName);
if (locale.equals(Locale.getDefault())) {
return null;
}
final int localeIndex = deduplicatedLocales.indexOf(locale);
if (localeIndex < 0 || localeIndex >= deduplicatedLocales.size() - 1) {
return Locale.getDefault();
}
// [0, deduplicatedLocales.size() - 2] is now the possible range for localeIndex
return deduplicatedLocales.get(localeIndex + 1);
}
};
return resourceBundleFactory.createBundle(baseName, desiredLocale, control);
}
}

View File

@@ -0,0 +1,13 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.i18n;
import java.util.Locale;
import java.util.ResourceBundle;
public interface ResourceBundleFactory {
ResourceBundle createBundle(String baseName, Locale locale, ResourceBundle.Control control);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm;
@@ -13,35 +13,39 @@ import java.util.List;
import java.util.Map;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.AbusiveMessageFilterConfiguration;
import org.whispersystems.textsecuregcm.configuration.AccountDatabaseCrawlerConfiguration;
import org.whispersystems.textsecuregcm.configuration.AccountsDatabaseConfiguration;
import org.whispersystems.textsecuregcm.configuration.AccountsDynamoDbConfiguration;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration;
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
import org.whispersystems.textsecuregcm.configuration.BoostConfiguration;
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration;
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryV2Configuration;
import org.whispersystems.textsecuregcm.configuration.DonationConfiguration;
import org.whispersystems.textsecuregcm.configuration.DynamoDbConfiguration;
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration;
import org.whispersystems.textsecuregcm.configuration.DynamoDbTables;
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguration;
import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
import org.whispersystems.textsecuregcm.configuration.MessageCacheConfiguration;
import org.whispersystems.textsecuregcm.configuration.MessageDynamoDbConfiguration;
import org.whispersystems.textsecuregcm.configuration.WavefrontConfiguration;
import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
import org.whispersystems.textsecuregcm.configuration.RecaptchaConfiguration;
import org.whispersystems.textsecuregcm.configuration.RecaptchaV2Configuration;
import org.whispersystems.textsecuregcm.configuration.RedisClusterConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
import org.whispersystems.textsecuregcm.configuration.RemoteConfigConfiguration;
import org.whispersystems.textsecuregcm.configuration.ReportMessageConfiguration;
import org.whispersystems.textsecuregcm.configuration.SecureBackupServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
import org.whispersystems.textsecuregcm.configuration.MonitoredS3ObjectConfiguration;
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
@@ -52,6 +56,21 @@ import org.whispersystems.websocket.configuration.WebSocketConfiguration;
/** @noinspection MismatchedQueryAndUpdateOfCollection, WeakerAccess */
public class WhisperServerConfiguration extends Configuration {
@NotNull
@Valid
@JsonProperty
private StripeConfiguration stripe;
@NotNull
@Valid
@JsonProperty
private DynamoDbClientConfiguration dynamoDbClientConfiguration;
@NotNull
@Valid
@JsonProperty
private DynamoDbTables dynamoDbTables;
@NotNull
@Valid
@JsonProperty
@@ -77,11 +96,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private CdnConfiguration cdn;
@NotNull
@Valid
@JsonProperty
private WavefrontConfiguration wavefront;
@NotNull
@Valid
@JsonProperty
@@ -107,6 +121,11 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private DirectoryConfiguration directory;
@NotNull
@Valid
@JsonProperty
private DirectoryV2Configuration directoryV2;
@NotNull
@Valid
@JsonProperty
@@ -132,41 +151,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private RedisClusterConfiguration clientPresenceCluster;
@Valid
@NotNull
@JsonProperty
private MessageDynamoDbConfiguration messageDynamoDb;
@Valid
@NotNull
@JsonProperty
private DynamoDbConfiguration keysDynamoDb;
@Valid
@NotNull
@JsonProperty
private AccountsDynamoDbConfiguration accountsDynamoDb;
@Valid
@NotNull
@JsonProperty
private DynamoDbConfiguration migrationDeletedAccountsDynamoDb;
@Valid
@NotNull
@JsonProperty
private DynamoDbConfiguration migrationRetryAccountsDynamoDb;
@Valid
@NotNull
@JsonProperty
private DynamoDbConfiguration pushChallengeDynamoDb;
@Valid
@NotNull
@JsonProperty
private DynamoDbConfiguration reportMessageDynamoDb;
@Valid
@NotNull
@JsonProperty
@@ -182,11 +166,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private List<MaxDeviceConfiguration> maxDevices = new LinkedList<>();
@Valid
@NotNull
@JsonProperty
private AccountsDatabaseConfiguration accountsDatabase;
@Valid
@NotNull
@JsonProperty
@@ -232,6 +211,11 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private RecaptchaConfiguration recaptcha;
@Valid
@NotNull
@JsonProperty
private RecaptchaV2Configuration recaptchaV2;
@Valid
@NotNull
@JsonProperty
@@ -262,27 +246,57 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private AppConfigConfiguration appConfig;
@Valid
@NotNull
@JsonProperty
private MonitoredS3ObjectConfiguration torExitNodeList;
@Valid
@NotNull
@JsonProperty
private MonitoredS3ObjectConfiguration asnTable;
@Valid
@NotNull
@JsonProperty
private DonationConfiguration donation;
@Valid
@NotNull
@JsonProperty
private BadgesConfiguration badges;
@Valid
@JsonProperty
@NotNull
private SubscriptionConfiguration subscription;
@Valid
@JsonProperty
@NotNull
private BoostConfiguration boost;
@Valid
@NotNull
@JsonProperty
private ReportMessageConfiguration reportMessage = new ReportMessageConfiguration();
@Valid
@JsonProperty
private AbusiveMessageFilterConfiguration abusiveMessageFilter;
private Map<String, String> transparentDataIndex = new HashMap<>();
public StripeConfiguration getStripe() {
return stripe;
}
public DynamoDbClientConfiguration getDynamoDbClientConfiguration() {
return dynamoDbClientConfiguration;
}
public DynamoDbTables getDynamoDbTables() {
return dynamoDbTables;
}
public RecaptchaConfiguration getRecaptchaConfiguration() {
return recaptcha;
}
public RecaptchaV2Configuration getRecaptchaV2Configuration() {
return recaptchaV2;
}
public VoiceVerificationConfiguration getVoiceVerificationConfiguration() {
return voiceVerification;
}
@@ -327,6 +341,10 @@ public class WhisperServerConfiguration extends Configuration {
return directory;
}
public DirectoryV2Configuration getDirectoryV2Configuration() {
return directoryV2;
}
public SecureStorageServiceConfiguration getSecureStorageServiceConfiguration() {
return storageService;
}
@@ -351,34 +369,10 @@ public class WhisperServerConfiguration extends Configuration {
return rateLimitersCluster;
}
public MessageDynamoDbConfiguration getMessageDynamoDbConfiguration() {
return messageDynamoDb;
}
public DynamoDbConfiguration getKeysDynamoDbConfiguration() {
return keysDynamoDb;
}
public AccountsDynamoDbConfiguration getAccountsDynamoDbConfiguration() {
return accountsDynamoDb;
}
public DynamoDbConfiguration getMigrationDeletedAccountsDynamoDbConfiguration() {
return migrationDeletedAccountsDynamoDb;
}
public DynamoDbConfiguration getMigrationRetryAccountsDynamoDbConfiguration() {
return migrationRetryAccountsDynamoDb;
}
public DatabaseConfiguration getAbuseDatabaseConfiguration() {
return abuseDatabase;
}
public AccountsDatabaseConfiguration getAccountsDatabaseConfiguration() {
return accountsDatabase;
}
public RateLimitsConfiguration getLimitsConfiguration() {
return limits;
}
@@ -399,10 +393,6 @@ public class WhisperServerConfiguration extends Configuration {
return cdn;
}
public WavefrontConfiguration getWavefrontConfiguration() {
return wavefront;
}
public DatadogConfiguration getDatadogConfiguration() {
return datadog;
}
@@ -457,23 +447,27 @@ public class WhisperServerConfiguration extends Configuration {
return appConfig;
}
public DynamoDbConfiguration getPushChallengeDynamoDbConfiguration() {
return pushChallengeDynamoDb;
}
public DynamoDbConfiguration getReportMessageDynamoDbConfiguration() {
return reportMessageDynamoDb;
}
public MonitoredS3ObjectConfiguration getTorExitNodeListConfiguration() {
return torExitNodeList;
}
public MonitoredS3ObjectConfiguration getAsnTableConfiguration() {
return asnTable;
}
public DonationConfiguration getDonationConfiguration() {
return donation;
}
public BadgesConfiguration getBadges() {
return badges;
}
public SubscriptionConfiguration getSubscription() {
return subscription;
}
public BoostConfiguration getBoost() {
return boost;
}
public ReportMessageConfiguration getReportMessageConfiguration() {
return reportMessage;
}
public AbusiveMessageFilterConfiguration getAbusiveMessageFilterConfiguration() {
return abusiveMessageFilter;
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.abuse;
import io.dropwizard.lifecycle.Managed;
import javax.ws.rs.container.ContainerRequestFilter;
import java.io.IOException;
/**
* An abusive message filter is a {@link ContainerRequestFilter} that filters requests to message-sending endpoints to
* detect and respond to patterns of abusive behavior.
* <p/>
* Abusive message filters are managed components that are generally loaded dynamically via a
* {@link java.util.ServiceLoader}. Their {@link #configure(String)} method will be called prior to be adding to the
* server's pool of {@link Managed} objects.
* <p/>
* Abusive message filters must be annotated with {@link FilterAbusiveMessages}, a name binding annotation that
* restricts the endpoints to which the filter may apply.
*/
public interface AbusiveMessageFilter extends ContainerRequestFilter, Managed {
/**
* Configures this abusive message filter. This method will be called before the filter is added to the server's pool
* of managed objects and before the server processes any requests.
*
* @param environmentName the name of the environment in which this filter is running (e.g. "staging" or "production")
* @throws IOException if the filter could not read its configuration source for any reason
*/
void configure(String environmentName) throws IOException;
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.abuse;
import javax.ws.rs.NameBinding;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* A name-binding annotation that associates {@link AbusiveMessageFilter}s with resource methods.
*/
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface FilterAbusiveMessages {
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.abuse;
import org.whispersystems.textsecuregcm.storage.Account;
import java.io.IOException;
public interface RateLimitChallengeListener {
void handleRateLimitChallengeAnswered(Account account);
/**
* Configures this rate limit challenge listener. This method will be called before the service begins processing any
* challenges.
*
* @param environmentName the name of the environment in which this listener is running (e.g. "staging" or "production")
* @throws IOException if the listener could not read its configuration source for any reason
*/
void configure(String environmentName) throws IOException;
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.abuse;
public enum RateLimitChallengeType {
PUSH_CHALLENGE,
RECAPTCHA
}

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
public interface AccountAndAuthenticatedDeviceHolder {
Account getAccount();
Device getAuthenticatedDevice();
}

View File

@@ -1,39 +1,38 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import io.micrometer.core.instrument.Metrics;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import java.util.Optional;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
import java.util.Optional;
import io.micrometer.core.instrument.Metrics;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import static com.codahale.metrics.MetricRegistry.name;
public class AccountAuthenticator extends BaseAccountAuthenticator implements Authenticator<BasicCredentials, Account> {
public class AccountAuthenticator extends BaseAccountAuthenticator implements
Authenticator<BasicCredentials, AuthenticatedAccount> {
private static final String AUTHENTICATION_COUNTER_NAME = name(AccountAuthenticator.class, "authenticate");
private static final String GV2_CAPABLE_TAG_NAME = "gv1Migration";
private static final String AUTHENTICATION_COUNTER_NAME = name(AccountAuthenticator.class, "authenticate");
public AccountAuthenticator(AccountsManager accountsManager) {
super(accountsManager);
}
@Override
public Optional<Account> authenticate(BasicCredentials basicCredentials) {
final Optional<Account> maybeAccount = super.authenticate(basicCredentials, true);
public Optional<AuthenticatedAccount> authenticate(BasicCredentials basicCredentials) {
final Optional<AuthenticatedAccount> maybeAuthenticatedAccount = super.authenticate(basicCredentials, true);
// TODO Remove this temporary counter when we can replace it with more generic feature adoption system
maybeAccount.ifPresent(account -> {
Metrics.counter(AUTHENTICATION_COUNTER_NAME, GV2_CAPABLE_TAG_NAME, String.valueOf(account.isGv1MigrationSupported())).increment();
});
// TODO Remove after announcement groups have launched
maybeAuthenticatedAccount.ifPresent(authenticatedAccount ->
Metrics.counter(AUTHENTICATION_COUNTER_NAME,
"supportsAnnouncementGroups",
String.valueOf(authenticatedAccount.getAccount().isAnnouncementGroupSupported()))
.increment());
return maybeAccount;
return maybeAuthenticatedAccount;
}
}

View File

@@ -1,45 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import java.util.UUID;
public class AmbiguousIdentifier {
private final UUID uuid;
private final String number;
public AmbiguousIdentifier(String target) {
if (target.startsWith("+")) {
this.uuid = null;
this.number = target;
} else {
this.uuid = UUID.fromString(target);
this.number = null;
}
}
public UUID getUuid() {
return uuid;
}
public String getNumber() {
return number;
}
public boolean hasUuid() {
return uuid != null;
}
public boolean hasNumber() {
return number != null;
}
@Override
public String toString() {
return hasUuid() ? uuid.toString() : number;
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import com.google.common.annotations.VisibleForTesting;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Pair;
/**
* This {@link WebsocketRefreshRequirementProvider} observes intra-request changes in {@link Account#isEnabled()} and
* {@link Device#isEnabled()}.
* <p>
* If a change in {@link Account#isEnabled()} or any associated {@link Device#isEnabled()} is observed, then any active
* WebSocket connections for the account must be closed in order for clients to get a refreshed
* {@link io.dropwizard.auth.Auth} object with a current device list.
*
* @see AuthenticatedAccount
* @see DisabledPermittedAuthenticatedAccount
*/
public class AuthEnablementRefreshRequirementProvider implements WebsocketRefreshRequirementProvider {
private final AccountsManager accountsManager;
private static final Logger logger = LoggerFactory.getLogger(AuthEnablementRefreshRequirementProvider.class);
private static final String ACCOUNT_UUID = AuthEnablementRefreshRequirementProvider.class.getName() + ".accountUuid";
private static final String DEVICES_ENABLED = AuthEnablementRefreshRequirementProvider.class.getName() + ".devicesEnabled";
public AuthEnablementRefreshRequirementProvider(final AccountsManager accountsManager) {
this.accountsManager = accountsManager;
}
@VisibleForTesting
static Map<Long, Boolean> buildDevicesEnabledMap(final Account account) {
return account.getDevices().stream().collect(Collectors.toMap(Device::getId, Device::isEnabled));
}
@Override
public void handleRequestFiltered(final RequestEvent requestEvent) {
if (requestEvent.getUriInfo().getMatchedResourceMethod().getInvocable().getHandlingMethod().getAnnotation(ChangesDeviceEnabledState.class) != null) {
// The authenticated principal, if any, will be available after filters have run.
// Now that the account is known, capture a snapshot of `isEnabled` for the account's devices before carrying out
// the requests business logic.
ContainerRequestUtil.getAuthenticatedAccount(requestEvent.getContainerRequest()).ifPresent(account ->
setAccount(requestEvent.getContainerRequest(), account));
}
}
public static void setAccount(final ContainerRequest containerRequest, final Account account) {
containerRequest.setProperty(ACCOUNT_UUID, account.getUuid());
containerRequest.setProperty(DEVICES_ENABLED, buildDevicesEnabledMap(account));
}
@Override
public List<Pair<UUID, Long>> handleRequestFinished(final RequestEvent requestEvent) {
// Now that the request is finished, check whether `isEnabled` changed for any of the devices. If the value did
// change or if a devices was added or removed, all devices must disconnect and reauthenticate.
if (requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED) != null) {
@SuppressWarnings("unchecked") final Map<Long, Boolean> initialDevicesEnabled =
(Map<Long, Boolean>) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED);
return accountsManager.getByAccountIdentifier((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
final Set<Long> deviceIdsToDisplace;
final Map<Long, Boolean> currentDevicesEnabled = buildDevicesEnabledMap(account);
if (!initialDevicesEnabled.equals(currentDevicesEnabled)) {
deviceIdsToDisplace = new HashSet<>(initialDevicesEnabled.keySet());
deviceIdsToDisplace.addAll(currentDevicesEnabled.keySet());
} else {
deviceIdsToDisplace = Collections.emptySet();
}
return deviceIdsToDisplace.stream()
.map(deviceId -> new Pair<>(account.getUuid(), deviceId))
.collect(Collectors.toList());
}).orElseGet(() -> {
logger.error("Request had account, but it is no longer present");
return Collections.emptyList();
});
} else
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import java.security.Principal;
import java.util.function.Supplier;
import javax.security.auth.Subject;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Pair;
public class AuthenticatedAccount implements Principal, AccountAndAuthenticatedDeviceHolder {
private final Supplier<Pair<Account, Device>> accountAndDevice;
public AuthenticatedAccount(final Supplier<Pair<Account, Device>> accountAndDevice) {
this.accountAndDevice = accountAndDevice;
}
@Override
public Account getAccount() {
return accountAndDevice.get().first();
}
@Override
public Device getAuthenticatedDevice() {
return accountAndDevice.get().second();
}
// Principal implementation
@Override
public String getName() {
return null;
}
@Override
public boolean implies(final Subject subject) {
return false;
}
}

View File

@@ -1,81 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
import java.util.Base64;
public class AuthorizationHeader {
private final AmbiguousIdentifier identifier;
private final long deviceId;
private final String password;
private AuthorizationHeader(AmbiguousIdentifier identifier, long deviceId, String password) {
this.identifier = identifier;
this.deviceId = deviceId;
this.password = password;
}
public static AuthorizationHeader fromUserAndPassword(String user, String password) throws InvalidAuthorizationHeaderException {
try {
String[] numberAndId = user.split("\\.");
return new AuthorizationHeader(new AmbiguousIdentifier(numberAndId[0]),
numberAndId.length > 1 ? Long.parseLong(numberAndId[1]) : 1,
password);
} catch (NumberFormatException nfe) {
throw new InvalidAuthorizationHeaderException(nfe);
}
}
public static AuthorizationHeader fromFullHeader(String header) throws InvalidAuthorizationHeaderException {
try {
if (header == null) {
throw new InvalidAuthorizationHeaderException("Null header");
}
String[] headerParts = header.split(" ");
if (headerParts == null || headerParts.length < 2) {
throw new InvalidAuthorizationHeaderException("Invalid authorization header: " + header);
}
if (!"Basic".equals(headerParts[0])) {
throw new InvalidAuthorizationHeaderException("Unsupported authorization method: " + headerParts[0]);
}
String concatenatedValues = new String(Base64.getDecoder().decode(headerParts[1]));
if (Util.isEmpty(concatenatedValues)) {
throw new InvalidAuthorizationHeaderException("Bad decoded value: " + concatenatedValues);
}
String[] credentialParts = concatenatedValues.split(":");
if (credentialParts == null || credentialParts.length < 2) {
throw new InvalidAuthorizationHeaderException("Badly formated credentials: " + concatenatedValues);
}
return fromUserAndPassword(credentialParts[0], credentialParts[1]);
} catch (IllegalArgumentException e) {
throw new InvalidAuthorizationHeaderException(e);
}
}
public AmbiguousIdentifier getIdentifier() {
return identifier;
}
public long getDeviceId() {
return deviceId;
}
public String getPassword() {
return password;
}
}

View File

@@ -1,49 +1,39 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import static com.codahale.metrics.MetricRegistry.name;
import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.auth.basic.BasicCredentials;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Metrics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import io.micrometer.core.instrument.Tags;
import java.time.Clock;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Optional;
import static com.codahale.metrics.MetricRegistry.name;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.RefreshingAccountAndDeviceSupplier;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.Util;
public class BaseAccountAuthenticator {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(), "authentication", "failed" ));
private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(), "authentication", "succeeded" ));
private final Meter noSuchAccountMeter = metricRegistry.meter(name(getClass(), "authentication", "noSuchAccount" ));
private final Meter noSuchDeviceMeter = metricRegistry.meter(name(getClass(), "authentication", "noSuchDevice" ));
private final Meter accountDisabledMeter = metricRegistry.meter(name(getClass(), "authentication", "accountDisabled"));
private final Meter deviceDisabledMeter = metricRegistry.meter(name(getClass(), "authentication", "deviceDisabled" ));
private final Meter invalidAuthHeaderMeter = metricRegistry.meter(name(getClass(), "authentication", "invalidHeader" ));
private final String daysSinceLastSeenDistributionName = name(getClass(), "authentication", "daysSinceLastSeen");
private static final String AUTHENTICATION_COUNTER_NAME = name(BaseAccountAuthenticator.class, "authentication");
private static final String AUTHENTICATION_SUCCEEDED_TAG_NAME = "succeeded";
private static final String AUTHENTICATION_FAILURE_REASON_TAG_NAME = "reason";
private static final String AUTHENTICATION_ENABLED_REQUIRED_TAG_NAME = "enabledRequired";
private static final String DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME = name(BaseAccountAuthenticator.class, "daysSinceLastSeen");
private static final String IS_PRIMARY_DEVICE_TAG = "isPrimary";
private final Logger logger = LoggerFactory.getLogger(BaseAccountAuthenticator.class);
private final AccountsManager accountsManager;
private final Clock clock;
@@ -57,65 +47,100 @@ public class BaseAccountAuthenticator {
this.clock = clock;
}
public Optional<Account> authenticate(BasicCredentials basicCredentials, boolean enabledRequired) {
try {
AuthorizationHeader authorizationHeader = AuthorizationHeader.fromUserAndPassword(basicCredentials.getUsername(), basicCredentials.getPassword());
Optional<Account> account = accountsManager.get(authorizationHeader.getIdentifier());
static Pair<String, Long> getIdentifierAndDeviceId(final String basicUsername) {
final String identifier;
final long deviceId;
if (!account.isPresent()) {
noSuchAccountMeter.mark();
final int deviceIdSeparatorIndex = basicUsername.indexOf('.');
if (deviceIdSeparatorIndex == -1) {
identifier = basicUsername;
deviceId = Device.MASTER_ID;
} else {
identifier = basicUsername.substring(0, deviceIdSeparatorIndex);
deviceId = Long.parseLong(basicUsername.substring(deviceIdSeparatorIndex + 1));
}
return new Pair<>(identifier, deviceId);
}
public Optional<AuthenticatedAccount> authenticate(BasicCredentials basicCredentials, boolean enabledRequired) {
boolean succeeded = false;
String failureReason = null;
try {
final UUID accountUuid;
final long deviceId;
{
final Pair<String, Long> identifierAndDeviceId = getIdentifierAndDeviceId(basicCredentials.getUsername());
accountUuid = UUID.fromString(identifierAndDeviceId.first());
deviceId = identifierAndDeviceId.second();
}
Optional<Account> account = accountsManager.getByAccountIdentifier(accountUuid);
if (account.isEmpty()) {
failureReason = "noSuchAccount";
return Optional.empty();
}
Optional<Device> device = account.get().getDevice(authorizationHeader.getDeviceId());
Optional<Device> device = account.get().getDevice(deviceId);
if (!device.isPresent()) {
noSuchDeviceMeter.mark();
if (device.isEmpty()) {
failureReason = "noSuchDevice";
return Optional.empty();
}
if (enabledRequired) {
if (!device.get().isEnabled()) {
deviceDisabledMeter.mark();
failureReason = "deviceDisabled";
return Optional.empty();
}
if (!account.get().isEnabled()) {
accountDisabledMeter.mark();
failureReason = "accountDisabled";
return Optional.empty();
}
}
if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
authenticationSucceededMeter.mark();
account.get().setAuthenticatedDevice(device.get());
updateLastSeen(account.get(), device.get());
return account;
succeeded = true;
final Account authenticatedAccount = updateLastSeen(account.get(), device.get());
return Optional.of(new AuthenticatedAccount(
new RefreshingAccountAndDeviceSupplier(authenticatedAccount, device.get().getId(), accountsManager)));
}
authenticationFailedMeter.mark();
return Optional.empty();
} catch (IllegalArgumentException | InvalidAuthorizationHeaderException iae) {
invalidAuthHeaderMeter.mark();
failureReason = "invalidHeader";
return Optional.empty();
} finally {
Tags tags = Tags.of(
AUTHENTICATION_SUCCEEDED_TAG_NAME, String.valueOf(succeeded),
AUTHENTICATION_ENABLED_REQUIRED_TAG_NAME, String.valueOf(enabledRequired));
if (StringUtils.isNotBlank(failureReason)) {
tags = tags.and(AUTHENTICATION_FAILURE_REASON_TAG_NAME, failureReason);
}
Metrics.counter(AUTHENTICATION_COUNTER_NAME, tags).increment();
}
}
@VisibleForTesting
public void updateLastSeen(Account account, Device device) {
public Account updateLastSeen(Account account, Device device) {
final long lastSeenOffsetSeconds = Math.abs(account.getUuid().getLeastSignificantBits()) % ChronoUnit.DAYS.getDuration().toSeconds();
final long todayInMillisWithOffset = Util.todayInMillisGivenOffsetFromNow(clock, Duration.ofSeconds(lastSeenOffsetSeconds).negated());
if (device.getLastSeen() < todayInMillisWithOffset) {
DistributionSummary.builder(daysSinceLastSeenDistributionName)
.tags(IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isMaster()))
.publishPercentileHistogram()
.register(Metrics.globalRegistry)
Metrics.summary(DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME, IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isMaster()))
.record(Duration.ofMillis(todayInMillisWithOffset - device.getLastSeen()).toDays());
device.setLastSeen(Util.todayInMillis(clock));
accountsManager.update(account);
return accountsManager.updateDeviceLastSeen(account, device, Util.todayInMillis(clock));
}
return account;
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import java.util.Base64;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Pair;
public class BasicAuthorizationHeader {
private final String username;
private final long deviceId;
private final String password;
private BasicAuthorizationHeader(final String username, final long deviceId, final String password) {
this.username = username;
this.deviceId = deviceId;
this.password = password;
}
public static BasicAuthorizationHeader fromString(final String header) throws InvalidAuthorizationHeaderException {
try {
if (StringUtils.isBlank(header)) {
throw new InvalidAuthorizationHeaderException("Blank header");
}
final int spaceIndex = header.indexOf(' ');
if (spaceIndex == -1) {
throw new InvalidAuthorizationHeaderException("Invalid authorization header: " + header);
}
final String authorizationType = header.substring(0, spaceIndex);
if (!"Basic".equals(authorizationType)) {
throw new InvalidAuthorizationHeaderException("Unsupported authorization method: " + authorizationType);
}
final String credentials;
try {
credentials = new String(Base64.getDecoder().decode(header.substring(spaceIndex + 1)));
} catch (final IndexOutOfBoundsException e) {
throw new InvalidAuthorizationHeaderException("Missing credentials");
}
if (StringUtils.isEmpty(credentials)) {
throw new InvalidAuthorizationHeaderException("Bad decoded value: " + credentials);
}
final int credentialSeparatorIndex = credentials.indexOf(':');
if (credentialSeparatorIndex == -1) {
throw new InvalidAuthorizationHeaderException("Badly-formatted credentials: " + credentials);
}
final String usernameComponent = credentials.substring(0, credentialSeparatorIndex);
final String username;
final long deviceId;
{
final Pair<String, Long> identifierAndDeviceId =
BaseAccountAuthenticator.getIdentifierAndDeviceId(usernameComponent);
username = identifierAndDeviceId.first();
deviceId = identifierAndDeviceId.second();
}
final String password = credentials.substring(credentialSeparatorIndex + 1);
if (StringUtils.isAnyBlank(username, password)) {
throw new InvalidAuthorizationHeaderException("Username or password were blank");
}
return new BasicAuthorizationHeader(username, deviceId, password);
} catch (final IllegalArgumentException | IndexOutOfBoundsException e) {
throw new InvalidAuthorizationHeaderException(e);
}
}
public String getUsername() {
return username;
}
public long getDeviceId() {
return deviceId;
}
public String getPassword() {
return password;
}
}

View File

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

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that an endpoint may change the "enabled" state of one or more devices associated with an account, and that
* any websockets associated with the account may need to be refreshed after a call to that endpoint.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ChangesDeviceEnabledState {
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import org.glassfish.jersey.server.ContainerRequest;
import org.whispersystems.textsecuregcm.storage.Account;
import javax.ws.rs.core.SecurityContext;
import java.util.Optional;
class ContainerRequestUtil {
static Optional<Account> getAuthenticatedAccount(final ContainerRequest request) {
return Optional.ofNullable(request.getSecurityContext())
.map(SecurityContext::getUserPrincipal)
.map(principal -> principal instanceof AccountAndAuthenticatedDeviceHolder
? ((AccountAndAuthenticatedDeviceHolder) principal).getAccount() : null);
}
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.storage.Account;
import javax.security.auth.Subject;
import java.security.Principal;
public class DisabledPermittedAccount implements Principal {
private final Account account;
public DisabledPermittedAccount(Account account) {
this.account = account;
}
public Account getAccount() {
return account;
}
// Principal implementation
@Override
public String getName() {
return null;
}
@Override
public boolean implies(Subject subject) {
return false;
}
}

View File

@@ -1,27 +1,25 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import java.util.Optional;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
import java.util.Optional;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
public class DisabledPermittedAccountAuthenticator extends BaseAccountAuthenticator implements Authenticator<BasicCredentials, DisabledPermittedAccount> {
public class DisabledPermittedAccountAuthenticator extends BaseAccountAuthenticator implements
Authenticator<BasicCredentials, DisabledPermittedAuthenticatedAccount> {
public DisabledPermittedAccountAuthenticator(AccountsManager accountsManager) {
super(accountsManager);
}
@Override
public Optional<DisabledPermittedAccount> authenticate(BasicCredentials credentials) {
Optional<Account> account = super.authenticate(credentials, false);
return account.map(DisabledPermittedAccount::new);
public Optional<DisabledPermittedAuthenticatedAccount> authenticate(BasicCredentials credentials) {
Optional<AuthenticatedAccount> account = super.authenticate(credentials, false);
return account.map(DisabledPermittedAuthenticatedAccount::new);
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import java.security.Principal;
import javax.security.auth.Subject;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
public class DisabledPermittedAuthenticatedAccount implements Principal, AccountAndAuthenticatedDeviceHolder {
private final AuthenticatedAccount authenticatedAccount;
public DisabledPermittedAuthenticatedAccount(final AuthenticatedAccount authenticatedAccount) {
this.authenticatedAccount = authenticatedAccount;
}
@Override
public Account getAccount() {
return authenticatedAccount.getAccount();
}
@Override
public Device getAuthenticatedDevice() {
return authenticatedAccount.getAuthenticatedDevice();
}
// Principal implementation
@Override
public String getName() {
return null;
}
@Override
public boolean implies(Subject subject) {
return false;
}
}

View File

@@ -1,97 +1,71 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Util;
import com.google.common.annotations.VisibleForTesting;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;
import org.apache.commons.codec.binary.Hex;
import org.whispersystems.textsecuregcm.util.Util;
public class ExternalServiceCredentialGenerator {
private final Logger logger = LoggerFactory.getLogger(ExternalServiceCredentialGenerator.class);
private final byte[] key;
private final byte[] userIdKey;
private final boolean usernameDerivation;
private final boolean prependUsername;
private final Clock clock;
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation) {
this.key = key;
this.userIdKey = userIdKey;
this.usernameDerivation = usernameDerivation;
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey) {
this(key, userIdKey, true, true);
}
public ExternalServiceCredentials generateFor(String number) {
Mac mac = getMacInstance();
String username = getUserId(number, mac, usernameDerivation);
long currentTimeSeconds = System.currentTimeMillis() / 1000;
String prefix = username + ":" + currentTimeSeconds;
String output = Hex.encodeHexString(Util.truncate(getHmac(key, prefix.getBytes(), mac), 10));
String token = prefix + ":" + output;
public ExternalServiceCredentialGenerator(byte[] key, boolean prependUsername) {
this(key, new byte[0], false, prependUsername);
}
@VisibleForTesting
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation) {
this(key, userIdKey, usernameDerivation, true);
}
private ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation,
boolean prependUsername) {
this(key, userIdKey, usernameDerivation, prependUsername, Clock.systemUTC());
}
@VisibleForTesting
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation,
boolean prependUsername, Clock clock) {
this.key = key;
this.userIdKey = userIdKey;
this.usernameDerivation = usernameDerivation;
this.prependUsername = prependUsername;
this.clock = clock;
}
public ExternalServiceCredentials generateFor(String identity) {
Mac mac = getMacInstance();
String username = getUserId(identity, mac, usernameDerivation);
long currentTimeSeconds = clock.millis() / 1000;
String prefix = username + ":" + currentTimeSeconds;
String output = Hex.encodeHexString(Util.truncate(getHmac(key, prefix.getBytes(), mac), 10));
String token = (prependUsername ? prefix : currentTimeSeconds) + ":" + output;
return new ExternalServiceCredentials(username, token);
}
public boolean isValid(String token, String number, long currentTimeMillis) {
String[] parts = token.split(":");
Mac mac = getMacInstance();
if (parts.length != 3) {
return false;
}
if (!getUserId(number, mac, usernameDerivation).equals(parts[0])) {
return false;
}
if (!isValidTime(parts[1], currentTimeMillis)) {
return false;
}
return isValidSignature(parts[0] + ":" + parts[1], parts[2], mac);
}
private String getUserId(String number, Mac mac, boolean usernameDerivation) {
if (usernameDerivation) return Hex.encodeHexString(Util.truncate(getHmac(userIdKey, number.getBytes(), mac), 10));
else return number;
}
private boolean isValidTime(String timeString, long currentTimeMillis) {
try {
long tokenTime = Long.parseLong(timeString);
long ourTime = TimeUnit.MILLISECONDS.toSeconds(currentTimeMillis);
return TimeUnit.SECONDS.toHours(Math.abs(ourTime - tokenTime)) < 24;
} catch (NumberFormatException e) {
logger.warn("Number Format", e);
return false;
}
}
private boolean isValidSignature(String prefix, String suffix, Mac mac) {
try {
byte[] ourSuffix = Util.truncate(getHmac(key, prefix.getBytes(), mac), 10);
byte[] theirSuffix = Hex.decodeHex(suffix.toCharArray());
return MessageDigest.isEqual(ourSuffix, theirSuffix);
} catch (DecoderException e) {
logger.warn("DirectoryCredentials", e);
return false;
}
}
private Mac getMacInstance() {
try {
return Mac.getInstance("HmacSHA256");

View File

@@ -5,12 +5,15 @@
package org.whispersystems.textsecuregcm.auth;
public class InvalidAuthorizationHeaderException extends Exception {
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response.Status;
public class InvalidAuthorizationHeaderException extends WebApplicationException {
public InvalidAuthorizationHeaderException(String s) {
super(s);
super(s, Status.UNAUTHORIZED);
}
public InvalidAuthorizationHeaderException(Exception e) {
super(e);
super(e, Status.UNAUTHORIZED);
}
}

View File

@@ -8,6 +8,8 @@ package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.security.MessageDigest;
@@ -36,9 +38,9 @@ public class OptionalAccess {
}
if (requestAccount.isPresent()) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
throw new NotFoundException();
} else {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
throw new NotAuthorizedException(Response.Status.UNAUTHORIZED);
}
}
} catch (NumberFormatException e) {
@@ -56,7 +58,7 @@ public class OptionalAccess {
//noinspection ConstantConditions
if (requestAccount.isPresent() && (targetAccount.isEmpty() || (targetAccount.isPresent() && !targetAccount.get().isEnabled()))) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
throw new NotFoundException();
}
if (accessKey.isPresent() && targetAccount.isPresent() && targetAccount.get().isEnabled() && targetAccount.get().isUnrestrictedUnidentifiedAccess()) {
@@ -72,7 +74,7 @@ public class OptionalAccess {
return;
}
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
throw new NotAuthorizedException(Response.Status.UNAUTHORIZED);
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.Pair;
public class PhoneNumberChangeRefreshRequirementProvider implements WebsocketRefreshRequirementProvider {
private static final String INITIAL_NUMBER_KEY =
PhoneNumberChangeRefreshRequirementProvider.class.getName() + ".initialNumber";
@Override
public void handleRequestFiltered(final RequestEvent requestEvent) {
ContainerRequestUtil.getAuthenticatedAccount(requestEvent.getContainerRequest())
.ifPresent(account -> requestEvent.getContainerRequest().setProperty(INITIAL_NUMBER_KEY, account.getNumber()));
}
@Override
public List<Pair<UUID, Long>> handleRequestFinished(final RequestEvent requestEvent) {
final String initialNumber = (String) requestEvent.getContainerRequest().getProperty(INITIAL_NUMBER_KEY);
if (initialNumber != null) {
final Optional<Account> maybeAuthenticatedAccount =
ContainerRequestUtil.getAuthenticatedAccount(requestEvent.getContainerRequest());
return maybeAuthenticatedAccount
.filter(account -> !initialNumber.equals(account.getNumber()))
.map(account -> account.getDevices().stream()
.map(device -> new Pair<>(account.getUuid(), device.getId()))
.collect(Collectors.toList()))
.orElse(Collections.emptyList());
} else {
return Collections.emptyList();
}
}
}

View File

@@ -9,7 +9,6 @@ import com.google.common.annotations.VisibleForTesting;
import org.whispersystems.textsecuregcm.util.Util;
import javax.annotation.Nullable;
import java.security.MessageDigest;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@@ -20,42 +19,33 @@ public class StoredRegistrationLock {
private final Optional<String> registrationLockSalt;
private final Optional<String> deprecatedPin;
private final long lastSeen;
public StoredRegistrationLock(Optional<String> registrationLock, Optional<String> registrationLockSalt, Optional<String> deprecatedPin, long lastSeen) {
public StoredRegistrationLock(Optional<String> registrationLock, Optional<String> registrationLockSalt, long lastSeen) {
this.registrationLock = registrationLock;
this.registrationLockSalt = registrationLockSalt;
this.deprecatedPin = deprecatedPin;
this.lastSeen = lastSeen;
}
public boolean requiresClientRegistrationLock() {
return ((registrationLock.isPresent() && registrationLockSalt.isPresent()) || deprecatedPin.isPresent()) && System.currentTimeMillis() - lastSeen < TimeUnit.DAYS.toMillis(7);
return registrationLock.isPresent() && registrationLockSalt.isPresent() && System.currentTimeMillis() - lastSeen < TimeUnit.DAYS.toMillis(7);
}
public boolean needsFailureCredentials() {
return registrationLock.isPresent() && registrationLockSalt.isPresent();
}
public boolean hasDeprecatedPin() {
return deprecatedPin.isPresent();
}
public long getTimeRemaining() {
return TimeUnit.DAYS.toMillis(7) - (System.currentTimeMillis() - lastSeen);
}
public boolean verify(@Nullable String clientRegistrationLock, @Nullable String clientDeprecatedPin) {
if (Util.isEmpty(clientRegistrationLock) && Util.isEmpty(clientDeprecatedPin)) {
public boolean verify(@Nullable String clientRegistrationLock) {
if (Util.isEmpty(clientRegistrationLock)) {
return false;
}
if (registrationLock.isPresent() && registrationLockSalt.isPresent() && !Util.isEmpty(clientRegistrationLock)) {
return new AuthenticationCredentials(registrationLock.get(), registrationLockSalt.get()).verify(clientRegistrationLock);
} else if (deprecatedPin.isPresent() && !Util.isEmpty(clientDeprecatedPin)) {
return MessageDigest.isEqual(deprecatedPin.get().getBytes(), clientDeprecatedPin.getBytes());
} else {
return false;
}
@@ -63,6 +53,6 @@ public class StoredRegistrationLock {
@VisibleForTesting
public StoredRegistrationLock forTime(long timestamp) {
return new StoredRegistrationLock(registrationLock, registrationLockSalt, deprecatedPin, timestamp);
return new StoredRegistrationLock(registrationLock, registrationLockSalt, timestamp);
}
}

View File

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

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import org.glassfish.jersey.server.monitoring.ApplicationEvent;
import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.server.monitoring.RequestEventListener;
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
/**
* Delegates request events to a listener that watches for intra-request changes that require websocket refreshes
*/
public class WebsocketRefreshApplicationEventListener implements ApplicationEventListener {
private final WebsocketRefreshRequestEventListener websocketRefreshRequestEventListener;
public WebsocketRefreshApplicationEventListener(final AccountsManager accountsManager,
final ClientPresenceManager clientPresenceManager) {
this.websocketRefreshRequestEventListener = new WebsocketRefreshRequestEventListener(clientPresenceManager,
new AuthEnablementRefreshRequirementProvider(accountsManager),
new PhoneNumberChangeRefreshRequirementProvider());
}
@Override
public void onEvent(final ApplicationEvent event) {
}
@Override
public RequestEventListener onRequest(final RequestEvent requestEvent) {
return websocketRefreshRequestEventListener;
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.server.monitoring.RequestEvent.Type;
import org.glassfish.jersey.server.monitoring.RequestEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
public class WebsocketRefreshRequestEventListener implements RequestEventListener {
private final ClientPresenceManager clientPresenceManager;
private final WebsocketRefreshRequirementProvider[] providers;
private static final Counter DISPLACED_ACCOUNTS = Metrics.counter(
name(WebsocketRefreshRequestEventListener.class, "displacedAccounts"));
private static final Counter DISPLACED_DEVICES = Metrics.counter(
name(WebsocketRefreshRequestEventListener.class, "displacedDevices"));
private static final Logger logger = LoggerFactory.getLogger(WebsocketRefreshRequestEventListener.class);
public WebsocketRefreshRequestEventListener(
final ClientPresenceManager clientPresenceManager,
final WebsocketRefreshRequirementProvider... providers) {
this.clientPresenceManager = clientPresenceManager;
this.providers = providers;
}
@Context
private ResourceInfo resourceInfo;
@Override
public void onEvent(final RequestEvent event) {
if (event.getType() == Type.REQUEST_FILTERED) {
for (final WebsocketRefreshRequirementProvider provider : providers) {
provider.handleRequestFiltered(event);
}
} else if (event.getType() == Type.FINISHED) {
final AtomicInteger displacedDevices = new AtomicInteger(0);
Arrays.stream(providers)
.flatMap(provider -> provider.handleRequestFinished(event).stream())
.distinct()
.forEach(pair -> {
try {
displacedDevices.incrementAndGet();
clientPresenceManager.disconnectPresence(pair.first(), pair.second());
} catch (final Exception e) {
logger.error("Could not displace device presence", e);
}
});
if (displacedDevices.get() > 0) {
DISPLACED_ACCOUNTS.increment();
DISPLACED_DEVICES.increment(displacedDevices.get());
}
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import java.util.List;
import java.util.UUID;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.whispersystems.textsecuregcm.util.Pair;
/**
* A websocket refresh requirement provider watches for intra-request changes (e.g. to authentication status) that
* require a websocket refresh.
*/
public interface WebsocketRefreshRequirementProvider {
/**
* Processes a request after filters have run and the request has been mapped to a destination controller.
*
* @param requestEvent the request event to observe
*/
void handleRequestFiltered(RequestEvent requestEvent);
/**
* Processes a request after all normal request handling has been completed.
*
* @param requestEvent the request event to observe
* @return a list of pairs of account UUID/device ID pairs identifying websockets that need to be refreshed as a
* result of the observed request
*/
List<Pair<UUID, Long>> handleRequestFinished(RequestEvent requestEvent);
}

View File

@@ -0,0 +1,14 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.badges;
import java.util.List;
import java.util.Locale;
import org.whispersystems.textsecuregcm.entities.Badge;
public interface BadgeTranslator {
Badge translate(List<Locale> acceptableLanguages, String badgeId);
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.badges;
import com.google.common.annotations.VisibleForTesting;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.signal.i18n.HeaderControlledResourceBundleLookup;
import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
import org.whispersystems.textsecuregcm.entities.Badge;
import org.whispersystems.textsecuregcm.entities.BadgeSvg;
import org.whispersystems.textsecuregcm.entities.SelfBadge;
import org.whispersystems.textsecuregcm.storage.AccountBadge;
public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter, BadgeTranslator {
@VisibleForTesting
static final String BASE_NAME = "org.signal.badges.Badges";
private final Clock clock;
private final Map<String, BadgeConfiguration> knownBadges;
private final List<String> badgeIdsEnabledForAll;
private final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup;
public ConfiguredProfileBadgeConverter(
final Clock clock,
final BadgesConfiguration badgesConfiguration,
final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup) {
this.clock = clock;
this.knownBadges = badgesConfiguration.getBadges().stream()
.collect(Collectors.toMap(BadgeConfiguration::getId, Function.identity()));
this.badgeIdsEnabledForAll = badgesConfiguration.getBadgeIdsEnabledForAll();
this.headerControlledResourceBundleLookup = headerControlledResourceBundleLookup;
}
@Override
public Badge translate(final List<Locale> acceptableLanguages, final String badgeId) {
final ResourceBundle resourceBundle = headerControlledResourceBundleLookup.getResourceBundle(BASE_NAME,
acceptableLanguages);
final BadgeConfiguration configuration = knownBadges.get(badgeId);
return newBadge(
false,
configuration.getId(),
configuration.getCategory(),
resourceBundle.getString(configuration.getId() + "_name"),
resourceBundle.getString(configuration.getId() + "_description"),
configuration.getSprites(),
configuration.getSvg(),
configuration.getSvgs(),
null,
false);
}
@Override
public List<Badge> convert(
final List<Locale> acceptableLanguages,
final List<AccountBadge> accountBadges,
final boolean isSelf) {
if (accountBadges.isEmpty() && badgeIdsEnabledForAll.isEmpty()) {
return List.of();
}
final Instant now = clock.instant();
final ResourceBundle resourceBundle = headerControlledResourceBundleLookup.getResourceBundle(BASE_NAME,
acceptableLanguages);
List<Badge> badges = accountBadges.stream()
.filter(accountBadge -> (isSelf || accountBadge.isVisible())
&& now.isBefore(accountBadge.getExpiration())
&& knownBadges.containsKey(accountBadge.getId()))
.map(accountBadge -> {
BadgeConfiguration configuration = knownBadges.get(accountBadge.getId());
return newBadge(
isSelf,
accountBadge.getId(),
configuration.getCategory(),
resourceBundle.getString(accountBadge.getId() + "_name"),
resourceBundle.getString(accountBadge.getId() + "_description"),
configuration.getSprites(),
configuration.getSvg(),
configuration.getSvgs(),
accountBadge.getExpiration(),
accountBadge.isVisible());
})
.collect(Collectors.toCollection(ArrayList::new));
badges.addAll(badgeIdsEnabledForAll.stream().filter(knownBadges::containsKey).map(id -> {
BadgeConfiguration configuration = knownBadges.get(id);
return newBadge(
isSelf,
id,
configuration.getCategory(),
resourceBundle.getString(id + "_name"),
resourceBundle.getString(id + "_description"),
configuration.getSprites(),
configuration.getSvg(),
configuration.getSvgs(),
now.plus(Duration.ofDays(1)),
true);
}).collect(Collectors.toList()));
return badges;
}
private Badge newBadge(
final boolean isSelf,
final String id,
final String category,
final String name,
final String description,
final List<String> sprites,
final String svg,
final List<BadgeSvg> svgs,
final Instant expiration,
final boolean visible) {
if (isSelf) {
return new SelfBadge(id, category, name, description, sprites, svg, svgs, expiration, visible);
} else {
return new Badge(id, category, name, description, sprites, svg, svgs);
}
}
}

View File

@@ -0,0 +1,13 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.badges;
import java.util.List;
import java.util.Locale;
public interface LevelTranslator {
String translate(List<Locale> acceptableLanguages, String badgeId);
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.badges;
import java.util.List;
import java.util.Locale;
import org.whispersystems.textsecuregcm.entities.Badge;
import org.whispersystems.textsecuregcm.storage.AccountBadge;
public interface ProfileBadgeConverter {
/**
* Converts the {@link AccountBadge}s for an account into the objects
* that can be returned on a profile fetch.
*/
List<Badge> convert(List<Locale> acceptableLanguages, List<AccountBadge> accountBadges, boolean isSelf);
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.badges;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.ResourceBundle;
import javax.annotation.Nonnull;
import org.signal.i18n.HeaderControlledResourceBundleLookup;
public class ResourceBundleLevelTranslator implements LevelTranslator {
private static final String BASE_NAME = "org.signal.subscriptions.Subscriptions";
private final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup;
public ResourceBundleLevelTranslator(
@Nonnull final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup) {
this.headerControlledResourceBundleLookup = Objects.requireNonNull(headerControlledResourceBundleLookup);
}
@Override
public String translate(final List<Locale> acceptableLanguages, final String badgeId) {
final ResourceBundle resourceBundle = headerControlledResourceBundleLookup.getResourceBundle(BASE_NAME,
acceptableLanguages);
return resourceBundle.getString(badgeId);
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotBlank;
public class AbusiveMessageFilterConfiguration {
@JsonProperty
@NotBlank
private final String environment;
@JsonCreator
public AbusiveMessageFilterConfiguration(@JsonProperty("environment") final String environment) {
this.environment = environment;
}
public String getEnvironment() {
return environment;
}
}

View File

@@ -1,23 +0,0 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
public class AccountsDatabaseConfiguration extends DatabaseConfiguration {
@JsonProperty
@NotNull
@Valid
private RetryConfiguration keyOperationRetry = new RetryConfiguration();
public RetryConfiguration getKeyOperationRetryConfiguration() {
return keyOperationRetry;
}
}

View File

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

View File

@@ -0,0 +1,49 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.configuration.DynamoDbTables.Table;
import javax.validation.constraints.NotBlank;
public class AccountsTableConfiguration extends Table {
private final String phoneNumberTableName;
private final String phoneNumberIdentifierTableName;
private final String usernamesTableName;
private final int scanPageSize;
@JsonCreator
public AccountsTableConfiguration(
@JsonProperty("tableName") final String tableName,
@JsonProperty("phoneNumberTableName") final String phoneNumberTableName,
@JsonProperty("phoneNumberIdentifierTableName") final String phoneNumberIdentifierTableName,
@JsonProperty("usernamesTableName") final String usernamesTableName,
@JsonProperty("scanPageSize") final int scanPageSize) {
super(tableName);
this.phoneNumberTableName = phoneNumberTableName;
this.phoneNumberIdentifierTableName = phoneNumberIdentifierTableName;
this.usernamesTableName = usernamesTableName;
this.scanPageSize = scanPageSize;
}
@NotBlank
public String getPhoneNumberTableName() {
return phoneNumberTableName;
}
@NotBlank
public String getPhoneNumberIdentifierTableName() {
return phoneNumberIdentifierTableName;
}
@NotBlank
public String getUsernamesTableName() {
return usernamesTableName;
}
public int getScanPageSize() {
return scanPageSize;
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.entities.BadgeSvg;
import org.whispersystems.textsecuregcm.util.ExactlySize;
public class BadgeConfiguration {
public static final String CATEGORY_TESTING = "testing";
private final String id;
private final String category;
private final List<String> sprites;
private final String svg;
private final List<BadgeSvg> svgs;
@JsonCreator
public BadgeConfiguration(
@JsonProperty("id") final String id,
@JsonProperty("category") final String category,
@JsonProperty("sprites") final List<String> sprites,
@JsonProperty("svg") final String svg,
@JsonProperty("svgs") final List<BadgeSvg> svgs) {
this.id = id;
this.category = category;
this.sprites = sprites;
this.svg = svg;
this.svgs = svgs;
}
@NotEmpty
public String getId() {
return id;
}
@NotEmpty
public String getCategory() {
return category;
}
@NotNull
@ExactlySize(6)
public List<String> getSprites() {
return sprites;
}
@NotEmpty
public String getSvg() {
return svg;
}
@NotNull
public List<BadgeSvg> getSvgs() {
return svgs;
}
public boolean isTestBadge() {
return CATEGORY_TESTING.equals(category);
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
import io.dropwizard.validation.ValidationMethod;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
public class BadgesConfiguration {
private final List<BadgeConfiguration> badges;
private final List<String> badgeIdsEnabledForAll;
private final Map<Long, String> receiptLevels;
@JsonCreator
public BadgesConfiguration(
@JsonProperty("badges") @JsonSetter(nulls = Nulls.AS_EMPTY) final List<BadgeConfiguration> badges,
@JsonProperty("badgeIdsEnabledForAll") @JsonSetter(nulls = Nulls.AS_EMPTY) final List<String> badgeIdsEnabledForAll,
@JsonProperty("receiptLevels") @JsonSetter(nulls = Nulls.AS_EMPTY) final Map<Long, String> receiptLevels) {
this.badges = Objects.requireNonNull(badges);
this.badgeIdsEnabledForAll = Objects.requireNonNull(badgeIdsEnabledForAll);
this.receiptLevels = Objects.requireNonNull(receiptLevels);
}
@Valid
@NotNull
public List<BadgeConfiguration> getBadges() {
return badges;
}
@Valid
@NotNull
public List<String> getBadgeIdsEnabledForAll() {
return badgeIdsEnabledForAll;
}
@Valid
@NotNull
public Map<Long, String> getReceiptLevels() {
return receiptLevels;
}
@JsonIgnore
@ValidationMethod(message = "contains receipt level mappings that are not configured badges")
public boolean isAllReceiptLevelsConfigured() {
final Set<String> badgeNames = badges.stream().map(BadgeConfiguration::getId).collect(Collectors.toSet());
return badgeNames.containsAll(receiptLevels.values());
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import javax.validation.Valid;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.util.ExactlySize;
public class BoostConfiguration {
private final long level;
private final Duration expiration;
private final Map<String, List<BigDecimal>> currencies;
private final String badge;
@JsonCreator
public BoostConfiguration(
@JsonProperty("level") long level,
@JsonProperty("expiration") Duration expiration,
@JsonProperty("currencies") Map<String, List<BigDecimal>> currencies,
@JsonProperty("badge") String badge) {
this.level = level;
this.expiration = expiration;
this.currencies = currencies;
this.badge = badge;
}
public long getLevel() {
return level;
}
@NotNull
public Duration getExpiration() {
return expiration;
}
@Valid
@NotNull
public Map<@NotEmpty String, @Valid @ExactlySize(6) List<@DecimalMin("0.01") @NotNull BigDecimal>> getCurrencies() {
return currencies;
}
@NotEmpty
public String getBadge() {
return badge;
}
}

View File

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

View File

@@ -6,11 +6,13 @@
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 {
public class DatadogConfiguration implements DatadogConfig {
@JsonProperty
@NotBlank
@@ -20,11 +22,40 @@ public class DatadogConfiguration {
@NotNull
private Duration step = Duration.ofSeconds(10);
public String getApiKey() {
@JsonProperty
@NotBlank
private String environment;
@JsonProperty
@Min(1)
private int batchSize = 5_000;
@Override
public String apiKey() {
return apiKey;
}
public Duration getStep() {
@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,25 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotBlank;
import org.whispersystems.textsecuregcm.configuration.DynamoDbTables.Table;
public class DeletedAccountsTableConfiguration extends Table {
private final String needsReconciliationIndexName;
@JsonCreator
public DeletedAccountsTableConfiguration(
@JsonProperty("tableName") final String tableName,
@JsonProperty("needsReconciliationIndexName") final String needsReconciliationIndexName) {
super(tableName);
this.needsReconciliationIndexName = needsReconciliationIndexName;
}
@NotBlank
public String getNeedsReconciliationIndexName() {
return needsReconciliationIndexName;
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.util.ExactlySize;
public class DirectoryV2ClientConfiguration {
private final byte[] userAuthenticationTokenSharedSecret;
@JsonCreator
public DirectoryV2ClientConfiguration(
@JsonProperty("userAuthenticationTokenSharedSecret") final byte[] userAuthenticationTokenSharedSecret) {
this.userAuthenticationTokenSharedSecret = userAuthenticationTokenSharedSecret;
}
@ExactlySize({32})
public byte[] getUserAuthenticationTokenSharedSecret() {
return userAuthenticationTokenSharedSecret;
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.Valid;
public class DirectoryV2Configuration {
private final DirectoryV2ClientConfiguration clientConfiguration;
@JsonCreator
public DirectoryV2Configuration(@JsonProperty("client") DirectoryV2ClientConfiguration clientConfiguration) {
this.clientConfiguration = clientConfiguration;
}
@Valid
public DirectoryV2ClientConfiguration getDirectoryV2ClientConfiguration() {
return clientConfiguration;
}
}

View File

@@ -15,7 +15,6 @@ import javax.validation.constraints.NotNull;
public class DonationConfiguration {
private String uri;
private String apiKey;
private String description;
private Set<String> supportedCurrencies;
private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
@@ -32,17 +31,6 @@ public class DonationConfiguration {
this.uri = uri;
}
@JsonProperty
@NotEmpty
public String getApiKey() {
return apiKey;
}
@VisibleForTesting
public void setApiKey(final String apiKey) {
this.apiKey = apiKey;
}
@JsonProperty
public String getDescription() {
return description;

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Duration;
import javax.validation.constraints.NotEmpty;
public class DynamoDbClientConfiguration {
private final String region;
private final Duration clientExecutionTimeout;
private final Duration clientRequestTimeout;
@JsonCreator
public DynamoDbClientConfiguration(
@JsonProperty("region") final String region,
@JsonProperty("clientExcecutionTimeout") final Duration clientExecutionTimeout,
@JsonProperty("clientRequestTimeout") final Duration clientRequestTimeout) {
this.region = region;
this.clientExecutionTimeout = clientExecutionTimeout != null ? clientExecutionTimeout : Duration.ofSeconds(30);
this.clientRequestTimeout = clientRequestTimeout != null ? clientRequestTimeout : Duration.ofSeconds(10);
}
@NotEmpty
public String getRegion() {
return region;
}
public Duration getClientExecutionTimeout() {
return clientExecutionTimeout;
}
public Duration getClientRequestTimeout() {
return clientRequestTimeout;
}
}

View File

@@ -1,44 +0,0 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import java.time.Duration;
public class DynamoDbConfiguration {
private String region;
private String tableName;
private Duration clientExecutionTimeout = Duration.ofSeconds(30);
private Duration clientRequestTimeout = Duration.ofSeconds(10);
@Valid
@NotEmpty
@JsonProperty
public String getRegion() {
return region;
}
@Valid
@NotEmpty
@JsonProperty
public String getTableName() {
return tableName;
}
@JsonProperty
public Duration getClientExecutionTimeout() {
return clientExecutionTimeout;
}
@JsonProperty
public Duration getClientRequestTimeout() {
return clientRequestTimeout;
}
}

View File

@@ -0,0 +1,197 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Duration;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class DynamoDbTables {
public static class Table {
private final String tableName;
@JsonCreator
public Table(
@JsonProperty("tableName") final String tableName) {
this.tableName = tableName;
}
@NotEmpty
public String getTableName() {
return tableName;
}
}
public static class TableWithExpiration extends Table {
private final Duration expiration;
@JsonCreator
public TableWithExpiration(
@JsonProperty("tableName") final String tableName,
@JsonProperty("expiration") final Duration expiration) {
super(tableName);
this.expiration = expiration;
}
@NotNull
public Duration getExpiration() {
return expiration;
}
}
private final AccountsTableConfiguration accounts;
private final DeletedAccountsTableConfiguration deletedAccounts;
private final Table deletedAccountsLock;
private final IssuedReceiptsTableConfiguration issuedReceipts;
private final Table keys;
private final TableWithExpiration messages;
private final Table pendingAccounts;
private final Table pendingDevices;
private final Table phoneNumberIdentifiers;
private final Table profiles;
private final Table pushChallenge;
private final TableWithExpiration redeemedReceipts;
private final Table remoteConfig;
private final Table reportMessage;
private final Table reservedUsernames;
private final Table subscriptions;
public DynamoDbTables(
@JsonProperty("accounts") final AccountsTableConfiguration accounts,
@JsonProperty("deletedAccounts") final DeletedAccountsTableConfiguration deletedAccounts,
@JsonProperty("deletedAccountsLock") final Table deletedAccountsLock,
@JsonProperty("issuedReceipts") final IssuedReceiptsTableConfiguration issuedReceipts,
@JsonProperty("keys") final Table keys,
@JsonProperty("messages") final TableWithExpiration messages,
@JsonProperty("pendingAccounts") final Table pendingAccounts,
@JsonProperty("pendingDevices") final Table pendingDevices,
@JsonProperty("phoneNumberIdentifiers") final Table phoneNumberIdentifiers,
@JsonProperty("profiles") final Table profiles,
@JsonProperty("pushChallenge") final Table pushChallenge,
@JsonProperty("redeemedReceipts") final TableWithExpiration redeemedReceipts,
@JsonProperty("remoteConfig") final Table remoteConfig,
@JsonProperty("reportMessage") final Table reportMessage,
@JsonProperty("reservedUsernames") final Table reservedUsernames,
@JsonProperty("subscriptions") final Table subscriptions) {
this.accounts = accounts;
this.deletedAccounts = deletedAccounts;
this.deletedAccountsLock = deletedAccountsLock;
this.issuedReceipts = issuedReceipts;
this.keys = keys;
this.messages = messages;
this.pendingAccounts = pendingAccounts;
this.pendingDevices = pendingDevices;
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
this.profiles = profiles;
this.pushChallenge = pushChallenge;
this.redeemedReceipts = redeemedReceipts;
this.remoteConfig = remoteConfig;
this.reportMessage = reportMessage;
this.reservedUsernames = reservedUsernames;
this.subscriptions = subscriptions;
}
@NotNull
@Valid
public AccountsTableConfiguration getAccounts() {
return accounts;
}
@NotNull
@Valid
public DeletedAccountsTableConfiguration getDeletedAccounts() {
return deletedAccounts;
}
@NotNull
@Valid
public Table getDeletedAccountsLock() {
return deletedAccountsLock;
}
@NotNull
@Valid
public IssuedReceiptsTableConfiguration getIssuedReceipts() {
return issuedReceipts;
}
@NotNull
@Valid
public Table getKeys() {
return keys;
}
@NotNull
@Valid
public TableWithExpiration getMessages() {
return messages;
}
@NotNull
@Valid
public Table getPendingAccounts() {
return pendingAccounts;
}
@NotNull
@Valid
public Table getPendingDevices() {
return pendingDevices;
}
@NotNull
@Valid
public Table getPhoneNumberIdentifiers() {
return phoneNumberIdentifiers;
}
@NotNull
@Valid
public Table getProfiles() {
return profiles;
}
@NotNull
@Valid
public Table getPushChallenge() {
return pushChallenge;
}
@NotNull
@Valid
public TableWithExpiration getRedeemedReceipts() {
return redeemedReceipts;
}
@NotNull
@Valid
public Table getRemoteConfig() {
return remoteConfig;
}
@NotNull
@Valid
public Table getReportMessage() {
return reportMessage;
}
@NotNull
@Valid
public Table getReservedUsernames() {
return reservedUsernames;
}
@NotNull
@Valid
public Table getSubscriptions() {
return subscriptions;
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Duration;
import javax.validation.constraints.NotEmpty;
public class IssuedReceiptsTableConfiguration extends DynamoDbTables.TableWithExpiration {
private final byte[] generator;
public IssuedReceiptsTableConfiguration(
@JsonProperty("tableName") final String tableName,
@JsonProperty("expiration") final Duration expiration,
@JsonProperty("generator") final byte[] generator) {
super(tableName, expiration);
this.generator = generator;
}
@NotEmpty
public byte[] getGenerator() {
return generator;
}
}

View File

@@ -1,20 +0,0 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import java.time.Duration;
public class MessageDynamoDbConfiguration extends DynamoDbConfiguration {
private Duration timeToLive = Duration.ofDays(7);
@Valid
public Duration getTimeToLive() {
return timeToLive;
}
}

View File

@@ -1,63 +0,0 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.time.Duration;
public class MonitoredS3ObjectConfiguration {
@JsonProperty
@NotBlank
private String s3Region;
@JsonProperty
@NotBlank
private String s3Bucket;
@JsonProperty
@NotBlank
private String objectKey;
@JsonProperty
private long maxSize = 16 * 1024 * 1024;
@JsonProperty
private Duration refreshInterval = Duration.ofMinutes(5);
public String getS3Region() {
return s3Region;
}
@VisibleForTesting
public void setS3Region(final String s3Region) {
this.s3Region = s3Region;
}
public String getS3Bucket() {
return s3Bucket;
}
public String getObjectKey() {
return objectKey;
}
public long getMaxSize() {
return maxSize;
}
@VisibleForTesting
public void setMaxSize(final long maxSize) {
this.maxSize = maxSize;
}
public Duration getRefreshInterval() {
return refreshInterval;
}
}

View File

@@ -63,6 +63,9 @@ public class RateLimitsConfiguration {
@JsonProperty
private RateLimitConfiguration usernameSet = new RateLimitConfiguration(100, 100 / (24.0 * 60.0));
@JsonProperty
private RateLimitConfiguration checkAccountExistence = new RateLimitConfiguration(1_000, 1_000 / 60.0);
public RateLimitConfiguration getAutoBlock() {
return autoBlock;
}
@@ -135,6 +138,10 @@ public class RateLimitsConfiguration {
return usernameSet;
}
public RateLimitConfiguration getCheckAccountExistence() {
return checkAccountExistence;
}
public static class RateLimitConfiguration {
@JsonProperty
private int bucketSize;

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import java.math.BigDecimal;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class RecaptchaV2Configuration {
private BigDecimal scoreFloor;
private String projectPath;
private String siteKey;
private String credentialConfigurationJson;
@DecimalMin("0")
@DecimalMax("1")
@NotNull
public BigDecimal getScoreFloor() {
return scoreFloor;
}
@NotEmpty
public String getProjectPath() {
return projectPath;
}
@NotEmpty
public String getSiteKey() {
return siteKey;
}
@NotEmpty
public String getCredentialConfigurationJson() {
return credentialConfigurationJson;
}
}

View File

@@ -11,41 +11,40 @@ import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.time.Duration;
import java.util.List;
public class RedisClusterConfiguration {
@JsonProperty
@NotEmpty
private List<String> urls;
@JsonProperty
@NotEmpty
private String configurationUri;
@JsonProperty
@NotNull
private Duration timeout = Duration.ofMillis(3_000);
@JsonProperty
@NotNull
private Duration timeout = Duration.ofMillis(3_000);
@JsonProperty
@NotNull
@Valid
private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
@JsonProperty
@NotNull
@Valid
private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
@JsonProperty
@NotNull
@Valid
private RetryConfiguration retry = new RetryConfiguration();
@JsonProperty
@NotNull
@Valid
private RetryConfiguration retry = new RetryConfiguration();
public List<String> getUrls() {
return urls;
}
public String getConfigurationUri() {
return configurationUri;
}
public Duration getTimeout() {
return timeout;
}
public Duration getTimeout() {
return timeout;
}
public CircuitBreakerConfiguration getCircuitBreakerConfiguration() {
return circuitBreaker;
}
public CircuitBreakerConfiguration getCircuitBreakerConfiguration() {
return circuitBreaker;
}
public RetryConfiguration getRetryConfiguration() {
return retry;
}
public RetryConfiguration getRetryConfiguration() {
return retry;
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotNull;
import java.time.Duration;
public class ReportMessageConfiguration {
@JsonProperty
@NotNull
private final Duration reportTtl = Duration.ofDays(7);
@JsonProperty
@NotNull
private final Duration counterTtl = Duration.ofDays(1);
public Duration getReportTtl() {
return reportTtl;
}
public Duration getCounterTtl() {
return counterTtl;
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotEmpty;
public class StripeConfiguration {
private final String apiKey;
private final byte[] idempotencyKeyGenerator;
private final String boostDescription;
@JsonCreator
public StripeConfiguration(
@JsonProperty("apiKey") final String apiKey,
@JsonProperty("idempotencyKeyGenerator") final byte[] idempotencyKeyGenerator,
@JsonProperty("boostDescription") final String boostDescription) {
this.apiKey = apiKey;
this.idempotencyKeyGenerator = idempotencyKeyGenerator;
this.boostDescription = boostDescription;
}
@NotEmpty
public String getApiKey() {
return apiKey;
}
@NotEmpty
public byte[] getIdempotencyKeyGenerator() {
return idempotencyKeyGenerator;
}
@NotEmpty
public String getBoostDescription() {
return boostDescription;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.dropwizard.validation.ValidationMethod;
import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
public class SubscriptionConfiguration {
private final Duration badgeGracePeriod;
private final Map<Long, SubscriptionLevelConfiguration> levels;
@JsonCreator
public SubscriptionConfiguration(
@JsonProperty("badgeGracePeriod") @Valid Duration badgeGracePeriod,
@JsonProperty("levels") @Valid Map<@NotNull @Min(1) Long, @NotNull @Valid SubscriptionLevelConfiguration> levels) {
this.badgeGracePeriod = badgeGracePeriod;
this.levels = levels;
}
public Duration getBadgeGracePeriod() {
return badgeGracePeriod;
}
public Map<Long, SubscriptionLevelConfiguration> getLevels() {
return levels;
}
@JsonIgnore
@ValidationMethod(message = "has a mismatch between the levels supported currencies")
public boolean isCurrencyListSameAcrossAllLevels() {
Optional<SubscriptionLevelConfiguration> any = levels.values().stream().findAny();
if (any.isEmpty()) {
return true;
}
Set<String> currencies = any.get().getPrices().keySet();
return levels.values().stream().allMatch(level -> currencies.equals(level.getPrices().keySet()));
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class SubscriptionLevelConfiguration {
private final String badge;
private final String product;
private final Map<String, SubscriptionPriceConfiguration> prices;
@JsonCreator
public SubscriptionLevelConfiguration(
@JsonProperty("badge") @NotEmpty String badge,
@JsonProperty("product") @NotEmpty String product,
@JsonProperty("prices") @Valid Map<@NotEmpty String, @NotNull @Valid SubscriptionPriceConfiguration> prices) {
this.badge = badge;
this.product = product;
this.prices = prices;
}
public String getBadge() {
return badge;
}
public String getProduct() {
return product;
}
public Map<String, SubscriptionPriceConfiguration> getPrices() {
return prices;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.math.BigDecimal;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class SubscriptionPriceConfiguration {
private final String id;
private final BigDecimal amount;
@JsonCreator
public SubscriptionPriceConfiguration(
@JsonProperty("id") @NotEmpty String id,
@JsonProperty("amount") @NotNull @DecimalMin("0.01") BigDecimal amount) {
this.id = id;
this.amount = amount;
}
public String getId() {
return id;
}
public BigDecimal getAmount() {
return amount;
}
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.databind.util.StdConverter;
import java.net.MalformedURLException;
import java.net.URL;
final class URLDeserializationConverter extends StdConverter<String, URL> {
@Override
public URL convert(final String value) {
try {
return new URL(value);
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.databind.util.StdConverter;
import java.net.URL;
final class URLSerializationConverter extends StdConverter<URL, String> {
@Override
public String convert(final URL value) {
return value.toString();
}
}

View File

@@ -1,28 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.Positive;
public class WavefrontConfiguration {
@JsonProperty
private String uri;
@JsonProperty
@Positive
private int batchSize = 10_000;
public String getUri() {
return uri;
}
public int getBatchSize() {
return batchSize;
}
}

View File

@@ -26,10 +26,6 @@ public class ZkConfig {
@NotNull
private byte[] serverPublic;
@JsonProperty
@NotNull
private Boolean enabled;
public byte[] getServerSecret() {
return serverSecret;
}
@@ -37,8 +33,4 @@ public class ZkConfig {
public byte[] getServerPublic() {
return serverPublic;
}
public boolean isEnabled() {
return enabled;
}
}

View File

@@ -1,62 +0,0 @@
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
public class DynamicAccountsDynamoDbMigrationConfiguration {
@JsonProperty
boolean backgroundMigrationEnabled;
@JsonProperty
int backgroundMigrationExecutorThreads = 1;
@JsonProperty
boolean deleteEnabled;
@JsonProperty
boolean writeEnabled;
@JsonProperty
boolean readEnabled;
@JsonProperty
boolean logMismatches;
public boolean isBackgroundMigrationEnabled() {
return backgroundMigrationEnabled;
}
public int getBackgroundMigrationExecutorThreads() {
return backgroundMigrationExecutorThreads;
}
public void setDeleteEnabled(boolean deleteEnabled) {
this.deleteEnabled = deleteEnabled;
}
public boolean isDeleteEnabled() {
return deleteEnabled;
}
public void setWriteEnabled(boolean writeEnabled) {
this.writeEnabled = writeEnabled;
}
public boolean isWriteEnabled() {
return writeEnabled;
}
@VisibleForTesting
public void setReadEnabled(boolean readEnabled) {
this.readEnabled = readEnabled;
}
public boolean isReadEnabled() {
return readEnabled;
}
public boolean isLogMismatches() {
return logMismatches;
}
}

View File

@@ -45,11 +45,15 @@ public class DynamicConfiguration {
private DynamicSignupCaptchaConfiguration signupCaptcha = new DynamicSignupCaptchaConfiguration();
@JsonProperty
private DynamicAccountsDynamoDbMigrationConfiguration accountsDynamoDbMigration = new DynamicAccountsDynamoDbMigrationConfiguration();
@Valid
private DynamicRateLimitChallengeConfiguration rateLimitChallenge = new DynamicRateLimitChallengeConfiguration();
@JsonProperty
private DynamicDirectoryReconcilerConfiguration directoryReconciler = new DynamicDirectoryReconcilerConfiguration();
@JsonProperty
@Valid
private DynamicRateLimitChallengeConfiguration rateLimitChallenge = new DynamicRateLimitChallengeConfiguration();
private DynamicPushLatencyConfiguration pushLatency = new DynamicPushLatencyConfiguration(Collections.emptyMap());
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
final String experimentName) {
@@ -94,11 +98,16 @@ public class DynamicConfiguration {
return signupCaptcha;
}
public DynamicAccountsDynamoDbMigrationConfiguration getAccountsDynamoDbMigrationConfiguration() {
return accountsDynamoDbMigration;
}
public DynamicRateLimitChallengeConfiguration getRateLimitChallengeConfiguration() {
return rateLimitChallenge;
}
public DynamicDirectoryReconcilerConfiguration getDirectoryReconcilerConfiguration() {
return directoryReconciler;
}
public DynamicPushLatencyConfiguration getPushLatencyConfiguration() {
return pushLatency;
}
}

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DynamicDirectoryReconcilerConfiguration {
@JsonProperty
private boolean enabled = true;
public boolean isEnabled() {
return enabled;
}
}

View File

@@ -6,66 +6,13 @@
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Duration;
import java.util.Collections;
import java.util.Set;
public class DynamicMessageRateConfiguration {
@JsonProperty
private boolean enforceUnsealedSenderRateLimit = false;
@JsonProperty
private Set<String> rateLimitedCountryCodes = Collections.emptySet();
@JsonProperty
private Set<String> rateLimitedHosts = Collections.emptySet();
@JsonProperty
private Duration responseDelay = Duration.ofNanos(1_200_000);
@JsonProperty
private Duration responseDelayJitter = Duration.ofNanos(500_000);
@JsonProperty
private Duration receiptDelay = Duration.ofMillis(1_200);
@JsonProperty
private Duration receiptDelayJitter = Duration.ofMillis(800);
@JsonProperty
private double receiptProbability = 0.82;
public boolean isEnforceUnsealedSenderRateLimit() {
return enforceUnsealedSenderRateLimit;
}
public Set<String> getRateLimitedCountryCodes() {
return rateLimitedCountryCodes;
}
public Set<String> getRateLimitedHosts() {
return rateLimitedHosts;
}
public Duration getResponseDelay() {
return responseDelay;
}
public Duration getResponseDelayJitter() {
return responseDelayJitter;
}
public Duration getReceiptDelay() {
return receiptDelay;
}
public Duration getReceiptDelayJitter() {
return receiptDelayJitter;
}
public double getReceiptProbability() {
return receiptProbability;
}
}

View File

@@ -7,14 +7,14 @@ package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Collections;
import java.util.Set;
import java.util.List;
public class DynamicPaymentsConfiguration {
@JsonProperty
private Set<String> allowedCountryCodes = Collections.emptySet();
private List<String> disallowedPrefixes = Collections.emptyList();
public Set<String> getAllowedCountryCodes() {
return allowedCountryCodes;
public List<String> getDisallowedPrefixes() {
return disallowedPrefixes;
}
}

View File

@@ -20,12 +20,16 @@ public class DynamicPreRegistrationExperimentEnrollmentConfiguration {
@JsonProperty
@Valid
private Set<String> excludedCountryCodes = Collections.emptySet();
private Set<String> excludedE164s = Collections.emptySet();
@JsonProperty
@Valid
private Set<String> includedCountryCodes = Collections.emptySet();
@JsonProperty
@Valid
private Set<String> excludedCountryCodes = Collections.emptySet();
@JsonProperty
@Valid
@Min(0)
@@ -36,14 +40,18 @@ public class DynamicPreRegistrationExperimentEnrollmentConfiguration {
return enrolledE164s;
}
public Set<String> getExcludedCountryCodes() {
return excludedCountryCodes;
public Set<String> getExcludedE164s() {
return excludedE164s;
}
public Set<String> getIncludedCountryCodes() {
return includedCountryCodes;
}
public Set<String> getExcludedCountryCodes() {
return excludedCountryCodes;
}
public int getEnrollmentPercentage() {
return enrollmentPercentage;
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.vdurmont.semver4j.Semver;
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
import java.util.Map;
import java.util.Set;
public class DynamicPushLatencyConfiguration {
private final Map<ClientPlatform, Set<Semver>> instrumentedVersions;
@JsonCreator
public DynamicPushLatencyConfiguration(@JsonProperty("instrumentedVersions") final Map<ClientPlatform, Set<Semver>> instrumentedVersions) {
this.instrumentedVersions = instrumentedVersions;
}
public Map<ClientPlatform, Set<Semver>> getInstrumentedVersions() {
return instrumentedVersions;
}
}

View File

@@ -11,9 +11,6 @@ import javax.validation.constraints.NotNull;
public class DynamicRateLimitChallengeConfiguration {
@JsonProperty
private boolean preKeyLimitEnforced = false;
@JsonProperty
boolean unsealedSenderLimitEnforced = false;
@@ -30,10 +27,6 @@ public class DynamicRateLimitChallengeConfiguration {
return Optional.ofNullable(clientSupportedVersions.get(platform));
}
public boolean isPreKeyLimitEnforced() {
return preKeyLimitEnforced;
}
public boolean isUnsealedSenderLimitEnforced() {
return unsealedSenderLimitEnforced;
}

View File

@@ -16,9 +16,6 @@ public class DynamicRateLimitsConfiguration {
@JsonProperty
private int unsealedSenderPermitIncrement = 50;
@JsonProperty
private RateLimitConfiguration unsealedSenderIp = new RateLimitConfiguration(120, 2.0 / 60);
@JsonProperty
private RateLimitConfiguration rateLimitReset = new RateLimitConfiguration(2, 2.0 / (60 * 24));
@@ -34,13 +31,6 @@ public class DynamicRateLimitsConfiguration {
@JsonProperty
private RateLimitConfiguration pushChallengeSuccess = new RateLimitConfiguration(2, 2.0 / (60 * 24));
@JsonProperty
private RateLimitConfiguration dailyPreKeys = new RateLimitConfiguration(50, 50.0 / (24.0 * 60));
public RateLimitConfiguration getUnsealedSenderIp() {
return unsealedSenderIp;
}
public CardinalityRateLimitConfiguration getUnsealedSenderNumber() {
return unsealedSenderNumber;
}
@@ -72,8 +62,4 @@ public class DynamicRateLimitsConfiguration {
public int getUnsealedSenderPermitIncrement() {
return unsealedSenderPermitIncrement;
}
public RateLimitConfiguration getDailyPreKeys() {
return dailyPreKeys;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import java.io.IOException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
@PreMatching
public class AcceptNumericOnlineFlagRequestFilter implements ContainerRequestFilter {
private final String pathPrefix;
public AcceptNumericOnlineFlagRequestFilter(final String pathPrefix) {
this.pathPrefix = pathPrefix;
}
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
if (requestContext.getUriInfo().getPath().startsWith(pathPrefix)) {
if ("1".equals(requestContext.getUriInfo().getQueryParameters().getFirst("online"))) {
requestContext.setRequestUri(requestContext.getUriInfo().getRequestUriBuilder()
.replaceQueryParam("online", "true")
.build());
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
@@ -15,6 +15,7 @@ import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
@@ -25,10 +26,14 @@ import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
@@ -36,25 +41,30 @@ import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.auth.TurnToken;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicSignupCaptchaConfiguration;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.AccountCreationResult;
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
import org.whispersystems.textsecuregcm.entities.DeprecatedPin;
import org.whispersystems.textsecuregcm.entities.ChangePhoneNumberRequest;
import org.whispersystems.textsecuregcm.entities.DeviceName;
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
import org.whispersystems.textsecuregcm.entities.RegistrationLock;
@@ -68,19 +78,20 @@ import org.whispersystems.textsecuregcm.push.GcmMessage;
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.AbusiveHostRule;
import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
import org.whispersystems.textsecuregcm.storage.UsernameNotAvailableException;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.ForwardedIpUtil;
import org.whispersystems.textsecuregcm.util.Hex;
import org.whispersystems.textsecuregcm.util.ImpossiblePhoneNumberException;
import org.whispersystems.textsecuregcm.util.NonNormalizedPhoneNumberException;
import org.whispersystems.textsecuregcm.util.Username;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
@@ -90,7 +101,6 @@ public class AccountController {
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter newUserMeter = metricRegistry.meter(name(AccountController.class, "brand_new_user" ));
private final Meter blockedHostMeter = metricRegistry.meter(name(AccountController.class, "blocked_host" ));
private final Meter filteredHostMeter = metricRegistry.meter(name(AccountController.class, "filtered_host" ));
private final Meter rateLimitedHostMeter = metricRegistry.meter(name(AccountController.class, "rate_limited_host" ));
@@ -112,33 +122,27 @@ public class AccountController {
private static final String VERIFY_EXPERIMENT_TAG_NAME = "twilioVerify";
private final PendingAccountsManager pendingAccounts;
private final StoredVerificationCodeManager pendingAccounts;
private final AccountsManager accounts;
private final UsernamesManager usernames;
private final AbusiveHostRules abusiveHostRules;
private final RateLimiters rateLimiters;
private final SmsSender smsSender;
private final DirectoryQueue directoryQueue;
private final MessagesManager messagesManager;
private final DynamicConfigurationManager dynamicConfigurationManager;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private final TurnTokenGenerator turnTokenGenerator;
private final Map<String, Integer> testDevices;
private final RecaptchaClient recaptchaClient;
private final RecaptchaClient recaptchaClient;
private final GCMSender gcmSender;
private final APNSender apnSender;
private final ExternalServiceCredentialGenerator backupServiceCredentialGenerator;
private final TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager;
public AccountController(PendingAccountsManager pendingAccounts,
public AccountController(StoredVerificationCodeManager pendingAccounts,
AccountsManager accounts,
UsernamesManager usernames,
AbusiveHostRules abusiveHostRules,
RateLimiters rateLimiters,
SmsSender smsSenderFactory,
DirectoryQueue directoryQueue,
MessagesManager messagesManager,
DynamicConfigurationManager dynamicConfigurationManager,
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
TurnTokenGenerator turnTokenGenerator,
Map<String, Integer> testDevices,
RecaptchaClient recaptchaClient,
@@ -149,16 +153,13 @@ public class AccountController {
{
this.pendingAccounts = pendingAccounts;
this.accounts = accounts;
this.usernames = usernames;
this.abusiveHostRules = abusiveHostRules;
this.rateLimiters = rateLimiters;
this.smsSender = smsSenderFactory;
this.directoryQueue = directoryQueue;
this.messagesManager = messagesManager;
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.testDevices = testDevices;
this.turnTokenGenerator = turnTokenGenerator;
this.recaptchaClient = recaptchaClient;
this.recaptchaClient = recaptchaClient;
this.gcmSender = gcmSender;
this.apnSender = apnSender;
this.backupServiceCredentialGenerator = backupServiceCredentialGenerator;
@@ -168,30 +169,31 @@ public class AccountController {
@Timed
@GET
@Path("/{type}/preauth/{token}/{number}")
@Produces(MediaType.APPLICATION_JSON)
public Response getPreAuth(@PathParam("type") String pushType,
@PathParam("token") String pushToken,
@PathParam("number") String number,
@QueryParam("voip") Optional<Boolean> useVoip)
{
throws ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
if (!"apn".equals(pushType) && !"fcm".equals(pushType)) {
return Response.status(400).build();
}
if (!Util.isValidNumber(number)) {
return Response.status(400).build();
}
Util.requireNormalizedNumber(number);
String pushChallenge = generatePushChallenge();
StoredVerificationCode storedVerificationCode = new StoredVerificationCode(null,
System.currentTimeMillis(),
pushChallenge);
pushChallenge,
null);
pendingAccounts.store(number, storedVerificationCode);
if ("fcm".equals(pushType)) {
gcmSender.sendMessage(new GcmMessage(pushToken, number, 0, GcmMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode())));
gcmSender.sendMessage(new GcmMessage(pushToken, null, 0, GcmMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode())));
} else if ("apn".equals(pushType)) {
apnSender.sendMessage(new ApnMessage(pushToken, number, 0, useVoip.orElse(true), ApnMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode())));
apnSender.sendMessage(new ApnMessage(pushToken, null, 0, useVoip.orElse(true), ApnMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode())));
} else {
throw new AssertionError();
}
@@ -202,6 +204,7 @@ public class AccountController {
@Timed
@GET
@Path("/{transport}/code/{number}")
@Produces(MediaType.APPLICATION_JSON)
public Response createAccount(@PathParam("transport") String transport,
@PathParam("number") String number,
@HeaderParam("X-Forwarded-For") String forwardedFor,
@@ -210,28 +213,21 @@ public class AccountController {
@QueryParam("client") Optional<String> client,
@QueryParam("captcha") Optional<String> captcha,
@QueryParam("challenge") Optional<String> pushChallenge)
throws RateLimitExceededException, RetryLaterException
{
if (!Util.isValidNumber(number)) {
logger.info("Invalid number: " + number);
throw new WebApplicationException(Response.status(400).build());
}
throws RateLimitExceededException, RetryLaterException, ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
if (number.startsWith("+98")) {
transport = "voice";
}
Util.requireNormalizedNumber(number);
String requester = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow();
String sourceHost = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow();
Optional<StoredVerificationCode> storedChallenge = pendingAccounts.getCodeForNumber(number);
CaptchaRequirement requirement = requiresCaptcha(number, transport, forwardedFor, requester, captcha, storedChallenge, pushChallenge);
CaptchaRequirement requirement = requiresCaptcha(number, transport, forwardedFor, sourceHost, captcha, storedChallenge, pushChallenge);
if (requirement.isCaptchaRequired()) {
captchaRequiredMeter.mark();
if (requirement.isAutoBlock() && shouldAutoBlock(requester)) {
logger.info("Auto-block: " + requester);
abusiveHostRules.setBlockedHost(requester, "Auto-Block");
if (requirement.isAutoBlock() && shouldAutoBlock(sourceHost)) {
logger.info("Auto-block: {}", sourceHost);
abusiveHostRules.setBlockedHost(sourceHost, "Auto-Block");
}
return Response.status(402).build();
@@ -317,6 +313,7 @@ public class AccountController {
});
});
// TODO Remove this meter when external dependencies have been resolved
metricRegistry.meter(name(AccountController.class, "create", Util.getCountryCode(number))).mark();
{
@@ -338,88 +335,110 @@ public class AccountController {
@Produces(MediaType.APPLICATION_JSON)
@Path("/code/{verification_code}")
public AccountCreationResult verifyAccount(@PathParam("verification_code") String verificationCode,
@HeaderParam("Authorization") String authorizationHeader,
@HeaderParam("X-Signal-Agent") String signalAgent,
@HeaderParam("User-Agent") String userAgent,
@QueryParam("transfer") Optional<Boolean> availableForTransfer,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
{
try {
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
String number = header.getIdentifier().getNumber();
String password = header.getPassword();
@HeaderParam("Authorization") BasicAuthorizationHeader authorizationHeader,
@HeaderParam("X-Signal-Agent") String signalAgent,
@HeaderParam("User-Agent") String userAgent,
@QueryParam("transfer") Optional<Boolean> availableForTransfer,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException, InterruptedException {
if (number == null) {
throw new WebApplicationException(400);
}
String number = authorizationHeader.getUsername();
String password = authorizationHeader.getPassword();
rateLimiters.getVerifyLimiter().validate(number);
rateLimiters.getVerifyLimiter().validate(number);
Optional<StoredVerificationCode> storedVerificationCode = pendingAccounts.getCodeForNumber(number);
// Note that successful verification depends on being able to find a stored verification code for the given number.
// We check that numbers are normalized before we store verification codes, and so don't need to re-assert
// normalization here.
Optional<StoredVerificationCode> storedVerificationCode = pendingAccounts.getCodeForNumber(number);
if (storedVerificationCode.isEmpty() || !storedVerificationCode.get().isValid(verificationCode)) {
throw new WebApplicationException(Response.status(403).build());
}
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
.ifPresent(smsSender::reportVerificationSucceeded);
Optional<Account> existingAccount = accounts.get(number);
Optional<StoredRegistrationLock> existingRegistrationLock = existingAccount.map(Account::getRegistrationLock);
Optional<ExternalServiceCredentials> existingBackupCredentials = existingAccount.map(Account::getUuid)
.map(uuid -> backupServiceCredentialGenerator.generateFor(uuid.toString()));
if (existingRegistrationLock.isPresent() && existingRegistrationLock.get().requiresClientRegistrationLock()) {
rateLimiters.getVerifyLimiter().clear(number);
if (!Util.isEmpty(accountAttributes.getRegistrationLock()) || !Util.isEmpty(accountAttributes.getPin())) {
rateLimiters.getPinLimiter().validate(number);
}
if (!existingRegistrationLock.get().verify(accountAttributes.getRegistrationLock(), accountAttributes.getPin())) {
throw new WebApplicationException(Response.status(423)
.entity(new RegistrationLockFailure(existingRegistrationLock.get().getTimeRemaining(),
existingRegistrationLock.get().needsFailureCredentials() ? existingBackupCredentials.orElseThrow() : null))
.build());
}
rateLimiters.getPinLimiter().clear(number);
}
if (availableForTransfer.orElse(false) && existingAccount.map(Account::isTransferSupported).orElse(false)) {
throw new WebApplicationException(Response.status(409).build());
}
Account account = createAccount(number, password, signalAgent, accountAttributes);
{
metricRegistry.meter(name(AccountController.class, "verify", Util.getCountryCode(number))).mark();
final List<Tag> tags = new ArrayList<>();
tags.add(Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)));
tags.add(UserAgentTagUtil.getPlatformTag(userAgent));
tags.add(Tag.of(VERIFY_EXPERIMENT_TAG_NAME, String.valueOf(storedVerificationCode.get().getTwilioVerificationSid().isPresent())));
Metrics.counter(ACCOUNT_VERIFY_COUNTER_NAME, tags).increment();
Metrics.timer(name(AccountController.class, "verifyDuration"), tags)
.record(Instant.now().toEpochMilli() - storedVerificationCode.get().getTimestamp(), TimeUnit.MILLISECONDS);
}
return new AccountCreationResult(account.getUuid(), existingAccount.map(Account::isStorageSupported).orElse(false));
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad Authorization Header", e);
throw new WebApplicationException(Response.status(401).build());
if (storedVerificationCode.isEmpty() || !storedVerificationCode.get().isValid(verificationCode)) {
throw new WebApplicationException(Response.status(403).build());
}
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
.ifPresent(smsSender::reportVerificationSucceeded);
Optional<Account> existingAccount = accounts.getByE164(number);
if (existingAccount.isPresent()) {
verifyRegistrationLock(existingAccount.get(), accountAttributes.getRegistrationLock());
}
if (availableForTransfer.orElse(false) && existingAccount.map(Account::isTransferSupported).orElse(false)) {
throw new WebApplicationException(Response.status(409).build());
}
rateLimiters.getVerifyLimiter().clear(number);
Account account = accounts.create(number, password, signalAgent, accountAttributes,
existingAccount.map(Account::getBadges).orElseGet(ArrayList::new));
{
metricRegistry.meter(name(AccountController.class, "verify", Util.getCountryCode(number))).mark();
final List<Tag> tags = new ArrayList<>();
tags.add(Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)));
tags.add(UserAgentTagUtil.getPlatformTag(userAgent));
tags.add(Tag.of(VERIFY_EXPERIMENT_TAG_NAME, String.valueOf(storedVerificationCode.get().getTwilioVerificationSid().isPresent())));
Metrics.counter(ACCOUNT_VERIFY_COUNTER_NAME, tags).increment();
Metrics.timer(name(AccountController.class, "verifyDuration"), tags)
.record(Instant.now().toEpochMilli() - storedVerificationCode.get().getTimestamp(), TimeUnit.MILLISECONDS);
}
return new AccountCreationResult(account.getUuid(),
account.getNumber(),
account.getPhoneNumberIdentifier(),
account.getUsername().orElse(null),
existingAccount.map(Account::isStorageSupported).orElse(false));
}
@Timed
@PUT
@Path("/number")
@Produces(MediaType.APPLICATION_JSON)
public void changeNumber(@Auth final AuthenticatedAccount authenticatedAccount, @Valid final ChangePhoneNumberRequest request)
throws RateLimitExceededException, InterruptedException, ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
if (request.getNumber().equals(authenticatedAccount.getAccount().getNumber())) {
// This may be a request that got repeated due to poor network conditions or other client error; take no action,
// but report success since the account is in the desired state
return;
}
Util.requireNormalizedNumber(request.getNumber());
rateLimiters.getVerifyLimiter().validate(request.getNumber());
final Optional<StoredVerificationCode> storedVerificationCode =
pendingAccounts.getCodeForNumber(request.getNumber());
if (storedVerificationCode.isEmpty() || !storedVerificationCode.get().isValid(request.getCode())) {
throw new WebApplicationException(Response.status(403).build());
}
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
.ifPresent(smsSender::reportVerificationSucceeded);
final Optional<Account> existingAccount = accounts.getByE164(request.getNumber());
if (existingAccount.isPresent()) {
verifyRegistrationLock(existingAccount.get(), request.getRegistrationLock());
}
rateLimiters.getVerifyLimiter().clear(request.getNumber());
accounts.changeNumber(authenticatedAccount.getAccount(), request.getNumber());
}
@Timed
@GET
@Path("/turn/")
@Produces(MediaType.APPLICATION_JSON)
public TurnToken getTurnToken(@Auth Account account) throws RateLimitExceededException {
rateLimiters.getTurnLimiter().validate(account.getNumber());
public TurnToken getTurnToken(@Auth AuthenticatedAccount auth) throws RateLimitExceededException {
rateLimiters.getTurnLimiter().validate(auth.getAccount().getUuid());
return turnTokenGenerator.generate();
}
@@ -427,221 +446,249 @@ public class AccountController {
@PUT
@Path("/gcm/")
@Consumes(MediaType.APPLICATION_JSON)
public void setGcmRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid GcmRegistrationId registrationId) {
Account account = disabledPermittedAccount.getAccount();
Device device = account.getAuthenticatedDevice().get();
boolean wasAccountEnabled = account.isEnabled();
@ChangesDeviceEnabledState
public void setGcmRegistrationId(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
@Valid GcmRegistrationId registrationId) {
Account account = disabledPermittedAuth.getAccount();
Device device = disabledPermittedAuth.getAuthenticatedDevice();
if (device.getGcmId() != null &&
device.getGcmId().equals(registrationId.getGcmRegistrationId()))
{
device.getGcmId().equals(registrationId.getGcmRegistrationId())) {
return;
}
device.setApnId(null);
device.setVoipApnId(null);
device.setGcmId(registrationId.getGcmRegistrationId());
device.setFetchesMessages(false);
accounts.update(account);
if (!wasAccountEnabled && account.isEnabled()) {
directoryQueue.refreshRegisteredUser(account);
}
accounts.updateDevice(account, device.getId(), d -> {
d.setApnId(null);
d.setVoipApnId(null);
d.setGcmId(registrationId.getGcmRegistrationId());
d.setFetchesMessages(false);
});
}
@Timed
@DELETE
@Path("/gcm/")
public void deleteGcmRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount) {
Account account = disabledPermittedAccount.getAccount();
Device device = account.getAuthenticatedDevice().get();
device.setGcmId(null);
device.setFetchesMessages(false);
device.setUserAgent("OWA");
@ChangesDeviceEnabledState
public void deleteGcmRegistrationId(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth) {
Account account = disabledPermittedAuth.getAccount();
Device device = disabledPermittedAuth.getAuthenticatedDevice();
accounts.update(account);
directoryQueue.refreshRegisteredUser(account);
accounts.updateDevice(account, device.getId(), d -> {
d.setGcmId(null);
d.setFetchesMessages(false);
d.setUserAgent("OWA");
});
}
@Timed
@PUT
@Path("/apn/")
@Consumes(MediaType.APPLICATION_JSON)
public void setApnRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid ApnRegistrationId registrationId) {
Account account = disabledPermittedAccount.getAccount();
Device device = account.getAuthenticatedDevice().get();
boolean wasAccountEnabled = account.isEnabled();
@ChangesDeviceEnabledState
public void setApnRegistrationId(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
@Valid ApnRegistrationId registrationId) {
Account account = disabledPermittedAuth.getAccount();
Device device = disabledPermittedAuth.getAuthenticatedDevice();
device.setApnId(registrationId.getApnRegistrationId());
device.setVoipApnId(registrationId.getVoipRegistrationId());
device.setGcmId(null);
device.setFetchesMessages(false);
accounts.update(account);
if (!wasAccountEnabled && account.isEnabled()) {
directoryQueue.refreshRegisteredUser(account);
}
accounts.updateDevice(account, device.getId(), d -> {
d.setApnId(registrationId.getApnRegistrationId());
d.setVoipApnId(registrationId.getVoipRegistrationId());
d.setGcmId(null);
d.setFetchesMessages(false);
});
}
@Timed
@DELETE
@Path("/apn/")
public void deleteApnRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount) {
Account account = disabledPermittedAccount.getAccount();
Device device = account.getAuthenticatedDevice().get();
device.setApnId(null);
device.setFetchesMessages(false);
if (device.getId() == 1) {
device.setUserAgent("OWI");
} else {
device.setUserAgent("OWP");
}
@ChangesDeviceEnabledState
public void deleteApnRegistrationId(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth) {
Account account = disabledPermittedAuth.getAccount();
Device device = disabledPermittedAuth.getAuthenticatedDevice();
accounts.update(account);
directoryQueue.refreshRegisteredUser(account);
accounts.updateDevice(account, device.getId(), d -> {
d.setApnId(null);
d.setFetchesMessages(false);
if (d.getId() == 1) {
d.setUserAgent("OWI");
} else {
d.setUserAgent("OWP");
}
});
}
@Timed
@PUT
@Produces(MediaType.APPLICATION_JSON)
@Path("/registration_lock")
public void setRegistrationLock(@Auth Account account, @Valid RegistrationLock accountLock) {
public void setRegistrationLock(@Auth AuthenticatedAccount auth, @Valid RegistrationLock accountLock) {
AuthenticationCredentials credentials = new AuthenticationCredentials(accountLock.getRegistrationLock());
account.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt());
account.setPin(null);
accounts.update(account);
accounts.update(auth.getAccount(),
a -> a.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt()));
}
@Timed
@DELETE
@Path("/registration_lock")
public void removeRegistrationLock(@Auth Account account) {
account.setRegistrationLock(null, null);
accounts.update(account);
}
@Timed
@PUT
@Produces(MediaType.APPLICATION_JSON)
@Path("/pin/")
public void setPin(@Auth Account account, @Valid DeprecatedPin accountLock) {
account.setPin(accountLock.getPin());
account.setRegistrationLock(null, null);
accounts.update(account);
}
@Timed
@DELETE
@Path("/pin/")
public void removePin(@Auth Account account) {
account.setPin(null);
accounts.update(account);
public void removeRegistrationLock(@Auth AuthenticatedAccount auth) {
accounts.update(auth.getAccount(), a -> a.setRegistrationLock(null, null));
}
@Timed
@PUT
@Path("/name/")
public void setName(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid DeviceName deviceName) {
Account account = disabledPermittedAccount.getAccount();
account.getAuthenticatedDevice().get().setName(deviceName.getDeviceName());
accounts.update(account);
public void setName(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth, @Valid DeviceName deviceName) {
Account account = disabledPermittedAuth.getAccount();
Device device = disabledPermittedAuth.getAuthenticatedDevice();
accounts.updateDevice(account, device.getId(), d -> d.setName(deviceName.getDeviceName()));
}
@Timed
@DELETE
@Path("/signaling_key")
public void removeSignalingKey(@Auth DisabledPermittedAccount disabledPermittedAccount) {
public void removeSignalingKey(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth) {
}
@Timed
@PUT
@Path("/attributes/")
@Consumes(MediaType.APPLICATION_JSON)
public void setAccountAttributes(@Auth DisabledPermittedAccount disabledPermittedAccount,
@HeaderParam("X-Signal-Agent") String userAgent,
@Valid AccountAttributes attributes)
{
Account account = disabledPermittedAccount.getAccount();
Device device = account.getAuthenticatedDevice().get();
@Produces(MediaType.APPLICATION_JSON)
@ChangesDeviceEnabledState
public void setAccountAttributes(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
@HeaderParam("X-Signal-Agent") String userAgent,
@Valid AccountAttributes attributes) {
Account account = disabledPermittedAuth.getAccount();
long deviceId = disabledPermittedAuth.getAuthenticatedDevice().getId();
device.setFetchesMessages(attributes.getFetchesMessages());
device.setName(attributes.getName());
device.setLastSeen(Util.todayInMillis());
device.setCapabilities(attributes.getCapabilities());
device.setRegistrationId(attributes.getRegistrationId());
device.setUserAgent(userAgent);
// temporary: For deterministic updates during the DynamoDB migration, use a fully parameterized registration lock
@Nullable final AuthenticationCredentials registrationLockCredentials =
Util.isEmpty(attributes.getRegistrationLock()) ? null
: new AuthenticationCredentials(attributes.getRegistrationLock());
setAccountRegistrationLockFromAttributes(account, attributes);
accounts.update(account, a -> {
a.getDevice(deviceId).ifPresent(d -> {
d.setFetchesMessages(attributes.getFetchesMessages());
d.setName(attributes.getName());
d.setLastSeen(Util.todayInMillis());
d.setCapabilities(attributes.getCapabilities());
d.setRegistrationId(attributes.getRegistrationId());
d.setUserAgent(userAgent);
});
final boolean hasDiscoverabilityChange = (account.isDiscoverableByPhoneNumber() != attributes.isDiscoverableByPhoneNumber());
// temporary: for deterministic updates during the DynamoDB migration, use a fully parameterized registration lock
// a.setRegistrationLockFromAttributes(attributes);
if (registrationLockCredentials != null) {
a.setRegistrationLock(registrationLockCredentials.getHashedAuthenticationToken(),
registrationLockCredentials.getSalt());
} else {
a.setRegistrationLock(null, null);
}
account.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey());
account.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess());
account.setDiscoverableByPhoneNumber(attributes.isDiscoverableByPhoneNumber());
accounts.update(account);
if (hasDiscoverabilityChange) {
directoryQueue.refreshRegisteredUser(account);
}
a.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey());
a.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess());
a.setDiscoverableByPhoneNumber(attributes.isDiscoverableByPhoneNumber());
});
}
@GET
@Path("/me")
@Produces(MediaType.APPLICATION_JSON)
public AccountCreationResult getMe(@Auth Account account) {
return whoAmI(account);
public AccountCreationResult getMe(@Auth AuthenticatedAccount auth) {
return whoAmI(auth);
}
@GET
@Path("/whoami")
@Produces(MediaType.APPLICATION_JSON)
public AccountCreationResult whoAmI(@Auth Account account) {
return new AccountCreationResult(account.getUuid(), account.isStorageSupported());
public AccountCreationResult whoAmI(@Auth AuthenticatedAccount auth) {
return new AccountCreationResult(auth.getAccount().getUuid(),
auth.getAccount().getNumber(),
auth.getAccount().getPhoneNumberIdentifier(),
auth.getAccount().getUsername().orElse(null),
auth.getAccount().isStorageSupported());
}
@DELETE
@Path("/username")
@Produces(MediaType.APPLICATION_JSON)
public void deleteUsername(@Auth Account account) {
usernames.delete(account.getUuid());
public void deleteUsername(@Auth AuthenticatedAccount auth) {
accounts.clearUsername(auth.getAccount());
}
@PUT
@Path("/username/{username}")
@Produces(MediaType.APPLICATION_JSON)
public Response setUsername(@Auth Account account, @PathParam("username") String username) throws RateLimitExceededException {
rateLimiters.getUsernameSetLimiter().validate(account.getUuid().toString());
public Response setUsername(@Auth AuthenticatedAccount auth, @PathParam("username") @Username String username)
throws RateLimitExceededException {
rateLimiters.getUsernameSetLimiter().validate(auth.getAccount().getUuid());
if (username == null || username.isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
username = username.toLowerCase();
if (!username.matches("^[a-z_][a-z0-9_]+$")) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
if (!usernames.put(account.getUuid(), username)) {
try {
accounts.setUsername(auth.getAccount(), username);
} catch (final UsernameNotAvailableException e) {
return Response.status(Response.Status.CONFLICT).build();
}
return Response.ok().build();
}
@HEAD
@Path("/account/{uuid}")
public Response accountExists(
@HeaderParam("X-Forwarded-For") final String forwardedFor,
@PathParam("uuid") final UUID uuid,
@Context HttpServletRequest request) throws RateLimitExceededException {
// Disallow clients from making authenticated requests to this endpoint
if (StringUtils.isNotBlank(request.getHeader("Authorization"))) {
throw new BadRequestException();
}
final String mostRecentProxy = ForwardedIpUtil.getMostRecentProxy(forwardedFor)
.orElseThrow(() -> new RateLimitExceededException(Duration.ofHours(1)));
rateLimiters.getCheckAccountExistenceLimiter().validate(mostRecentProxy);
final Status status = accounts.getByAccountIdentifier(uuid)
.or(() -> accounts.getByPhoneNumberIdentifier(uuid))
.isPresent() ? Status.OK : Status.NOT_FOUND;
return Response.status(status).build();
}
private void verifyRegistrationLock(final Account existingAccount, @Nullable final String clientRegistrationLock)
throws RateLimitExceededException, WebApplicationException {
final StoredRegistrationLock existingRegistrationLock = existingAccount.getRegistrationLock();
final ExternalServiceCredentials existingBackupCredentials =
backupServiceCredentialGenerator.generateFor(existingAccount.getUuid().toString());
if (existingRegistrationLock.requiresClientRegistrationLock()) {
if (!Util.isEmpty(clientRegistrationLock)) {
rateLimiters.getPinLimiter().validate(existingAccount.getNumber());
}
if (!existingRegistrationLock.verify(clientRegistrationLock)) {
throw new WebApplicationException(Response.status(423)
.entity(new RegistrationLockFailure(existingRegistrationLock.getTimeRemaining(),
existingRegistrationLock.needsFailureCredentials() ? existingBackupCredentials : null))
.build());
}
rateLimiters.getPinLimiter().clear(existingAccount.getNumber());
}
}
private CaptchaRequirement requiresCaptcha(String number, String transport, String forwardedFor,
String requester,
String sourceHost,
Optional<String> captchaToken,
Optional<StoredVerificationCode> storedVerificationCode,
Optional<String> pushChallenge)
{
if (captchaToken.isPresent()) {
boolean validToken = recaptchaClient.verify(captchaToken.get(), requester);
boolean validToken = recaptchaClient.verify(captchaToken.get(), sourceHost);
if (validToken) {
captchaSuccessMeter.mark();
@@ -679,18 +726,18 @@ public class AccountController {
}
}
List<AbusiveHostRule> abuseRules = abusiveHostRules.getAbusiveHostRulesFor(requester);
List<AbusiveHostRule> abuseRules = abusiveHostRules.getAbusiveHostRulesFor(sourceHost);
for (AbusiveHostRule abuseRule : abuseRules) {
if (abuseRule.isBlocked()) {
logger.info("Blocked host: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")");
if (abuseRule.blocked()) {
logger.info("Blocked host: {}, {}, {} ({})", transport, number, sourceHost, forwardedFor);
blockedHostMeter.mark();
return new CaptchaRequirement(true, false);
}
if (!abuseRule.getRegions().isEmpty()) {
if (abuseRule.getRegions().stream().noneMatch(number::startsWith)) {
logger.info("Restricted host: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")");
if (!abuseRule.regions().isEmpty()) {
if (abuseRule.regions().stream().noneMatch(number::startsWith)) {
logger.info("Restricted host: {}, {}, {} ({})", transport, number, sourceHost, forwardedFor);
filteredHostMeter.mark();
return new CaptchaRequirement(true, false);
}
@@ -698,9 +745,9 @@ public class AccountController {
}
try {
rateLimiters.getSmsVoiceIpLimiter().validate(requester);
rateLimiters.getSmsVoiceIpLimiter().validate(sourceHost);
} catch (RateLimitExceededException e) {
logger.info("Rate limited exceeded: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")");
logger.info("Rate limit exceeded: {}, {}, {} ({})", transport, number, sourceHost, forwardedFor);
rateLimitedHostMeter.mark();
return new CaptchaRequirement(true, true);
}
@@ -708,7 +755,7 @@ public class AccountController {
try {
rateLimiters.getSmsVoicePrefixLimiter().validate(Util.getNumberPrefix(number));
} catch (RateLimitExceededException e) {
logger.info("Prefix rate limit exceeded: " + transport + ", " + number + ", (" + forwardedFor + ")");
logger.info("Prefix rate limit exceeded: {}, {}, {} ({})", transport, number, sourceHost, forwardedFor);
rateLimitedPrefixMeter.mark();
return new CaptchaRequirement(true, true);
}
@@ -724,13 +771,13 @@ public class AccountController {
@Timed
@DELETE
@Path("/me")
public void deleteAccount(@Auth Account account) {
accounts.delete(account, AccountsManager.DeletionReason.USER_REQUEST);
public void deleteAccount(@Auth AuthenticatedAccount auth) throws InterruptedException {
accounts.delete(auth.getAccount(), AccountsManager.DeletionReason.USER_REQUEST);
}
private boolean shouldAutoBlock(String requester) {
private boolean shouldAutoBlock(String sourceHost) {
try {
rateLimiters.getAutoBlockLimiter().validate(requester);
rateLimiters.getAutoBlockLimiter().validate(sourceHost);
} catch (RateLimitExceededException e) {
return true;
}
@@ -738,52 +785,6 @@ public class AccountController {
return false;
}
private Account createAccount(String number, String password, String signalAgent, AccountAttributes accountAttributes) {
Optional<Account> maybeExistingAccount = accounts.get(number);
Device device = new Device();
device.setId(Device.MASTER_ID);
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
device.setName(accountAttributes.getName());
device.setCapabilities(accountAttributes.getCapabilities());
device.setCreated(System.currentTimeMillis());
device.setLastSeen(Util.todayInMillis());
device.setUserAgent(signalAgent);
Account account = new Account();
account.setNumber(number);
account.setUuid(UUID.randomUUID());
account.addDevice(device);
setAccountRegistrationLockFromAttributes(account, accountAttributes);
account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
account.setUnrestrictedUnidentifiedAccess(accountAttributes.isUnrestrictedUnidentifiedAccess());
account.setDiscoverableByPhoneNumber(accountAttributes.isDiscoverableByPhoneNumber());
if (accounts.create(account)) {
newUserMeter.mark();
}
directoryQueue.refreshRegisteredUser(account);
maybeExistingAccount.ifPresent(definitelyExistingAccount -> messagesManager.clear(definitelyExistingAccount.getUuid()));
pendingAccounts.remove(number);
return account;
}
private void setAccountRegistrationLockFromAttributes(Account account, @Valid AccountAttributes attributes) {
if (!Util.isEmpty(attributes.getPin())) {
account.setPin(attributes.getPin());
} else if (!Util.isEmpty(attributes.getRegistrationLock())) {
AuthenticationCredentials credentials = new AuthenticationCredentials(attributes.getRegistrationLock());
account.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt());
} else {
account.setPin(null);
account.setRegistrationLock(null, null);
}
}
@VisibleForTesting protected
VerificationCode generateVerificationCode(String number) {
if (testDevices.containsKey(number)) {

View File

@@ -1,29 +1,27 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import com.amazonaws.HttpMethod;
import com.codahale.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV1;
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.s3.UrlSigner;
import org.whispersystems.textsecuregcm.storage.Account;
import io.dropwizard.auth.Auth;
import java.io.IOException;
import java.net.URL;
import java.util.stream.Stream;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.net.URL;
import java.util.stream.Stream;
import io.dropwizard.auth.Auth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV1;
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.s3.UrlSigner;
@Path("/v1/attachments")
@@ -35,25 +33,25 @@ public class AttachmentControllerV1 extends AttachmentControllerBase {
private static final String[] UNACCELERATED_REGIONS = {"+20", "+971", "+968", "+974"};
private final RateLimiters rateLimiters;
private final UrlSigner urlSigner;
private final UrlSigner urlSigner;
public AttachmentControllerV1(RateLimiters rateLimiters, String accessKey, String accessSecret, String bucket) {
this.rateLimiters = rateLimiters;
this.urlSigner = new UrlSigner(accessKey, accessSecret, bucket);
this.urlSigner = new UrlSigner(accessKey, accessSecret, bucket);
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
public AttachmentDescriptorV1 allocateAttachment(@Auth Account account)
throws RateLimitExceededException
{
if (account.isRateLimited()) {
rateLimiters.getAttachmentLimiter().validate(account.getNumber());
public AttachmentDescriptorV1 allocateAttachment(@Auth AuthenticatedAccount auth)
throws RateLimitExceededException {
if (auth.getAccount().isRateLimited()) {
rateLimiters.getAttachmentLimiter().validate(auth.getAccount().getUuid());
}
long attachmentId = generateAttachmentId();
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.PUT, Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> account.getNumber().startsWith(region)));
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.PUT,
Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> auth.getAccount().getNumber().startsWith(region)));
return new AttachmentDescriptorV1(attachmentId, url.toExternalForm());
@@ -63,11 +61,11 @@ public class AttachmentControllerV1 extends AttachmentControllerBase {
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/{attachmentId}")
public AttachmentUri redirectToAttachment(@Auth Account account,
@PathParam("attachmentId") long attachmentId)
throws IOException
{
return new AttachmentUri(urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET, Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> account.getNumber().startsWith(region))));
public AttachmentUri redirectToAttachment(@Auth AuthenticatedAccount auth,
@PathParam("attachmentId") long attachmentId)
throws IOException {
return new AttachmentUri(urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET,
Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> auth.getAccount().getNumber().startsWith(region))));
}
}

View File

@@ -1,28 +1,26 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV2;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.s3.PolicySigner;
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.Pair;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import io.dropwizard.auth.Auth;
@Path("/v2/attachments")
public class AttachmentControllerV2 extends AttachmentControllerBase {
@@ -40,19 +38,20 @@ public class AttachmentControllerV2 extends AttachmentControllerBase {
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/form/upload")
public AttachmentDescriptorV2 getAttachmentUploadForm(@Auth Account account) throws RateLimitExceededException {
rateLimiter.validate(account.getNumber());
public AttachmentDescriptorV2 getAttachmentUploadForm(@Auth AuthenticatedAccount auth)
throws RateLimitExceededException {
rateLimiter.validate(auth.getAccount().getUuid());
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
long attachmentId = generateAttachmentId();
String objectName = String.valueOf(attachmentId);
Pair<String, String> policy = policyGenerator.createFor(now, String.valueOf(objectName), 100 * 1024 * 1024);
String signature = policySigner.getSignature(now, policy.second());
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
long attachmentId = generateAttachmentId();
String objectName = String.valueOf(attachmentId);
Pair<String, String> policy = policyGenerator.createFor(now, String.valueOf(objectName), 100 * 1024 * 1024);
String signature = policySigner.getSignature(now, policy.second());
return new AttachmentDescriptorV2(attachmentId, objectName, policy.first(),
"private", "AWS4-HMAC-SHA256",
now.format(PostPolicyGenerator.AWS_DATE_TIME),
policy.second(), signature);
"private", "AWS4-HMAC-SHA256",
now.format(PostPolicyGenerator.AWS_DATE_TIME),
policy.second(), signature);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -7,19 +7,6 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV3;
import org.whispersystems.textsecuregcm.gcp.CanonicalRequest;
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestGenerator;
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestSigner;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import javax.annotation.Nonnull;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
@@ -29,6 +16,18 @@ import java.time.ZonedDateTime;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV3;
import org.whispersystems.textsecuregcm.gcp.CanonicalRequest;
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestGenerator;
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestSigner;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
@Path("/v3/attachments")
public class AttachmentControllerV3 extends AttachmentControllerBase {
@@ -45,26 +44,29 @@ public class AttachmentControllerV3 extends AttachmentControllerBase {
@Nonnull
private final SecureRandom secureRandom;
public AttachmentControllerV3(@Nonnull RateLimiters rateLimiters, @Nonnull String domain, @Nonnull String email, int maxSizeInBytes, @Nonnull String pathPrefix, @Nonnull String rsaSigningKey)
public AttachmentControllerV3(@Nonnull RateLimiters rateLimiters, @Nonnull String domain, @Nonnull String email,
int maxSizeInBytes, @Nonnull String pathPrefix, @Nonnull String rsaSigningKey)
throws IOException, InvalidKeyException, InvalidKeySpecException {
this.rateLimiter = rateLimiters.getAttachmentLimiter();
this.rateLimiter = rateLimiters.getAttachmentLimiter();
this.canonicalRequestGenerator = new CanonicalRequestGenerator(domain, email, maxSizeInBytes, pathPrefix);
this.canonicalRequestSigner = new CanonicalRequestSigner(rsaSigningKey);
this.secureRandom = new SecureRandom();
this.canonicalRequestSigner = new CanonicalRequestSigner(rsaSigningKey);
this.secureRandom = new SecureRandom();
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/form/upload")
public AttachmentDescriptorV3 getAttachmentUploadForm(@Auth Account account) throws RateLimitExceededException {
rateLimiter.validate(account.getNumber());
public AttachmentDescriptorV3 getAttachmentUploadForm(@Auth AuthenticatedAccount auth)
throws RateLimitExceededException {
rateLimiter.validate(auth.getAccount().getUuid());
final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
final String key = generateAttachmentKey();
final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
final String key = generateAttachmentKey();
final CanonicalRequest canonicalRequest = canonicalRequestGenerator.createFor(key, now);
return new AttachmentDescriptorV3(2, key, getHeaderMap(canonicalRequest), getSignedUploadLocation(canonicalRequest));
return new AttachmentDescriptorV3(2, key, getHeaderMap(canonicalRequest),
getSignedUploadLocation(canonicalRequest));
}
private String getSignedUploadLocation(@Nonnull CanonicalRequest canonicalRequest) {
@@ -74,11 +76,10 @@ public class AttachmentControllerV3 extends AttachmentControllerBase {
}
private static Map<String, String> getHeaderMap(@Nonnull CanonicalRequest canonicalRequest) {
Map<String, String> result = new HashMap<>(3);
result.put("host", canonicalRequest.getDomain());
result.put("x-goog-content-length-range", "1," + canonicalRequest.getMaxSizeInBytes());
result.put("x-goog-resumable", "start");
return result;
return Map.of(
"host", canonicalRequest.getDomain(),
"x-goog-content-length-range", "1," + canonicalRequest.getMaxSizeInBytes(),
"x-goog-resumable", "start");
}
private String generateAttachmentKey() {

View File

@@ -1,21 +1,20 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import static com.codahale.metrics.MetricRegistry.name;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import org.signal.zkgroup.auth.ServerZkAuthOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
import org.whispersystems.textsecuregcm.entities.GroupCredentials;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.Util;
import io.micrometer.core.instrument.Metrics;
import java.security.InvalidKeyException;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@@ -24,65 +23,76 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import org.signal.zkgroup.auth.ServerZkAuthOperations;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
import org.whispersystems.textsecuregcm.entities.GroupCredentials;
import org.whispersystems.textsecuregcm.util.Util;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v1/certificate")
public class CertificateController {
private final Logger logger = LoggerFactory.getLogger(CertificateController.class);
private final CertificateGenerator certificateGenerator;
private final ServerZkAuthOperations serverZkAuthOperations;
private final boolean isZkEnabled;
public CertificateController(CertificateGenerator certificateGenerator, ServerZkAuthOperations serverZkAuthOperations, 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) {
this.certificateGenerator = certificateGenerator;
this.serverZkAuthOperations = serverZkAuthOperations;
this.isZkEnabled = isZkEnabled;
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/delivery")
public DeliveryCertificate getDeliveryCertificate(@Auth Account account,
@QueryParam("includeE164") Optional<Boolean> includeE164)
throws IOException, InvalidKeyException
{
if (account.getAuthenticatedDevice().isEmpty()) {
throw new AssertionError();
}
if (Util.isEmpty(account.getIdentityKey())) {
public DeliveryCertificate getDeliveryCertificate(@Auth AuthenticatedAccount auth,
@QueryParam("includeE164") Optional<Boolean> maybeIncludeE164)
throws InvalidKeyException {
if (Util.isEmpty(auth.getAccount().getIdentityKey())) {
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
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(auth.getAccount(), auth.getAuthenticatedDevice(), includeE164));
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/group/{startRedemptionTime}/{endRedemptionTime}")
public GroupCredentials getAuthenticationCredentials(@Auth Account account,
@PathParam("startRedemptionTime") int startRedemptionTime,
@PathParam("endRedemptionTime") int endRedemptionTime)
{
if (!isZkEnabled) throw new WebApplicationException(Response.Status.NOT_FOUND);
if (startRedemptionTime > endRedemptionTime) throw new WebApplicationException(Response.Status.BAD_REQUEST);
if (endRedemptionTime > Util.currentDaysSinceEpoch() + 7) throw new WebApplicationException(Response.Status.BAD_REQUEST);
if (startRedemptionTime < Util.currentDaysSinceEpoch()) throw new WebApplicationException(Response.Status.BAD_REQUEST);
public GroupCredentials getAuthenticationCredentials(@Auth AuthenticatedAccount auth,
@PathParam("startRedemptionTime") int startRedemptionTime,
@PathParam("endRedemptionTime") int endRedemptionTime,
@QueryParam("identity") Optional<String> identityType) {
if (startRedemptionTime > endRedemptionTime) {
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
if (endRedemptionTime > Util.currentDaysSinceEpoch() + 7) {
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
if (startRedemptionTime < Util.currentDaysSinceEpoch()) {
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
List<GroupCredentials.GroupCredential> credentials = new LinkedList<>();
for (int i=startRedemptionTime;i<=endRedemptionTime;i++) {
credentials.add(new GroupCredentials.GroupCredential(serverZkAuthOperations.issueAuthCredential(account.getUuid(), i)
.serialize(),
i));
final UUID identifier = identityType.map(String::toLowerCase).orElse("aci").equals("pni") ?
auth.getAccount().getPhoneNumberIdentifier() :
auth.getAccount().getUuid();
for (int i = startRedemptionTime; i <= endRedemptionTime; i++) {
credentials.add(new GroupCredentials.GroupCredential(
serverZkAuthOperations.issueAuthCredential(identifier, i).serialize(),
i));
}
return new GroupCredentials(credentials);

View File

@@ -17,12 +17,12 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.entities.AnswerChallengeRequest;
import org.whispersystems.textsecuregcm.entities.AnswerPushChallengeRequest;
import org.whispersystems.textsecuregcm.entities.AnswerRecaptchaChallengeRequest;
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.ForwardedIpUtil;
@Path("/v1/challenge")
@@ -38,7 +38,7 @@ public class ChallengeController {
@PUT
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response handleChallengeResponse(@Auth final Account account,
public Response handleChallengeResponse(@Auth final AuthenticatedAccount auth,
@Valid final AnswerChallengeRequest answerRequest,
@HeaderParam("X-Forwarded-For") String forwardedFor) throws RetryLaterException {
@@ -46,14 +46,15 @@ public class ChallengeController {
if (answerRequest instanceof AnswerPushChallengeRequest) {
final AnswerPushChallengeRequest pushChallengeRequest = (AnswerPushChallengeRequest) answerRequest;
rateLimitChallengeManager.answerPushChallenge(account, pushChallengeRequest.getChallenge());
rateLimitChallengeManager.answerPushChallenge(auth.getAccount(), pushChallengeRequest.getChallenge());
} else if (answerRequest instanceof AnswerRecaptchaChallengeRequest) {
try {
final AnswerRecaptchaChallengeRequest recaptchaChallengeRequest = (AnswerRecaptchaChallengeRequest) answerRequest;
final String mostRecentProxy = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow();
rateLimitChallengeManager.answerRecaptchaChallenge(account, recaptchaChallengeRequest.getCaptcha(), mostRecentProxy);
rateLimitChallengeManager.answerRecaptchaChallenge(auth.getAccount(), recaptchaChallengeRequest.getCaptcha(),
mostRecentProxy);
} catch (final NoSuchElementException e) {
return Response.status(400).build();
@@ -69,9 +70,9 @@ public class ChallengeController {
@Timed
@POST
@Path("/push")
public Response requestPushChallenge(@Auth final Account account) {
public Response requestPushChallenge(@Auth final AuthenticatedAccount auth) {
try {
rateLimitChallengeManager.sendPushChallenge(account);
rateLimitChallengeManager.sendPushChallenge(auth.getAccount());
return Response.status(200).build();
} catch (final NotPushRegisteredException e) {
return Response.status(404).build();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
@@ -7,29 +7,11 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.auth.Auth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
import org.whispersystems.textsecuregcm.entities.DeviceInfoList;
import org.whispersystems.textsecuregcm.entities.DeviceResponse;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@@ -40,39 +22,56 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.glassfish.jersey.server.ContainerRequest;
import org.whispersystems.textsecuregcm.auth.AuthEnablementRefreshRequirementProvider;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
import org.whispersystems.textsecuregcm.entities.DeviceInfoList;
import org.whispersystems.textsecuregcm.entities.DeviceResponse;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
@Path("/v1/devices")
public class DeviceController {
private final Logger logger = LoggerFactory.getLogger(DeviceController.class);
private static final int MAX_DEVICES = 6;
private final PendingDevicesManager pendingDevices;
private final StoredVerificationCodeManager pendingDevices;
private final AccountsManager accounts;
private final MessagesManager messages;
private final Keys keys;
private final RateLimiters rateLimiters;
private final Map<String, Integer> maxDeviceConfiguration;
private final DirectoryQueue directoryQueue;
public DeviceController(PendingDevicesManager pendingDevices,
public DeviceController(StoredVerificationCodeManager pendingDevices,
AccountsManager accounts,
MessagesManager messages,
DirectoryQueue directoryQueue,
Keys keys,
RateLimiters rateLimiters,
Map<String, Integer> maxDeviceConfiguration)
{
this.pendingDevices = pendingDevices;
this.accounts = accounts;
this.messages = messages;
this.directoryQueue = directoryQueue;
this.keys = keys;
this.rateLimiters = rateLimiters;
this.maxDeviceConfiguration = maxDeviceConfiguration;
}
@@ -80,12 +79,12 @@ public class DeviceController {
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
public DeviceInfoList getDevices(@Auth Account account) {
public DeviceInfoList getDevices(@Auth AuthenticatedAccount auth) {
List<DeviceInfo> devices = new LinkedList<>();
for (Device device : account.getDevices()) {
for (Device device : auth.getAccount().getDevices()) {
devices.add(new DeviceInfo(device.getId(), device.getName(),
device.getLastSeen(), device.getCreated()));
device.getLastSeen(), device.getCreated()));
}
return new DeviceInfoList(devices);
@@ -94,14 +93,17 @@ public class DeviceController {
@Timed
@DELETE
@Path("/{device_id}")
public void removeDevice(@Auth Account account, @PathParam("device_id") long deviceId) {
if (account.getAuthenticatedDevice().get().getId() != Device.MASTER_ID) {
@ChangesDeviceEnabledState
public void removeDevice(@Auth AuthenticatedAccount auth, @PathParam("device_id") long deviceId) {
Account account = auth.getAccount();
if (auth.getAuthenticatedDevice().getId() != Device.MASTER_ID) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
account.removeDevice(deviceId);
accounts.update(account);
directoryQueue.refreshRegisteredUser(account);
messages.clear(account.getUuid(), deviceId);
account = accounts.update(account, a -> a.removeDevice(deviceId));
keys.delete(account.getUuid(), deviceId);
// ensure any messages that came in after the first clear() are also removed
messages.clear(account.getUuid(), deviceId);
}
@@ -109,10 +111,12 @@ public class DeviceController {
@GET
@Path("/provisioning/code")
@Produces(MediaType.APPLICATION_JSON)
public VerificationCode createDeviceToken(@Auth Account account)
throws RateLimitExceededException, DeviceLimitExceededException
{
rateLimiters.getAllocateDeviceLimiter().validate(account.getNumber());
public VerificationCode createDeviceToken(@Auth AuthenticatedAccount auth)
throws RateLimitExceededException, DeviceLimitExceededException {
final Account account = auth.getAccount();
rateLimiters.getAllocateDeviceLimiter().validate(account.getUuid());
int maxDeviceLimit = MAX_DEVICES;
@@ -124,13 +128,14 @@ public class DeviceController {
throw new DeviceLimitExceededException(account.getDevices().size(), MAX_DEVICES);
}
if (account.getAuthenticatedDevice().get().getId() != Device.MASTER_ID) {
if (auth.getAuthenticatedDevice().getId() != Device.MASTER_ID) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
VerificationCode verificationCode = generateVerificationCode();
StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(),
System.currentTimeMillis(),
null,
null);
pendingDevices.store(account.getNumber(), storedVerificationCode);
@@ -143,86 +148,87 @@ public class DeviceController {
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Path("/{verification_code}")
@ChangesDeviceEnabledState
public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode,
@HeaderParam("Authorization") String authorizationHeader,
@HeaderParam("User-Agent") String userAgent,
@Valid AccountAttributes accountAttributes)
@HeaderParam("Authorization") BasicAuthorizationHeader authorizationHeader,
@HeaderParam("User-Agent") String userAgent,
@Valid AccountAttributes accountAttributes,
@Context ContainerRequest containerRequest)
throws RateLimitExceededException, DeviceLimitExceededException
{
try {
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
String number = header.getIdentifier().getNumber();
String password = header.getPassword();
if (number == null) throw new WebApplicationException(400);
String number = authorizationHeader.getUsername();
String password = authorizationHeader.getPassword();
rateLimiters.getVerifyDeviceLimiter().validate(number);
rateLimiters.getVerifyDeviceLimiter().validate(number);
Optional<StoredVerificationCode> storedVerificationCode = pendingDevices.getCodeForNumber(number);
Optional<StoredVerificationCode> storedVerificationCode = pendingDevices.getCodeForNumber(number);
if (!storedVerificationCode.isPresent() || !storedVerificationCode.get().isValid(verificationCode)) {
throw new WebApplicationException(Response.status(403).build());
}
Optional<Account> account = accounts.get(number);
if (!account.isPresent()) {
throw new WebApplicationException(Response.status(403).build());
}
int maxDeviceLimit = MAX_DEVICES;
if (maxDeviceConfiguration.containsKey(account.get().getNumber())) {
maxDeviceLimit = maxDeviceConfiguration.get(account.get().getNumber());
}
if (account.get().getEnabledDeviceCount() >= maxDeviceLimit) {
throw new DeviceLimitExceededException(account.get().getDevices().size(), MAX_DEVICES);
}
final DeviceCapabilities capabilities = accountAttributes.getCapabilities();
if (capabilities != null && isCapabilityDowngrade(account.get(), capabilities, userAgent)) {
throw new WebApplicationException(Response.status(409).build());
}
Device device = new Device();
device.setName(accountAttributes.getName());
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setId(account.get().getNextDeviceId());
device.setRegistrationId(accountAttributes.getRegistrationId());
device.setLastSeen(Util.todayInMillis());
device.setCreated(System.currentTimeMillis());
device.setCapabilities(accountAttributes.getCapabilities());
account.get().addDevice(device);
messages.clear(account.get().getUuid(), device.getId());
accounts.update(account.get());
pendingDevices.remove(number);
return new DeviceResponse(device.getId());
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad Authorization Header", e);
throw new WebApplicationException(Response.status(401).build());
if (!storedVerificationCode.isPresent() || !storedVerificationCode.get().isValid(verificationCode)) {
throw new WebApplicationException(Response.status(403).build());
}
Optional<Account> account = accounts.getByE164(number);
if (!account.isPresent()) {
throw new WebApplicationException(Response.status(403).build());
}
// Normally, the the "do we need to refresh somebody's websockets" listener can do this on its own. In this case,
// we're not using the conventional authentication system, and so we need to give it a hint so it knows who the
// active user is and what their device states look like.
AuthEnablementRefreshRequirementProvider.setAccount(containerRequest, account.get());
int maxDeviceLimit = MAX_DEVICES;
if (maxDeviceConfiguration.containsKey(account.get().getNumber())) {
maxDeviceLimit = maxDeviceConfiguration.get(account.get().getNumber());
}
if (account.get().getEnabledDeviceCount() >= maxDeviceLimit) {
throw new DeviceLimitExceededException(account.get().getDevices().size(), MAX_DEVICES);
}
final DeviceCapabilities capabilities = accountAttributes.getCapabilities();
if (capabilities != null && isCapabilityDowngrade(account.get(), capabilities, userAgent)) {
throw new WebApplicationException(Response.status(409).build());
}
Device device = new Device();
device.setName(accountAttributes.getName());
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
device.setLastSeen(Util.todayInMillis());
device.setCreated(System.currentTimeMillis());
device.setCapabilities(accountAttributes.getCapabilities());
final Account updatedAccount = accounts.update(account.get(), a -> {
device.setId(a.getNextDeviceId());
messages.clear(a.getUuid(), device.getId());
a.addDevice(device);
});
pendingDevices.remove(number);
return new DeviceResponse(updatedAccount.getUuid(), updatedAccount.getPhoneNumberIdentifier(), device.getId());
}
@Timed
@PUT
@Path("/unauthenticated_delivery")
public void setUnauthenticatedDelivery(@Auth Account account) {
assert(account.getAuthenticatedDevice().isPresent());
public void setUnauthenticatedDelivery(@Auth AuthenticatedAccount auth) {
assert (auth.getAuthenticatedDevice() != null);
// Deprecated
}
@Timed
@PUT
@Path("/capabilities")
public void setCapabiltities(@Auth Account account, @Valid DeviceCapabilities capabilities) {
assert(account.getAuthenticatedDevice().isPresent());
account.getAuthenticatedDevice().get().setCapabilities(capabilities);
accounts.update(account);
public void setCapabiltities(@Auth AuthenticatedAccount auth, @Valid DeviceCapabilities capabilities) {
assert (auth.getAuthenticatedDevice() != null);
final long deviceId = auth.getAuthenticatedDevice().getId();
accounts.updateDevice(auth.getAccount(), deviceId, d -> d.setCapabilities(capabilities));
}
@VisibleForTesting protected VerificationCode generateVerificationCode() {
@@ -234,13 +240,10 @@ public class DeviceController {
private boolean isCapabilityDowngrade(Account account, DeviceCapabilities capabilities, String userAgent) {
boolean isDowngrade = false;
if (account.isSenderKeySupported() && !capabilities.isSenderKey()) {
isDowngrade = true;
}
if (account.isGv1MigrationSupported() && !capabilities.isGv1Migration()) {
isDowngrade = true;
}
isDowngrade |= account.isChangeNumberSupported() && !capabilities.isChangeNumber();
isDowngrade |= account.isAnnouncementGroupSupported() && !capabilities.isAnnouncementGroup();
isDowngrade |= account.isSenderKeySupported() && !capabilities.isSenderKey();
isDowngrade |= account.isGv1MigrationSupported() && !capabilities.isGv1Migration();
if (account.isGroupsV2Supported()) {
try {

View File

@@ -1,14 +1,11 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.storage.Account;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
@@ -16,6 +13,8 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
@Path("/v1/directory")
public class DirectoryController {
@@ -30,15 +29,15 @@ public class DirectoryController {
@GET
@Path("/auth")
@Produces(MediaType.APPLICATION_JSON)
public Response getAuthToken(@Auth Account account) {
return Response.ok().entity(directoryServiceTokenGenerator.generateFor(account.getNumber())).build();
public Response getAuthToken(@Auth AuthenticatedAccount auth) {
return Response.ok().entity(directoryServiceTokenGenerator.generateFor(auth.getAccount().getNumber())).build();
}
@PUT
@Path("/feedback-v3/{status}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response setFeedback(@Auth Account account) {
public Response setFeedback(@Auth AuthenticatedAccount auth) {
return Response.ok().build();
}
@@ -47,7 +46,7 @@ public class DirectoryController {
@GET
@Path("/{token}")
@Produces(MediaType.APPLICATION_JSON)
public Response getTokenPresence(@Auth Account account) {
public Response getTokenPresence(@Auth AuthenticatedAccount auth) {
return Response.status(429).build();
}
@@ -56,7 +55,7 @@ public class DirectoryController {
@Path("/tokens")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response getContactIntersection(@Auth Account account) {
public Response getContactIntersection(@Auth AuthenticatedAccount auth) {
return Response.status(429).build();
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import java.util.Base64;
import java.util.UUID;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.util.ByteUtil;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
import org.whispersystems.textsecuregcm.util.Util;
@Path("/v2/directory")
public class DirectoryV2Controller {
private final ExternalServiceCredentialGenerator directoryServiceTokenGenerator;
public DirectoryV2Controller(ExternalServiceCredentialGenerator userTokenGenerator) {
this.directoryServiceTokenGenerator = userTokenGenerator;
}
@Timed
@GET
@Path("/auth")
@Produces(MediaType.APPLICATION_JSON)
public Response getAuthToken(@Auth AuthenticatedAccount auth) {
final UUID uuid = auth.getAccount().getUuid();
final String e164 = auth.getAccount().getNumber();
final long e164AsLong = Long.parseLong(e164, e164.indexOf('+'), e164.length() - 1, 10);
final byte[] uuidAndNumber = ByteUtil.combine(UUIDUtil.toBytes(uuid), Util.longToByteArray(e164AsLong));
final String username = Base64.getEncoder().encodeToString(uuidAndNumber);
final ExternalServiceCredentials credentials = directoryServiceTokenGenerator.generateFor(username);
return Response.ok().entity(credentials).build();
}
}

View File

@@ -18,13 +18,22 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinPool.ManagedBlocker;
import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
@@ -32,30 +41,68 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.receipts.ReceiptCredentialPresentation;
import org.signal.zkgroup.receipts.ReceiptSerial;
import org.signal.zkgroup.receipts.ServerZkReceiptOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
import org.whispersystems.textsecuregcm.configuration.DonationConfiguration;
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
import org.whispersystems.textsecuregcm.entities.ApplePayAuthorizationRequest;
import org.whispersystems.textsecuregcm.entities.ApplePayAuthorizationResponse;
import org.whispersystems.textsecuregcm.entities.RedeemReceiptRequest;
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
import org.whispersystems.textsecuregcm.http.FormDataBodyPublisher;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountBadge;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager;
import org.whispersystems.textsecuregcm.util.SystemMapper;
@Path("/v1/donation")
public class DonationController {
private final Logger logger = LoggerFactory.getLogger(DonationController.class);
public interface ReceiptCredentialPresentationFactory {
ReceiptCredentialPresentation build(byte[] bytes) throws InvalidInputException;
}
private static final Logger logger = LoggerFactory.getLogger(DonationController.class);
private final Clock clock;
private final ServerZkReceiptOperations serverZkReceiptOperations;
private final RedeemedReceiptsManager redeemedReceiptsManager;
private final AccountsManager accountsManager;
private final BadgesConfiguration badgesConfiguration;
private final ReceiptCredentialPresentationFactory receiptCredentialPresentationFactory;
private final URI uri;
private final String apiKey;
private final String description;
private final Set<String> supportedCurrencies;
private final FaultTolerantHttpClient httpClient;
public DonationController(final Executor executor, final DonationConfiguration configuration) {
public DonationController(
@Nonnull final Clock clock,
@Nonnull final ServerZkReceiptOperations serverZkReceiptOperations,
@Nonnull final RedeemedReceiptsManager redeemedReceiptsManager,
@Nonnull final AccountsManager accountsManager,
@Nonnull final BadgesConfiguration badgesConfiguration,
@Nonnull final ReceiptCredentialPresentationFactory receiptCredentialPresentationFactory,
@Nonnull final Executor httpClientExecutor,
@Nonnull final DonationConfiguration configuration,
@Nonnull final StripeConfiguration stripeConfiguration) {
this.clock = Objects.requireNonNull(clock);
this.serverZkReceiptOperations = Objects.requireNonNull(serverZkReceiptOperations);
this.redeemedReceiptsManager = Objects.requireNonNull(redeemedReceiptsManager);
this.accountsManager = Objects.requireNonNull(accountsManager);
this.badgesConfiguration = Objects.requireNonNull(badgesConfiguration);
this.receiptCredentialPresentationFactory = Objects.requireNonNull(receiptCredentialPresentationFactory);
this.uri = URI.create(configuration.getUri());
this.apiKey = configuration.getApiKey();
this.apiKey = stripeConfiguration.getApiKey();
this.description = configuration.getDescription();
this.supportedCurrencies = configuration.getSupportedCurrencies();
this.httpClient = FaultTolerantHttpClient.newBuilder()
@@ -64,18 +111,88 @@ public class DonationController {
.withVersion(Version.HTTP_2)
.withConnectTimeout(Duration.ofSeconds(10))
.withRedirect(Redirect.NEVER)
.withExecutor(executor)
.withExecutor(Objects.requireNonNull(httpClientExecutor))
.withName("donation")
.withSecurityProtocol(FaultTolerantHttpClient.SECURITY_PROTOCOL_TLS_1_3)
.build();
}
@Timed
@POST
@Path("/redeem-receipt")
@Consumes(MediaType.APPLICATION_JSON)
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
public CompletionStage<Response> redeemReceipt(
@Auth final AuthenticatedAccount auth,
@Valid final RedeemReceiptRequest request) {
return CompletableFuture.supplyAsync(() -> {
ReceiptCredentialPresentation receiptCredentialPresentation;
try {
receiptCredentialPresentation = receiptCredentialPresentationFactory.build(
request.getReceiptCredentialPresentation());
} catch (InvalidInputException e) {
return CompletableFuture.completedFuture(Response.status(Status.BAD_REQUEST).entity("invalid receipt credential presentation").type(MediaType.TEXT_PLAIN_TYPE).build());
}
try {
serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation);
} catch (VerificationFailedException e) {
return CompletableFuture.completedFuture(Response.status(Status.BAD_REQUEST).entity("receipt credential presentation verification failed").type(MediaType.TEXT_PLAIN_TYPE).build());
}
final ReceiptSerial receiptSerial = receiptCredentialPresentation.getReceiptSerial();
final Instant receiptExpiration = Instant.ofEpochSecond(receiptCredentialPresentation.getReceiptExpirationTime());
final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();
final String badgeId = badgesConfiguration.getReceiptLevels().get(receiptLevel);
if (badgeId == null) {
return CompletableFuture.completedFuture(Response.serverError().entity("server does not recognize the requested receipt level").type(MediaType.TEXT_PLAIN_TYPE).build());
}
final CompletionStage<Boolean> putStage = redeemedReceiptsManager.put(
receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, auth.getAccount().getUuid());
return putStage.thenApplyAsync(receiptMatched -> {
if (!receiptMatched) {
return Response.status(Status.BAD_REQUEST).entity("receipt serial is already redeemed").type(MediaType.TEXT_PLAIN_TYPE).build();
}
try {
ForkJoinPool.managedBlock(new ManagedBlocker() {
boolean done = false;
@Override
public boolean block() {
final Optional<Account> optionalAccount = accountsManager.getByAccountIdentifier(auth.getAccount().getUuid());
optionalAccount.ifPresent(account -> {
accountsManager.update(account, a -> {
a.addBadge(clock, new AccountBadge(badgeId, receiptExpiration, request.isVisible()));
if (request.isPrimary()) {
a.makeBadgePrimaryIfExists(clock, badgeId);
}
});
});
done = true;
return true;
}
@Override
public boolean isReleasable() {
return done;
}
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return Response.serverError().build();
}
return Response.ok().build();
});
}).thenCompose(Function.identity());
}
@Timed
@POST
@Path("/authorize-apple-pay")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public CompletableFuture<Response> getApplePayAuthorization(@Auth Account account, @Valid ApplePayAuthorizationRequest request) {
public CompletableFuture<Response> getApplePayAuthorization(@Auth AuthenticatedAccount auth, @Valid ApplePayAuthorizationRequest request) {
if (!supportedCurrencies.contains(request.getCurrency())) {
return CompletableFuture.completedFuture(Response.status(422).build());
}

View File

@@ -1,28 +1,27 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import static com.codahale.metrics.MetricRegistry.name;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Metrics;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
import org.whispersystems.websocket.session.WebSocketSession;
import org.whispersystems.websocket.session.WebSocketSessionContext;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import static com.codahale.metrics.MetricRegistry.name;
@Path("/v1/keepalive")
public class KeepAliveController {
@@ -40,15 +39,14 @@ public class KeepAliveController {
@Timed
@GET
public Response getKeepAlive(@Auth Account account,
@WebSocketSession WebSocketSessionContext context)
{
if (account != null) {
if (!clientPresenceManager.isLocallyPresent(account.getUuid(), account.getAuthenticatedDevice().get().getId())) {
public Response getKeepAlive(@Auth AuthenticatedAccount auth,
@WebSocketSession WebSocketSessionContext context) {
if (auth != null) {
if (!clientPresenceManager.isLocallyPresent(auth.getAccount().getUuid(), auth.getAuthenticatedDevice().getId())) {
logger.warn("***** No local subscription found for {}::{}; age = {}ms, User-Agent = {}",
account.getUuid(), account.getAuthenticatedDevice().get().getId(),
System.currentTimeMillis() - context.getClient().getCreatedTimestamp(),
context.getClient().getUserAgent());
auth.getAccount().getUuid(), auth.getAuthenticatedDevice().getId(),
System.currentTimeMillis() - context.getClient().getCreatedTimestamp(),
context.getClient().getUserAgent());
context.getClient().close(1000, "OK");

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