Compare commits
74
Commits
dockerfile
...
v6.89.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf1190696e | ||
|
|
71dd0890de | ||
|
|
e5acdf1402 | ||
|
|
0f08b6bb59 | ||
|
|
6198a7b69a | ||
|
|
067aee6664 | ||
|
|
138a2ebbd0 | ||
|
|
296f6a7a88 | ||
|
|
069ffa9921 | ||
|
|
f42fd8a840 | ||
|
|
10f27af6f2 | ||
|
|
0bbd34d060 | ||
|
|
282daeb0dc | ||
|
|
d33b313c11 | ||
|
|
fb7316c9ae | ||
|
|
279b0a51d9 | ||
|
|
6547d5ebf3 | ||
|
|
4f1ef9a039 | ||
|
|
4c80714d19 | ||
|
|
077ead71a5 | ||
|
|
caba110266 | ||
|
|
0fdb23c1e9 | ||
|
|
13a84f0c72 | ||
|
|
669bd58e33 | ||
|
|
6e82740a9b | ||
|
|
7ea43a728d | ||
|
|
71b38356b1 | ||
|
|
5a99708f56 | ||
|
|
24191d9599 | ||
|
|
482ea8eb40 | ||
|
|
1dae05651f | ||
|
|
5164e92538 | ||
|
|
f89a20dbc7 | ||
|
|
3a4c5a2bfb | ||
|
|
5e1334e8de | ||
|
|
fa6e3d3690 | ||
|
|
9383e7716b | ||
|
|
cfe34fbf0f | ||
|
|
9fe110625c | ||
|
|
975f753c2b | ||
|
|
e6237480f8 | ||
|
|
966d4e29d4 | ||
|
|
26f876a2cb | ||
|
|
ab9e6ac48a | ||
|
|
c1d6c04ab2 | ||
|
|
888cec3d56 | ||
|
|
1461bcc2c2 | ||
|
|
11f1cf80bd | ||
|
|
c675cc8b26 | ||
|
|
0011b8925b | ||
|
|
73ea6e4251 | ||
|
|
e4441dddbb | ||
|
|
8d1d56f694 | ||
|
|
2015ba77ca | ||
|
|
7033a0f68f | ||
|
|
6ada76da7f | ||
|
|
cbdec0cb22 | ||
|
|
de6e9d31c9 | ||
|
|
f0a6be32fc | ||
|
|
5c4855cca6 | ||
|
|
2e1e380418 | ||
|
|
d07f0b4f71 | ||
|
|
aaa2a6eef1 | ||
|
|
b1f56c3324 | ||
|
|
da5c0ae4b6 | ||
|
|
1e1394560d | ||
|
|
bae0196bcf | ||
|
|
3398955c1a | ||
|
|
a1b925d1e0 | ||
|
|
31c0c3275f | ||
|
|
0a4392f700 | ||
|
|
eb86986cf4 | ||
|
|
1053a47e42 | ||
|
|
99b1f48e0e |
@@ -11,8 +11,8 @@ jobs:
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@3bc31aaf88e8fc94dc1e632d48af61be5ca8721c
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: 11
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
cache: 'maven'
|
||||
- name: Build with Maven
|
||||
run: mvn -e -B verify
|
||||
|
||||
+1
-1
Submodule abusive-message-filter updated: 56398ec89d...d20873c7d7
@@ -34,7 +34,7 @@
|
||||
<commons-codec.version>1.15</commons-codec.version>
|
||||
<commons-csv.version>1.8</commons-csv.version>
|
||||
<commons-io.version>2.9.0</commons-io.version>
|
||||
<dropwizard.version>2.0.22</dropwizard.version>
|
||||
<dropwizard.version>2.0.25</dropwizard.version>
|
||||
<dropwizard-metrics-datadog.version>1.1.13</dropwizard-metrics-datadog.version>
|
||||
<gson.version>2.8.8</gson.version>
|
||||
<guava.version>30.1.1-jre</guava.version>
|
||||
@@ -44,7 +44,7 @@
|
||||
<libphonenumber.version>8.12.33</libphonenumber.version>
|
||||
<logstash.logback.version>6.6</logstash.logback.version>
|
||||
<micrometer.version>1.5.3</micrometer.version>
|
||||
<mockito.version>3.11.1</mockito.version>
|
||||
<mockito.version>4.0.0</mockito.version>
|
||||
<netty.version>4.1.65.Final</netty.version>
|
||||
<netty.tcnative-boringssl-static.version>2.0.39.Final</netty.tcnative-boringssl-static.version>
|
||||
<opentest4j.version>1.2.0</opentest4j.version>
|
||||
@@ -337,6 +337,7 @@
|
||||
<version>0.6.1</version>
|
||||
<configuration>
|
||||
<protocArtifact>com.google.protobuf:protoc:3.18.0:exe:${os.detected.classifier}</protocArtifact>
|
||||
<checkStaleness>true</checkStaleness>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
@@ -353,8 +354,7 @@
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<source>11</source>
|
||||
<target>11</target>
|
||||
<release>17</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@@ -418,7 +418,7 @@
|
||||
<rules>
|
||||
<dependencyConvergence/>
|
||||
<requireMavenVersion>
|
||||
<version>3.0.0</version>
|
||||
<version>3.8.3</version>
|
||||
</requireMavenVersion>
|
||||
</rules>
|
||||
</configuration>
|
||||
|
||||
+271
-164
@@ -1,59 +1,67 @@
|
||||
# Example, relatively minimal, configuration that passes validation (see `io.dropwizard.cli.CheckCommand`)
|
||||
#
|
||||
# `unset` values will need to be set to work properly.
|
||||
# Most other values are technically valid for a local/demonstration environment, but are probably not production-ready.
|
||||
|
||||
stripe:
|
||||
apiKey:
|
||||
idempotencyKeyGenerator:
|
||||
apiKey: unset
|
||||
idempotencyKeyGenerator: abcdefg12345678= # base64 for creating request idempotency hash
|
||||
boostDescription: >
|
||||
Example
|
||||
|
||||
dynamoDbClientConfiguration:
|
||||
region: # AWS Region
|
||||
region: us-west-2 # AWS Region
|
||||
|
||||
dynamoDbTables:
|
||||
issuedReceipts:
|
||||
tableName: # DDB Table Name
|
||||
expiration: # Duration of time until rows expire
|
||||
generator: # random binary sequence
|
||||
tableName: Example_IssuedReceipts
|
||||
expiration: P30D # Duration of time until rows expire
|
||||
generator: abcdefg12345678= # random base64-encoded binary sequence
|
||||
redeemedReceipts:
|
||||
tableName: # DDB Table Name
|
||||
expiration: # Duration of time until rows expire
|
||||
tableName: Example_RedeemedReceipts
|
||||
expiration: P30D # Duration of time until rows expire
|
||||
subscriptions:
|
||||
tableName: # DDB Table Name
|
||||
tableName: Example_Subscriptions
|
||||
|
||||
twilio: # Twilio gateway configuration
|
||||
accountId:
|
||||
accountToken:
|
||||
nanpaMessagingServiceSid: # Twilio SID for the messaging service to use for NANPA.
|
||||
messagingServiceSid: # Twilio SID for the message service to use for non-NANPA.
|
||||
verifyServiceSid: # Twilio SID for a Verify service
|
||||
localDomain: # Domain Twilio can connect back to for calls. Should be domain of your service.
|
||||
accountId: unset
|
||||
accountToken: unset
|
||||
nanpaMessagingServiceSid: unset # Twilio SID for the messaging service to use for NANPA.
|
||||
messagingServiceSid: unset # Twilio SID for the message service to use for non-NANPA.
|
||||
verifyServiceSid: unset # Twilio SID for a Verify service
|
||||
localDomain: example.com # Domain Twilio can connect back to for calls. Should be domain of your service.
|
||||
defaultClientVerificationTexts:
|
||||
ios: # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1.
|
||||
androidNg: # Text to use for the verification message on android-ng client types. Will be passed to String.format with the verification code as argument 1.
|
||||
android202001: # Text to use for the verification message on android-2020-01 client types. Will be passed to String.format with the verification code as argument 1.
|
||||
android202103: # Text to use for the verification message on android-2021-03 client types. Will be passed to String.format with the verification code as argument 1.
|
||||
generic: # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1.
|
||||
ios: example %1$s # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1.
|
||||
androidNg: example %1$s # Text to use for the verification message on android-ng client types. Will be passed to String.format with the verification code as argument 1.
|
||||
android202001: example %1$s # Text to use for the verification message on android-2020-01 client types. Will be passed to String.format with the verification code as argument 1.
|
||||
android202103: example %1$s # Text to use for the verification message on android-2021-03 client types. Will be passed to String.format with the verification code as argument 1.
|
||||
generic: example %1$s # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1.
|
||||
regionalClientVerificationTexts: # Map of country codes to custom texts
|
||||
999: # example country code
|
||||
ios:
|
||||
# … all keys from defaultClientVerificationTexts are required
|
||||
androidAppHash: # Hash appended to Android
|
||||
verifyServiceFriendlyName: # Service name used in template. Requires Twilio account rep to enable
|
||||
ios: example %1$s # all keys from defaultClientVerificationTexts are required
|
||||
androidNg: example %1$s
|
||||
android202001: example %1$s
|
||||
android202103: example %1$s
|
||||
generic: example %1$s
|
||||
androidAppHash: example # Hash appended to Android
|
||||
verifyServiceFriendlyName: example # Service name used in template. Requires Twilio account rep to enable
|
||||
|
||||
push:
|
||||
queueSize: # Size of push pending queue
|
||||
queueSize: 1000 # Size of push pending queue
|
||||
|
||||
turn: # TURN server configuration
|
||||
secret: # TURN server secret
|
||||
secret: example # TURN server secret
|
||||
uris:
|
||||
- stun:yourdomain:80
|
||||
- stun:yourdomain.com:443
|
||||
- turn:yourdomain:443?transport=udp
|
||||
- turn:etc.com:80?transport=udp
|
||||
- stun:example.com:80
|
||||
- stun:another.example.com:443
|
||||
- turn:example.com:443?transport=udp
|
||||
- turn:ya.example.com:80?transport=udp
|
||||
|
||||
cacheCluster: # Redis server configuration for cache cluster
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
clientPresenceCluster: # Redis server configuration for client presence cluster
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
pubsub: # Redis server configuration for pubsub cluster
|
||||
url: redis://redis.example.com:6379/
|
||||
@@ -61,148 +69,190 @@ pubsub: # Redis server configuration for pubsub cluster
|
||||
- redis://redis.example.com:6379/
|
||||
|
||||
pushSchedulerCluster: # Redis server configuration for push scheduler cluster
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
rateLimitersCluster: # Redis server configuration for rate limiters cluster
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
directory:
|
||||
client: # Configuration for interfacing with Contact Discovery Service cluster
|
||||
userAuthenticationTokenSharedSecret: # hex-encoded secret shared with CDS used to generate auth tokens for Signal users
|
||||
userAuthenticationTokenUserIdSecret: # hex-encoded secret shared among Signal-Servers to obscure user phone numbers from CDS
|
||||
userAuthenticationTokenSharedSecret: 00000f # hex-encoded secret shared with CDS used to generate auth tokens for Signal users
|
||||
userAuthenticationTokenUserIdSecret: 00000f # hex-encoded secret shared among Signal-Servers to obscure user phone numbers from CDS
|
||||
sqs:
|
||||
accessKey: # AWS SQS accessKey
|
||||
accessSecret: # AWS SQS accessSecret
|
||||
queueUrls: # AWS SQS queue urls
|
||||
accessKey: test # AWS SQS accessKey
|
||||
accessSecret: test # AWS SQS accessSecret
|
||||
queueUrls: # AWS SQS queue urls
|
||||
- https://sqs.example.com/directory.fifo
|
||||
server: # One or more CDS servers
|
||||
- replicationName: # CDS replication name
|
||||
replicationUrl: # CDS replication endpoint base url
|
||||
replicationPassword: # CDS replication endpoint password
|
||||
replicationCaCertificate: # CDS replication endpoint TLS certificate trust root
|
||||
- replicationName: example # CDS replication name
|
||||
replicationUrl: cds.example.com # CDS replication endpoint base url
|
||||
replicationPassword: example # CDS replication endpoint password
|
||||
replicationCaCertificate: | # CDS replication endpoint TLS certificate trust root
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
directoryV2:
|
||||
client: # Configuration for interfacing with Contact Discovery Service v2 cluster
|
||||
userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth tokens for Signal users
|
||||
|
||||
messageCache: # Redis server configuration for message store cache
|
||||
persistDelayMinutes:
|
||||
|
||||
persistDelayMinutes: 1
|
||||
cluster:
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
metricsCluster:
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
messageDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
region: us-west-2
|
||||
tableName: Example_Messages
|
||||
|
||||
keysDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
region: us-west-2
|
||||
tableName: Example_PreKeys
|
||||
|
||||
accountsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
phoneNumberTableName:
|
||||
region: us-west-2
|
||||
tableName: Example_Accounts
|
||||
phoneNumberTableName: Example_Accounts_PhoneNumbers
|
||||
phoneNumberIdentifierTableName: Example_Accounts_PhoneNumberIdentifiers
|
||||
|
||||
deletedAccountsDynamoDb: # DynamoDb table configuration
|
||||
region:
|
||||
tableName:
|
||||
needsReconciliationIndexName:
|
||||
region: us-west-2
|
||||
tableName: Example_DeletedAccounts
|
||||
needsReconciliationIndexName: NeedsReconciliation
|
||||
|
||||
deletedAccountsLockDynamoDb: # DynamoDb table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
redeemedReceiptsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
expirationTime: # ISO8601 Duration
|
||||
|
||||
migrationDeletedAccountsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
migrationRetryAccountsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
region: us-west-2
|
||||
tableName: Example_DeletedAccountsLock
|
||||
|
||||
pendingAccountsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
region: us-west-2
|
||||
tableName: Example_PendingAccounts
|
||||
|
||||
pendingDevicesDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
region: us-west-2
|
||||
tableName: Example_PendingDevices
|
||||
|
||||
phoneNumberIdentifiersDynamoDb: # DynamoDB table configuration
|
||||
region: us-west-2
|
||||
tableName: Example_PhoneNumberIdentifiers
|
||||
|
||||
pushChallengeDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
region: us-west-2
|
||||
tableName: Example_PushChallenges
|
||||
|
||||
reportMessageDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
region: us-west-2
|
||||
tableName: Example_ReportedMessages
|
||||
|
||||
awsAttachments: # AWS S3 configuration
|
||||
accessKey:
|
||||
accessSecret:
|
||||
bucket:
|
||||
region:
|
||||
accessKey: test
|
||||
accessSecret: test
|
||||
bucket: aws-attachments
|
||||
region: us-west-2
|
||||
|
||||
gcpAttachments: # GCP Storage configuration
|
||||
domain:
|
||||
email:
|
||||
maxSizeInBytes:
|
||||
domain: example.com
|
||||
email: user@example.cocm
|
||||
maxSizeInBytes: 1024
|
||||
pathPrefix:
|
||||
rsaSigningKey:
|
||||
rsaSigningKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAA
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
abuseDatabase: # Postgresql database configuration
|
||||
driverClass: org.postgresql.Driver
|
||||
user:
|
||||
password:
|
||||
url:
|
||||
user: example
|
||||
password: password
|
||||
url: jdbc:postgresql://example.com:5432/abusedb
|
||||
|
||||
accountsDatabase: # Postgresql database configuration
|
||||
driverClass: org.postgresql.Driver
|
||||
user:
|
||||
password:
|
||||
url:
|
||||
user: example
|
||||
password: password
|
||||
url: jdbc:postgresql://example.com:5432/accountsdb
|
||||
|
||||
accountDatabaseCrawler:
|
||||
chunkSize: # accounts per run
|
||||
chunkIntervalMs: # time per run
|
||||
|
||||
dynamoDbMigrationCrawler:
|
||||
chunkSize: # accounts per run
|
||||
chunkIntervalMs: # time per run
|
||||
chunkSize: 10 # accounts per run
|
||||
chunkIntervalMs: 60000 # time per run
|
||||
|
||||
apn: # Apple Push Notifications configuration
|
||||
sandbox: true
|
||||
bundleId:
|
||||
keyId:
|
||||
teamId:
|
||||
signingKey:
|
||||
bundleId: com.example.textsecuregcm
|
||||
keyId: unset
|
||||
teamId: unset
|
||||
signingKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAA
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
gcm: # GCM Configuration
|
||||
senderId:
|
||||
apiKey:
|
||||
senderId: 123456789
|
||||
apiKey: unset
|
||||
|
||||
cdn:
|
||||
accessKey: # AWS Access Key ID
|
||||
accessSecret: # AWS Access Secret
|
||||
bucket: # S3 Bucket name
|
||||
region: # AWS region
|
||||
accessKey: test # AWS Access Key ID
|
||||
accessSecret: test # AWS Access Secret
|
||||
bucket: cdn # S3 Bucket name
|
||||
region: us-west-2 # AWS region
|
||||
|
||||
datadog:
|
||||
apiKey:
|
||||
environment:
|
||||
apiKey: unset
|
||||
environment: dev
|
||||
|
||||
unidentifiedDelivery:
|
||||
certificate:
|
||||
privateKey:
|
||||
expiresDays:
|
||||
certificate: ABCD1234
|
||||
privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA
|
||||
expiresDays: 7
|
||||
|
||||
voiceVerification:
|
||||
url: https://cdn-ca.signal.org/verification/
|
||||
@@ -210,33 +260,74 @@ voiceVerification:
|
||||
- en
|
||||
|
||||
recaptcha:
|
||||
secret:
|
||||
secret: unset
|
||||
|
||||
recaptchaV2:
|
||||
siteKey:
|
||||
scoreFloor:
|
||||
projectPath:
|
||||
credentialConfigurationJson:
|
||||
siteKey: unset
|
||||
scoreFloor: 1.0
|
||||
projectPath: projects/example
|
||||
credentialConfigurationJson: "{ }" # service account configuration for backend authentication
|
||||
|
||||
storageService:
|
||||
uri:
|
||||
userAuthenticationTokenSharedSecret:
|
||||
storageCaCertificate:
|
||||
uri: storage.example.com
|
||||
userAuthenticationTokenSharedSecret: 00000f
|
||||
storageCaCertificate: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
backupService:
|
||||
uri:
|
||||
userAuthenticationTokenSharedSecret:
|
||||
backupCaCertificate:
|
||||
uri: backup.example.com
|
||||
userAuthenticationTokenSharedSecret: 00000f
|
||||
backupCaCertificate: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
zkConfig:
|
||||
serverPublic:
|
||||
serverSecret:
|
||||
enabled:
|
||||
serverPublic: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
|
||||
|
||||
appConfig:
|
||||
application:
|
||||
environment:
|
||||
configuration:
|
||||
application: example
|
||||
environment: example
|
||||
configuration: example
|
||||
|
||||
remoteConfig:
|
||||
authorizedTokens:
|
||||
@@ -245,48 +336,64 @@ remoteConfig:
|
||||
- # ...
|
||||
- # Nth authorized token
|
||||
globalConfig: # keys and values that are given to clients on GET /v1/config
|
||||
|
||||
EXAMPLE_KEY: VALUE
|
||||
|
||||
paymentsService:
|
||||
userAuthenticationTokenSharedSecret: # hex-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
|
||||
fixerApiKey:
|
||||
userAuthenticationTokenSharedSecret: 0000000f0000000f0000000f0000000f0000000f0000000f0000000f0000000f # hex-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
|
||||
fixerApiKey: unset
|
||||
paymentCurrencies:
|
||||
-
|
||||
|
||||
torExitNodeList:
|
||||
s3Region:
|
||||
s3Bucket:
|
||||
objectKey:
|
||||
maxSize:
|
||||
|
||||
asnTable:
|
||||
s3Region:
|
||||
s3Bucket:
|
||||
objectKey:
|
||||
maxSize:
|
||||
# list of symbols for supported currencies
|
||||
- MOB
|
||||
|
||||
donation:
|
||||
uri: # value
|
||||
uri: donation.example.com # value
|
||||
supportedCurrencies:
|
||||
- # 1st supported currency
|
||||
- # 2nd supported currency
|
||||
- # ...
|
||||
- # Nth supported currency
|
||||
circuitBreaker:
|
||||
failureRateThreshold: # value
|
||||
ringBufferSizeInHalfOpenState: # value
|
||||
ringBufferSizeInClosedState: # value
|
||||
waitDurationInOpenStateInSeconds: # value
|
||||
retry:
|
||||
maxAttempts: # value
|
||||
waitDuration: # value
|
||||
|
||||
badges:
|
||||
badges:
|
||||
- id: TEST
|
||||
imageUrl: https://example.com/test-badge
|
||||
category: other
|
||||
sprites: # exactly 6
|
||||
- sprite-1.png
|
||||
- sprite-2.png
|
||||
- sprite-3.png
|
||||
- sprite-4.png
|
||||
- sprite-5.png
|
||||
- sprite-6.png
|
||||
svg: example.svg
|
||||
svgs:
|
||||
- light: example-light.svg
|
||||
dark: example-dark.svg
|
||||
badgeIdsEnabledForAll:
|
||||
- TEST
|
||||
receiptLevels:
|
||||
'1': TEST
|
||||
|
||||
subscription: # configuration for Stripe subscriptions
|
||||
badgeGracePeriod: P15D
|
||||
levels:
|
||||
500:
|
||||
badge: EXAMPLE
|
||||
prices:
|
||||
# list of ISO 4217 currency codes and amounts for the given badge level
|
||||
xts:
|
||||
amount: '10'
|
||||
id: price_example # stripe ID
|
||||
|
||||
boost:
|
||||
level: 1
|
||||
expiration: P90D
|
||||
badge: EXAMPLE
|
||||
currencies:
|
||||
# ISO 4217 currency codes and amounts in those currencies
|
||||
xts:
|
||||
- '1'
|
||||
- '2'
|
||||
- '4'
|
||||
- '8'
|
||||
- '20'
|
||||
- '40'
|
||||
|
||||
@@ -593,6 +593,28 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>check-all-service-config</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>java</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<mainClass>org.whispersystems.textsecuregcm.CheckServiceConfigurations</mainClass>
|
||||
<classpathScope>test</classpathScope>
|
||||
<arguments>
|
||||
<argument>${project.basedir}/config</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
+19
@@ -27,6 +27,7 @@ import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DeletedAccountsDynamoDbConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryV2Configuration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DonationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbConfiguration;
|
||||
@@ -125,6 +126,11 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private DirectoryConfiguration directory;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DirectoryV2Configuration directoryV2;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
@@ -165,6 +171,11 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private AccountsDynamoDbConfiguration accountsDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration phoneNumberIdentifiersDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
@@ -390,6 +401,10 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public DirectoryV2Configuration getDirectoryV2Configuration() {
|
||||
return directoryV2;
|
||||
}
|
||||
|
||||
public SecureStorageServiceConfiguration getSecureStorageServiceConfiguration() {
|
||||
return storageService;
|
||||
}
|
||||
@@ -426,6 +441,10 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return accountsDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getPhoneNumberIdentifiersDynamoDbConfiguration() {
|
||||
return phoneNumberIdentifiersDynamoDb;
|
||||
}
|
||||
|
||||
public DeletedAccountsDynamoDbConfiguration getDeletedAccountsDynamoDbConfiguration() {
|
||||
return deletedAccountsDynamoDb;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ import java.util.concurrent.TimeUnit;
|
||||
import javax.servlet.DispatcherType;
|
||||
import javax.servlet.FilterRegistration;
|
||||
import javax.servlet.ServletRegistration;
|
||||
import javax.ws.rs.container.DynamicFeature;
|
||||
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||
import org.glassfish.jersey.server.ServerProperties;
|
||||
import org.jdbi.v3.core.Jdbi;
|
||||
@@ -80,6 +79,7 @@ import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter;
|
||||
import org.whispersystems.textsecuregcm.badges.ResourceBundleLevelTranslator;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryServerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.AcceptNumericOnlineFlagRequestFilter;
|
||||
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV1;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2;
|
||||
@@ -88,6 +88,7 @@ import org.whispersystems.textsecuregcm.controllers.CertificateController;
|
||||
import org.whispersystems.textsecuregcm.controllers.ChallengeController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DirectoryV2Controller;
|
||||
import org.whispersystems.textsecuregcm.controllers.DonationController;
|
||||
import org.whispersystems.textsecuregcm.controllers.KeepAliveController;
|
||||
import org.whispersystems.textsecuregcm.controllers.KeysController;
|
||||
@@ -115,6 +116,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimitResetMetricsManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.limits.UnsealedSenderRateLimiter;
|
||||
import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle;
|
||||
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
|
||||
@@ -179,12 +181,13 @@ import org.whispersystems.textsecuregcm.storage.DirectoryReconciliationClient;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
|
||||
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagePersister;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesCache;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.NonNormalizedAccountCrawlerListener;
|
||||
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
|
||||
import org.whispersystems.textsecuregcm.storage.Profiles;
|
||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
@@ -329,6 +332,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
DynamoDbClient accountsDynamoDbClient = DynamoDbFromConfig.client(config.getAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient phoneNumberIdentifiersDynamoDbClient =
|
||||
DynamoDbFromConfig.client(config.getPhoneNumberIdentifiersDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client(config.getDeletedAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
@@ -364,11 +371,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
Accounts accounts = new Accounts(accountsDynamoDbClient,
|
||||
config.getAccountsDynamoDbConfiguration().getTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getScanPageSize());
|
||||
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
|
||||
config.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
KeysDynamoDb keysDynamoDb = new KeysDynamoDb(preKeyDynamoDb, config.getKeysDynamoDbConfiguration().getTableName());
|
||||
Keys keys = new Keys(preKeyDynamoDb, config.getKeysDynamoDbConfiguration().getTableName());
|
||||
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messageDynamoDb,
|
||||
config.getMessageDynamoDbConfiguration().getTableName(),
|
||||
config.getMessageDynamoDbConfiguration().getTimeToLive());
|
||||
@@ -382,24 +392,15 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
RedisClientFactory pubSubClientFactory = new RedisClientFactory("pubsub_cache", config.getPubsubCacheConfiguration().getUrl(), config.getPubsubCacheConfiguration().getReplicaUrls(), config.getPubsubCacheConfiguration().getCircuitBreakerConfiguration());
|
||||
ReplicatedJedisPool pubsubClient = pubSubClientFactory.getRedisClientPool();
|
||||
|
||||
ClientResources generalCacheClientResources = ClientResources.builder().build();
|
||||
ClientResources messageCacheClientResources = ClientResources.builder().build();
|
||||
ClientResources presenceClientResources = ClientResources.builder().build();
|
||||
ClientResources metricsCacheClientResources = ClientResources.builder().build();
|
||||
ClientResources pushSchedulerCacheClientResources = ClientResources.builder().ioThreadPoolSize(4).build();
|
||||
ClientResources rateLimitersCacheClientResources = ClientResources.builder().build();
|
||||
ClientResources redisClientResources = ClientResources.builder().build();
|
||||
ConnectionEventLogger.logConnectionEvents(redisClientResources);
|
||||
|
||||
ConnectionEventLogger.logConnectionEvents(generalCacheClientResources);
|
||||
ConnectionEventLogger.logConnectionEvents(messageCacheClientResources);
|
||||
ConnectionEventLogger.logConnectionEvents(presenceClientResources);
|
||||
ConnectionEventLogger.logConnectionEvents(metricsCacheClientResources);
|
||||
|
||||
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", config.getCacheClusterConfiguration(), generalCacheClientResources);
|
||||
FaultTolerantRedisCluster messagesCluster = new FaultTolerantRedisCluster("messages_cluster", config.getMessageCacheConfiguration().getRedisClusterConfiguration(), messageCacheClientResources);
|
||||
FaultTolerantRedisCluster clientPresenceCluster = new FaultTolerantRedisCluster("client_presence_cluster", config.getClientPresenceClusterConfiguration(), presenceClientResources);
|
||||
FaultTolerantRedisCluster metricsCluster = new FaultTolerantRedisCluster("metrics_cluster", config.getMetricsClusterConfiguration(), metricsCacheClientResources);
|
||||
FaultTolerantRedisCluster pushSchedulerCluster = new FaultTolerantRedisCluster("push_scheduler", config.getPushSchedulerCluster(), pushSchedulerCacheClientResources);
|
||||
FaultTolerantRedisCluster rateLimitersCluster = new FaultTolerantRedisCluster("rate_limiters", config.getRateLimitersCluster(), rateLimitersCacheClientResources);
|
||||
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", config.getCacheClusterConfiguration(), redisClientResources);
|
||||
FaultTolerantRedisCluster messagesCluster = new FaultTolerantRedisCluster("messages_cluster", config.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClientResources);
|
||||
FaultTolerantRedisCluster clientPresenceCluster = new FaultTolerantRedisCluster("client_presence_cluster", config.getClientPresenceClusterConfiguration(), redisClientResources);
|
||||
FaultTolerantRedisCluster metricsCluster = new FaultTolerantRedisCluster("metrics_cluster", config.getMetricsClusterConfiguration(), redisClientResources);
|
||||
FaultTolerantRedisCluster pushSchedulerCluster = new FaultTolerantRedisCluster("push_scheduler", config.getPushSchedulerCluster(), redisClientResources);
|
||||
FaultTolerantRedisCluster rateLimitersCluster = new FaultTolerantRedisCluster("rate_limiters", config.getRateLimitersCluster(), redisClientResources);
|
||||
|
||||
BlockingQueue<Runnable> keyspaceNotificationDispatchQueue = new ArrayBlockingQueue<>(10_000);
|
||||
Metrics.gaugeCollectionSize(name(getClass(), "keyspaceNotificationDispatchQueueSize"), Collections.emptyList(), keyspaceNotificationDispatchQueue);
|
||||
@@ -421,11 +422,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
build();
|
||||
|
||||
StripeManager stripeManager = new StripeManager(config.getStripe().getApiKey(), stripeExecutor,
|
||||
config.getStripe().getIdempotencyKeyGenerator());
|
||||
config.getStripe().getIdempotencyKeyGenerator(), config.getStripe().getBoostDescription());
|
||||
|
||||
ExternalServiceCredentialGenerator directoryCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(),
|
||||
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenUserIdSecret(),
|
||||
true);
|
||||
ExternalServiceCredentialGenerator directoryCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(),
|
||||
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenUserIdSecret());
|
||||
ExternalServiceCredentialGenerator directoryV2CredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
config.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration()
|
||||
.getUserAuthenticationTokenSharedSecret(), false);
|
||||
|
||||
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager =
|
||||
new DynamicConfigurationManager<>(config.getAppConfig().getApplication(),
|
||||
@@ -440,9 +444,12 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager = new TwilioVerifyExperimentEnrollmentManager(
|
||||
config.getVoiceVerificationConfiguration(), experimentEnrollmentManager);
|
||||
|
||||
ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false);
|
||||
ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false);
|
||||
ExternalServiceCredentialGenerator paymentsCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getPaymentsServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false);
|
||||
ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
config.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
|
||||
ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
config.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
|
||||
ExternalServiceCredentialGenerator paymentsCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
config.getPaymentsServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
|
||||
|
||||
SecureBackupClient secureBackupClient = new SecureBackupClient(backupCredentialsGenerator, backupServiceExecutor, config.getSecureBackupServiceConfiguration());
|
||||
SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator, storageServiceExecutor, config.getSecureStorageServiceConfiguration());
|
||||
@@ -453,14 +460,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster);
|
||||
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster);
|
||||
MessagesCache messagesCache = new MessagesCache(messagesCluster, messagesCluster, keyspaceNotificationDispatchExecutor);
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster);
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, dynamicConfigurationManager);
|
||||
ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, rateLimitersCluster, Metrics.globalRegistry, config.getReportMessageConfiguration().getCounterTtl());
|
||||
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, pushLatencyManager, reportMessageManager);
|
||||
DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
|
||||
deletedAccountsLockDynamoDbClient, config.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keys, messagesManager, usernamesManager, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
|
||||
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
|
||||
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(accountsManager, messagesManager);
|
||||
DispatchManager dispatchManager = new DispatchManager(pubSubClientFactory, Optional.of(deadLetterHandler));
|
||||
@@ -510,9 +517,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
MessagePersister messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, dynamicConfigurationManager, Duration.ofMinutes(config.getMessageCacheConfiguration().getPersistDelayMinutes()));
|
||||
|
||||
// TODO listeners must be ordered so that ones that directly update accounts come last, so that read-only ones are not working with stale data
|
||||
final List<AccountDatabaseCrawlerListener> accountDatabaseCrawlerListeners = new ArrayList<>();
|
||||
|
||||
final List<AccountDatabaseCrawlerListener> directoryReconciliationAccountDatabaseCrawlerListeners = new ArrayList<>();
|
||||
final List<DeletedAccountsDirectoryReconciler> deletedAccountsDirectoryReconcilers = new ArrayList<>();
|
||||
for (DirectoryServerConfiguration directoryServerConfiguration : config.getDirectoryConfiguration()
|
||||
.getDirectoryServerConfiguration()) {
|
||||
@@ -522,25 +527,33 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
directoryServerConfiguration.getReplicationName(), directoryReconciliationClient,
|
||||
dynamicConfigurationManager);
|
||||
// reconcilers are read-only
|
||||
accountDatabaseCrawlerListeners.add(directoryReconciler);
|
||||
directoryReconciliationAccountDatabaseCrawlerListeners.add(directoryReconciler);
|
||||
|
||||
final DeletedAccountsDirectoryReconciler deletedAccountsDirectoryReconciler = new DeletedAccountsDirectoryReconciler(
|
||||
directoryServerConfiguration.getReplicationName(), directoryReconciliationClient);
|
||||
deletedAccountsDirectoryReconcilers.add(deletedAccountsDirectoryReconciler);
|
||||
}
|
||||
accountDatabaseCrawlerListeners.add(new NonNormalizedAccountCrawlerListener(accountsManager, metricsCluster));
|
||||
accountDatabaseCrawlerListeners.add(new ContactDiscoveryWriter(accountsManager));
|
||||
// PushFeedbackProcessor may update device properties
|
||||
accountDatabaseCrawlerListeners.add(new PushFeedbackProcessor(accountsManager));
|
||||
// delete accounts last
|
||||
accountDatabaseCrawlerListeners.add(new AccountCleaner(accountsManager));
|
||||
|
||||
HttpClient currencyClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).connectTimeout(Duration.ofSeconds(10)).build();
|
||||
FixerClient fixerClient = new FixerClient(currencyClient, config.getPaymentsServiceConfiguration().getFixerApiKey());
|
||||
FtxClient ftxClient = new FtxClient(currencyClient);
|
||||
CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, ftxClient, config.getPaymentsServiceConfiguration().getPaymentCurrencies());
|
||||
AccountDatabaseCrawlerCache directoryReconciliationAccountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(
|
||||
cacheCluster, AccountDatabaseCrawlerCache.DIRECTORY_RECONCILER_PREFIX);
|
||||
AccountDatabaseCrawler directoryReconciliationAccountDatabaseCrawler = new AccountDatabaseCrawler(accountsManager,
|
||||
directoryReconciliationAccountDatabaseCrawlerCache, directoryReconciliationAccountDatabaseCrawlerListeners,
|
||||
config.getAccountDatabaseCrawlerConfiguration().getChunkSize(),
|
||||
config.getAccountDatabaseCrawlerConfiguration().getChunkIntervalMs()
|
||||
);
|
||||
|
||||
AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(cacheCluster);
|
||||
// TODO listeners must be ordered so that ones that directly update accounts come last, so that read-only ones are not working with stale data
|
||||
final List<AccountDatabaseCrawlerListener> accountDatabaseCrawlerListeners = List.of(
|
||||
new NonNormalizedAccountCrawlerListener(accountsManager, metricsCluster),
|
||||
new ContactDiscoveryWriter(accountsManager),
|
||||
// PushFeedbackProcessor may update device properties
|
||||
new PushFeedbackProcessor(accountsManager),
|
||||
// delete accounts last
|
||||
new AccountCleaner(accountsManager)
|
||||
);
|
||||
|
||||
AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(cacheCluster,
|
||||
AccountDatabaseCrawlerCache.GENERAL_PURPOSE_PREFIX);
|
||||
AccountDatabaseCrawler accountDatabaseCrawler = new AccountDatabaseCrawler(accountsManager,
|
||||
accountDatabaseCrawlerCache, accountDatabaseCrawlerListeners,
|
||||
config.getAccountDatabaseCrawlerConfiguration().getChunkSize(),
|
||||
@@ -549,12 +562,18 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
DeletedAccountsTableCrawler deletedAccountsTableCrawler = new DeletedAccountsTableCrawler(deletedAccountsManager, deletedAccountsDirectoryReconcilers, cacheCluster, recurringJobExecutor);
|
||||
|
||||
HttpClient currencyClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).connectTimeout(Duration.ofSeconds(10)).build();
|
||||
FixerClient fixerClient = new FixerClient(currencyClient, config.getPaymentsServiceConfiguration().getFixerApiKey());
|
||||
FtxClient ftxClient = new FtxClient(currencyClient);
|
||||
CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, ftxClient, config.getPaymentsServiceConfiguration().getPaymentCurrencies());
|
||||
|
||||
apnSender.setApnFallbackManager(apnFallbackManager);
|
||||
environment.lifecycle().manage(new ApplicationShutdownMonitor());
|
||||
environment.lifecycle().manage(apnFallbackManager);
|
||||
environment.lifecycle().manage(pubSubManager);
|
||||
environment.lifecycle().manage(messageSender);
|
||||
environment.lifecycle().manage(accountDatabaseCrawler);
|
||||
environment.lifecycle().manage(directoryReconciliationAccountDatabaseCrawler);
|
||||
environment.lifecycle().manage(deletedAccountsTableCrawler);
|
||||
environment.lifecycle().manage(remoteConfigsManager);
|
||||
environment.lifecycle().manage(messagesCache);
|
||||
@@ -590,7 +609,12 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
.addFilter("RemoteDeprecationFilter", new RemoteDeprecationFilter(dynamicConfigurationManager))
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
|
||||
|
||||
// TODO Remove on or after 2022-03-01
|
||||
final AcceptNumericOnlineFlagRequestFilter acceptNumericOnlineFlagRequestFilter =
|
||||
new AcceptNumericOnlineFlagRequestFilter("v1/messages/multi_recipient");
|
||||
|
||||
environment.jersey().register(new ContentLengthFilter(TrafficSource.HTTP));
|
||||
environment.jersey().register(acceptNumericOnlineFlagRequestFilter);
|
||||
environment.jersey().register(MultiRecipientMessageProvider.class);
|
||||
environment.jersey().register(new MetricsApplicationEventListener(TrafficSource.HTTP));
|
||||
environment.jersey()
|
||||
@@ -612,6 +636,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
clientPresenceManager, retrySchedulingExecutor));
|
||||
webSocketEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
||||
webSocketEnvironment.jersey().register(new ContentLengthFilter(TrafficSource.WEBSOCKET));
|
||||
webSocketEnvironment.jersey().register(acceptNumericOnlineFlagRequestFilter);
|
||||
webSocketEnvironment.jersey().register(MultiRecipientMessageProvider.class);
|
||||
webSocketEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET));
|
||||
webSocketEnvironment.jersey().register(new KeepAliveController(clientPresenceManager));
|
||||
@@ -622,7 +647,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
smsSender, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
|
||||
transitionalRecaptchaClient, gcmSender, apnSender, backupCredentialsGenerator,
|
||||
verifyExperimentEnrollmentManager));
|
||||
environment.jersey().register(new KeysController(rateLimiters, keysDynamoDb, accountsManager, preKeyRateLimiter, rateLimitChallengeManager));
|
||||
environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager, preKeyRateLimiter, rateLimitChallengeManager));
|
||||
|
||||
final List<Object> commonControllers = Lists.newArrayList(
|
||||
new AttachmentControllerV1(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getBucket()),
|
||||
@@ -630,8 +655,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new AttachmentControllerV3(rateLimiters, config.getGcpAttachmentsConfiguration().getDomain(), config.getGcpAttachmentsConfiguration().getEmail(), config.getGcpAttachmentsConfiguration().getMaxSizeInBytes(), config.getGcpAttachmentsConfiguration().getPathPrefix(), config.getGcpAttachmentsConfiguration().getRsaSigningKey()),
|
||||
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays()), zkAuthOperations),
|
||||
new ChallengeController(rateLimitChallengeManager),
|
||||
new DeviceController(pendingDevicesManager, accountsManager, messagesManager, keysDynamoDb, rateLimiters, config.getMaxDevices()),
|
||||
new DeviceController(pendingDevicesManager, accountsManager, messagesManager, keys, rateLimiters, config.getMaxDevices()),
|
||||
new DirectoryController(directoryCredentialsGenerator),
|
||||
new DirectoryV2Controller(directoryV2CredentialsGenerator),
|
||||
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
|
||||
ReceiptCredentialPresentation::new, stripeExecutor, config.getDonationConfiguration(), config.getStripe()),
|
||||
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, unsealedSenderRateLimiter, apnFallbackManager,
|
||||
@@ -744,6 +770,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
List.of(
|
||||
new LoggingUnhandledExceptionMapper(),
|
||||
new CompletionExceptionMapper(),
|
||||
new IOExceptionMapper(),
|
||||
new RateLimitExceededExceptionMapper(),
|
||||
new InvalidWebsocketAddressExceptionMapper(),
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ public class AuthEnablementRefreshRequirementProvider implements WebsocketRefres
|
||||
@SuppressWarnings("unchecked") final Map<Long, Boolean> initialDevicesEnabled =
|
||||
(Map<Long, Boolean>) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED);
|
||||
|
||||
return accountsManager.get((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
|
||||
return accountsManager.getByAccountIdentifier((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
|
||||
final Set<Long> deviceIdsToDisplace;
|
||||
final Map<Long, Boolean> currentDevicesEnabled = buildDevicesEnabledMap(account);
|
||||
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ public class BaseAccountAuthenticator {
|
||||
deviceId = identifierAndDeviceId.second();
|
||||
}
|
||||
|
||||
Optional<Account> account = accountsManager.get(accountUuid);
|
||||
Optional<Account> account = accountsManager.getByAccountIdentifier(accountUuid);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
failureReason = "noSuchAccount";
|
||||
|
||||
+42
-68
@@ -1,97 +1,71 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Clock;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public class ExternalServiceCredentialGenerator {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ExternalServiceCredentialGenerator.class);
|
||||
|
||||
private final byte[] key;
|
||||
private final byte[] userIdKey;
|
||||
private final boolean usernameDerivation;
|
||||
private final boolean prependUsername;
|
||||
private final Clock clock;
|
||||
|
||||
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation) {
|
||||
this.key = key;
|
||||
this.userIdKey = userIdKey;
|
||||
this.usernameDerivation = usernameDerivation;
|
||||
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey) {
|
||||
this(key, userIdKey, true, true);
|
||||
}
|
||||
|
||||
public ExternalServiceCredentials generateFor(String number) {
|
||||
Mac mac = getMacInstance();
|
||||
String username = getUserId(number, mac, usernameDerivation);
|
||||
long currentTimeSeconds = System.currentTimeMillis() / 1000;
|
||||
String prefix = username + ":" + currentTimeSeconds;
|
||||
String output = Hex.encodeHexString(Util.truncate(getHmac(key, prefix.getBytes(), mac), 10));
|
||||
String token = prefix + ":" + output;
|
||||
public ExternalServiceCredentialGenerator(byte[] key, boolean prependUsername) {
|
||||
this(key, new byte[0], false, prependUsername);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation) {
|
||||
this(key, userIdKey, usernameDerivation, true);
|
||||
}
|
||||
|
||||
private ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation,
|
||||
boolean prependUsername) {
|
||||
this(key, userIdKey, usernameDerivation, prependUsername, Clock.systemUTC());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation,
|
||||
boolean prependUsername, Clock clock) {
|
||||
this.key = key;
|
||||
this.userIdKey = userIdKey;
|
||||
this.usernameDerivation = usernameDerivation;
|
||||
this.prependUsername = prependUsername;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public ExternalServiceCredentials generateFor(String identity) {
|
||||
Mac mac = getMacInstance();
|
||||
String username = getUserId(identity, mac, usernameDerivation);
|
||||
long currentTimeSeconds = clock.millis() / 1000;
|
||||
String prefix = username + ":" + currentTimeSeconds;
|
||||
String output = Hex.encodeHexString(Util.truncate(getHmac(key, prefix.getBytes(), mac), 10));
|
||||
String token = (prependUsername ? prefix : currentTimeSeconds) + ":" + output;
|
||||
|
||||
return new ExternalServiceCredentials(username, token);
|
||||
}
|
||||
|
||||
|
||||
public boolean isValid(String token, String number, long currentTimeMillis) {
|
||||
String[] parts = token.split(":");
|
||||
Mac mac = getMacInstance();
|
||||
|
||||
if (parts.length != 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getUserId(number, mac, usernameDerivation).equals(parts[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidTime(parts[1], currentTimeMillis)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isValidSignature(parts[0] + ":" + parts[1], parts[2], mac);
|
||||
}
|
||||
|
||||
private String getUserId(String number, Mac mac, boolean usernameDerivation) {
|
||||
if (usernameDerivation) return Hex.encodeHexString(Util.truncate(getHmac(userIdKey, number.getBytes(), mac), 10));
|
||||
else return number;
|
||||
}
|
||||
|
||||
private boolean isValidTime(String timeString, long currentTimeMillis) {
|
||||
try {
|
||||
long tokenTime = Long.parseLong(timeString);
|
||||
long ourTime = TimeUnit.MILLISECONDS.toSeconds(currentTimeMillis);
|
||||
|
||||
return TimeUnit.SECONDS.toHours(Math.abs(ourTime - tokenTime)) < 24;
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Number Format", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidSignature(String prefix, String suffix, Mac mac) {
|
||||
try {
|
||||
byte[] ourSuffix = Util.truncate(getHmac(key, prefix.getBytes(), mac), 10);
|
||||
byte[] theirSuffix = Hex.decodeHex(suffix.toCharArray());
|
||||
|
||||
return MessageDigest.isEqual(ourSuffix, theirSuffix);
|
||||
} catch (DecoderException e) {
|
||||
logger.warn("DirectoryCredentials", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Mac getMacInstance() {
|
||||
try {
|
||||
return Mac.getInstance("HmacSHA256");
|
||||
|
||||
+8
@@ -8,6 +8,9 @@ public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
@NotNull
|
||||
private String phoneNumberTableName;
|
||||
|
||||
@NotNull
|
||||
private String phoneNumberIdentifierTableName;
|
||||
|
||||
private int scanPageSize = 100;
|
||||
|
||||
@JsonProperty
|
||||
@@ -15,6 +18,11 @@ public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
return phoneNumberTableName;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
public String getPhoneNumberIdentifierTableName() {
|
||||
return phoneNumberIdentifierTableName;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
public int getScanPageSize() {
|
||||
return scanPageSize;
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
|
||||
public class DirectoryV2ClientConfiguration {
|
||||
|
||||
private final byte[] userAuthenticationTokenSharedSecret;
|
||||
|
||||
@JsonCreator
|
||||
public DirectoryV2ClientConfiguration(
|
||||
@JsonProperty("userAuthenticationTokenSharedSecret") final byte[] userAuthenticationTokenSharedSecret) {
|
||||
this.userAuthenticationTokenSharedSecret = userAuthenticationTokenSharedSecret;
|
||||
}
|
||||
|
||||
@ExactlySize({32})
|
||||
public byte[] getUserAuthenticationTokenSharedSecret() {
|
||||
return userAuthenticationTokenSharedSecret;
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import javax.validation.Valid;
|
||||
|
||||
public class DirectoryV2Configuration {
|
||||
|
||||
private final DirectoryV2ClientConfiguration clientConfiguration;
|
||||
|
||||
@JsonCreator
|
||||
public DirectoryV2Configuration(@JsonProperty("client") DirectoryV2ClientConfiguration clientConfiguration) {
|
||||
this.clientConfiguration = clientConfiguration;
|
||||
}
|
||||
|
||||
@Valid
|
||||
public DirectoryV2ClientConfiguration getDirectoryV2ClientConfiguration() {
|
||||
return clientConfiguration;
|
||||
}
|
||||
}
|
||||
+7
@@ -63,6 +63,9 @@ public class RateLimitsConfiguration {
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration usernameSet = new RateLimitConfiguration(100, 100 / (24.0 * 60.0));
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration checkAccountExistence = new RateLimitConfiguration(1_000, 1_000 / 60.0);
|
||||
|
||||
public RateLimitConfiguration getAutoBlock() {
|
||||
return autoBlock;
|
||||
}
|
||||
@@ -135,6 +138,10 @@ public class RateLimitsConfiguration {
|
||||
return usernameSet;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getCheckAccountExistence() {
|
||||
return checkAccountExistence;
|
||||
}
|
||||
|
||||
public static class RateLimitConfiguration {
|
||||
@JsonProperty
|
||||
private int bucketSize;
|
||||
|
||||
+9
-1
@@ -13,13 +13,16 @@ public class StripeConfiguration {
|
||||
|
||||
private final String apiKey;
|
||||
private final byte[] idempotencyKeyGenerator;
|
||||
private final String boostDescription;
|
||||
|
||||
@JsonCreator
|
||||
public StripeConfiguration(
|
||||
@JsonProperty("apiKey") final String apiKey,
|
||||
@JsonProperty("idempotencyKeyGenerator") final byte[] idempotencyKeyGenerator) {
|
||||
@JsonProperty("idempotencyKeyGenerator") final byte[] idempotencyKeyGenerator,
|
||||
@JsonProperty("boostDescription") final String boostDescription) {
|
||||
this.apiKey = apiKey;
|
||||
this.idempotencyKeyGenerator = idempotencyKeyGenerator;
|
||||
this.boostDescription = boostDescription;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
@@ -31,4 +34,9 @@ public class StripeConfiguration {
|
||||
public byte[] getIdempotencyKeyGenerator() {
|
||||
return idempotencyKeyGenerator;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getBoostDescription() {
|
||||
return boostDescription;
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -51,6 +51,10 @@ public class DynamicConfiguration {
|
||||
@JsonProperty
|
||||
private DynamicDirectoryReconcilerConfiguration directoryReconciler = new DynamicDirectoryReconcilerConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private DynamicPushLatencyConfiguration pushLatency = new DynamicPushLatencyConfiguration(Collections.emptyMap());
|
||||
|
||||
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
|
||||
final String experimentName) {
|
||||
return Optional.ofNullable(experiments.get(experimentName));
|
||||
@@ -101,4 +105,8 @@ public class DynamicConfiguration {
|
||||
public DynamicDirectoryReconcilerConfiguration getDirectoryReconcilerConfiguration() {
|
||||
return directoryReconciler;
|
||||
}
|
||||
|
||||
public DynamicPushLatencyConfiguration getPushLatencyConfiguration() {
|
||||
return pushLatency;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -7,14 +7,14 @@ package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.List;
|
||||
|
||||
public class DynamicPaymentsConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private Set<String> allowedCountryCodes = Collections.emptySet();
|
||||
private List<String> disallowedPrefixes = Collections.emptyList();
|
||||
|
||||
public Set<String> getAllowedCountryCodes() {
|
||||
return allowedCountryCodes;
|
||||
public List<String> getDisallowedPrefixes() {
|
||||
return disallowedPrefixes;
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class DynamicPushLatencyConfiguration {
|
||||
|
||||
private final Map<ClientPlatform, Set<Semver>> instrumentedVersions;
|
||||
|
||||
@JsonCreator
|
||||
public DynamicPushLatencyConfiguration(@JsonProperty("instrumentedVersions") final Map<ClientPlatform, Set<Semver>> instrumentedVersions) {
|
||||
this.instrumentedVersions = instrumentedVersions;
|
||||
}
|
||||
|
||||
public Map<ClientPlatform, Set<Semver>> getInstrumentedVersions() {
|
||||
return instrumentedVersions;
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.container.ContainerRequestFilter;
|
||||
import javax.ws.rs.container.PreMatching;
|
||||
|
||||
@PreMatching
|
||||
public class AcceptNumericOnlineFlagRequestFilter implements ContainerRequestFilter {
|
||||
|
||||
private final String pathPrefix;
|
||||
|
||||
public AcceptNumericOnlineFlagRequestFilter(final String pathPrefix) {
|
||||
this.pathPrefix = pathPrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void filter(final ContainerRequestContext requestContext) throws IOException {
|
||||
if (requestContext.getUriInfo().getPath().startsWith(pathPrefix)) {
|
||||
if ("1".equals(requestContext.getUriInfo().getQueryParameters().getFirst("online"))) {
|
||||
requestContext.setRequestUri(requestContext.getUriInfo().getRequestUriBuilder()
|
||||
.replaceQueryParam("online", "true")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
-8
@@ -15,6 +15,7 @@ import io.dropwizard.auth.Auth;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -22,13 +23,17 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.BadRequestException;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.HEAD;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
@@ -36,8 +41,11 @@ import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
@@ -353,11 +361,11 @@ public class AccountController {
|
||||
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
|
||||
.ifPresent(smsSender::reportVerificationSucceeded);
|
||||
|
||||
Optional<Account> existingAccount = accounts.get(number);
|
||||
Optional<Account> existingAccount = accounts.getByE164(number);
|
||||
|
||||
if (existingAccount.isPresent()) {
|
||||
verifyRegistrationLock(existingAccount.get(), accountAttributes.getRegistrationLock());
|
||||
}
|
||||
if (existingAccount.isPresent()) {
|
||||
verifyRegistrationLock(existingAccount.get(), accountAttributes.getRegistrationLock());
|
||||
}
|
||||
|
||||
if (availableForTransfer.orElse(false) && existingAccount.map(Account::isTransferSupported).orElse(false)) {
|
||||
throw new WebApplicationException(Response.status(409).build());
|
||||
@@ -365,7 +373,8 @@ public class AccountController {
|
||||
|
||||
rateLimiters.getVerifyLimiter().clear(number);
|
||||
|
||||
Account account = accounts.create(number, password, signalAgent, accountAttributes);
|
||||
Account account = accounts.create(number, password, signalAgent, accountAttributes,
|
||||
existingAccount.map(Account::getBadges).orElseGet(ArrayList::new));
|
||||
|
||||
{
|
||||
metricRegistry.meter(name(AccountController.class, "verify", Util.getCountryCode(number))).mark();
|
||||
@@ -381,7 +390,10 @@ public class AccountController {
|
||||
.record(Instant.now().toEpochMilli() - storedVerificationCode.get().getTimestamp(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
return new AccountCreationResult(account.getUuid(), account.getNumber(), existingAccount.map(Account::isStorageSupported).orElse(false));
|
||||
return new AccountCreationResult(account.getUuid(),
|
||||
account.getNumber(),
|
||||
account.getPhoneNumberIdentifier(),
|
||||
existingAccount.map(Account::isStorageSupported).orElse(false));
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -411,7 +423,7 @@ public class AccountController {
|
||||
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
|
||||
.ifPresent(smsSender::reportVerificationSucceeded);
|
||||
|
||||
final Optional<Account> existingAccount = accounts.get(request.getNumber());
|
||||
final Optional<Account> existingAccount = accounts.getByE164(request.getNumber());
|
||||
|
||||
if (existingAccount.isPresent()) {
|
||||
verifyRegistrationLock(existingAccount.get(), request.getRegistrationLock());
|
||||
@@ -543,6 +555,7 @@ public class AccountController {
|
||||
@PUT
|
||||
@Path("/attributes/")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@ChangesDeviceEnabledState
|
||||
public void setAccountAttributes(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
|
||||
@HeaderParam("X-Signal-Agent") String userAgent,
|
||||
@@ -591,7 +604,10 @@ public class AccountController {
|
||||
@Path("/whoami")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public AccountCreationResult whoAmI(@Auth AuthenticatedAccount auth) {
|
||||
return new AccountCreationResult(auth.getAccount().getUuid(), auth.getAccount().getNumber(), auth.getAccount().isStorageSupported());
|
||||
return new AccountCreationResult(auth.getAccount().getUuid(),
|
||||
auth.getAccount().getNumber(),
|
||||
auth.getAccount().getPhoneNumberIdentifier(),
|
||||
auth.getAccount().isStorageSupported());
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@@ -625,6 +641,30 @@ public class AccountController {
|
||||
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 {
|
||||
|
||||
|
||||
+8
-3
@@ -14,6 +14,7 @@ import java.security.InvalidKeyException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
@@ -70,7 +71,8 @@ public class CertificateController {
|
||||
@Path("/group/{startRedemptionTime}/{endRedemptionTime}")
|
||||
public GroupCredentials getAuthenticationCredentials(@Auth AuthenticatedAccount auth,
|
||||
@PathParam("startRedemptionTime") int startRedemptionTime,
|
||||
@PathParam("endRedemptionTime") int endRedemptionTime) {
|
||||
@PathParam("endRedemptionTime") int endRedemptionTime,
|
||||
@QueryParam("identity") Optional<String> identityType) {
|
||||
if (startRedemptionTime > endRedemptionTime) {
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
@@ -83,10 +85,13 @@ public class CertificateController {
|
||||
|
||||
List<GroupCredentials.GroupCredential> credentials = new LinkedList<>();
|
||||
|
||||
final UUID identifier = identityType.map(String::toLowerCase).orElse("aci").equals("pni") ?
|
||||
auth.getAccount().getPhoneNumberIdentifier() :
|
||||
auth.getAccount().getUuid();
|
||||
|
||||
for (int i = startRedemptionTime; i <= endRedemptionTime; i++) {
|
||||
credentials.add(new GroupCredentials.GroupCredential(
|
||||
serverZkAuthOperations.issueAuthCredential(auth.getAccount().getUuid(), i)
|
||||
.serialize(),
|
||||
serverZkAuthOperations.issueAuthCredential(identifier, i).serialize(),
|
||||
i));
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -41,7 +41,7 @@ import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
@@ -57,14 +57,14 @@ public class DeviceController {
|
||||
private final StoredVerificationCodeManager pendingDevices;
|
||||
private final AccountsManager accounts;
|
||||
private final MessagesManager messages;
|
||||
private final KeysDynamoDb keys;
|
||||
private final Keys keys;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final Map<String, Integer> maxDeviceConfiguration;
|
||||
|
||||
public DeviceController(StoredVerificationCodeManager pendingDevices,
|
||||
AccountsManager accounts,
|
||||
MessagesManager messages,
|
||||
KeysDynamoDb keys,
|
||||
Keys keys,
|
||||
RateLimiters rateLimiters,
|
||||
Map<String, Integer> maxDeviceConfiguration)
|
||||
{
|
||||
@@ -168,7 +168,7 @@ public class DeviceController {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
Optional<Account> account = accounts.get(number);
|
||||
Optional<Account> account = accounts.getByE164(number);
|
||||
|
||||
if (!account.isPresent()) {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.util.ByteUtil;
|
||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
@Path("/v2/directory")
|
||||
public class DirectoryV2Controller {
|
||||
|
||||
private final ExternalServiceCredentialGenerator directoryServiceTokenGenerator;
|
||||
|
||||
public DirectoryV2Controller(ExternalServiceCredentialGenerator userTokenGenerator) {
|
||||
this.directoryServiceTokenGenerator = userTokenGenerator;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/auth")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response getAuthToken(@Auth AuthenticatedAccount auth) {
|
||||
|
||||
final UUID uuid = auth.getAccount().getUuid();
|
||||
final String e164 = auth.getAccount().getNumber();
|
||||
final long e164AsLong = Long.parseLong(e164, e164.indexOf('+'), e164.length() - 1, 10);
|
||||
|
||||
final byte[] uuidAndNumber = ByteUtil.combine(UUIDUtil.toBytes(uuid), Util.longToByteArray(e164AsLong));
|
||||
final String username = Base64.getEncoder().encodeToString(uuidAndNumber);
|
||||
|
||||
final ExternalServiceCredentials credentials = directoryServiceTokenGenerator.generateFor(username);
|
||||
|
||||
return Response.ok().entity(credentials).build();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -159,7 +159,7 @@ public class DonationController {
|
||||
|
||||
@Override
|
||||
public boolean block() {
|
||||
final Optional<Account> optionalAccount = accountsManager.get(auth.getAccount().getUuid());
|
||||
final Optional<Account> optionalAccount = accountsManager.getByAccountIdentifier(auth.getAccount().getUuid());
|
||||
optionalAccount.ifPresent(account -> {
|
||||
accountsManager.update(account, a -> {
|
||||
a.addBadge(clock, new AccountBadge(badgeId, receiptExpiration, request.isVisible()));
|
||||
|
||||
+98
-40
@@ -11,6 +11,7 @@ import io.dropwizard.auth.Auth;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -24,6 +25,7 @@ import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
@@ -44,7 +46,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@@ -52,7 +54,7 @@ import org.whispersystems.textsecuregcm.util.Util;
|
||||
public class KeysController {
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final KeysDynamoDb keysDynamoDb;
|
||||
private final Keys keys;
|
||||
private final AccountsManager accounts;
|
||||
private final PreKeyRateLimiter preKeyRateLimiter;
|
||||
|
||||
@@ -64,11 +66,11 @@ public class KeysController {
|
||||
private static final String SOURCE_COUNTRY_TAG_NAME = "sourceCountry";
|
||||
private static final String INTERNATIONAL_TAG_NAME = "international";
|
||||
|
||||
public KeysController(RateLimiters rateLimiters, KeysDynamoDb keysDynamoDb, AccountsManager accounts,
|
||||
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
|
||||
PreKeyRateLimiter preKeyRateLimiter,
|
||||
RateLimitChallengeManager rateLimitChallengeManager) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.keysDynamoDb = keysDynamoDb;
|
||||
this.keys = keys;
|
||||
this.accounts = accounts;
|
||||
this.preKeyRateLimiter = preKeyRateLimiter;
|
||||
this.rateLimitChallengeManager = rateLimitChallengeManager;
|
||||
@@ -76,8 +78,10 @@ public class KeysController {
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public PreKeyCount getStatus(@Auth AuthenticatedAccount auth) {
|
||||
int count = keysDynamoDb.getCount(auth.getAccount(), auth.getAuthenticatedDevice().getId());
|
||||
public PreKeyCount getStatus(@Auth final AuthenticatedAccount auth,
|
||||
@QueryParam("identity") final Optional<String> identityType) {
|
||||
|
||||
int count = keys.getCount(getIdentifier(auth.getAccount(), identityType), auth.getAuthenticatedDevice().getId());
|
||||
|
||||
if (count > 0) {
|
||||
count = count - 1;
|
||||
@@ -89,7 +93,9 @@ public class KeysController {
|
||||
@Timed
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setKeys(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth, @Valid PreKeyState preKeys) {
|
||||
public void setKeys(@Auth final DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
|
||||
@Valid final PreKeyState preKeys,
|
||||
@QueryParam("identity") final Optional<String> identityType) {
|
||||
Account account = disabledPermittedAuth.getAccount();
|
||||
Device device = disabledPermittedAuth.getAuthenticatedDevice();
|
||||
boolean updateAccount = false;
|
||||
@@ -102,14 +108,27 @@ public class KeysController {
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
final boolean usePhoneNumberIdentity = usePhoneNumberIdentity(identityType);
|
||||
|
||||
if (updateAccount) {
|
||||
account = accounts.update(account, a -> {
|
||||
a.getDevice(device.getId()).ifPresent(d -> d.setSignedPreKey(preKeys.getSignedPreKey()));
|
||||
a.setIdentityKey(preKeys.getIdentityKey());
|
||||
a.getDevice(device.getId()).ifPresent(d -> {
|
||||
if (usePhoneNumberIdentity) {
|
||||
d.setPhoneNumberIdentitySignedPreKey(preKeys.getSignedPreKey());
|
||||
} else {
|
||||
d.setSignedPreKey(preKeys.getSignedPreKey());
|
||||
}
|
||||
});
|
||||
|
||||
if (usePhoneNumberIdentity) {
|
||||
a.setPhoneNumberIdentityKey(preKeys.getIdentityKey());
|
||||
} else {
|
||||
a.setIdentityKey(preKeys.getIdentityKey());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
keysDynamoDb.store(account, device.getId(), preKeys.getPreKeys());
|
||||
keys.store(getIdentifier(account, identityType), device.getId(), preKeys.getPreKeys());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -129,14 +148,19 @@ public class KeysController {
|
||||
|
||||
final Optional<Account> account = auth.map(AuthenticatedAccount::getAccount);
|
||||
|
||||
Optional<Account> target = accounts.get(targetUuid);
|
||||
OptionalAccess.verify(account, accessKey, target, deviceId);
|
||||
final Account target;
|
||||
{
|
||||
final Optional<Account> maybeTarget = accounts.getByAccountIdentifier(targetUuid)
|
||||
.or(() -> accounts.getByPhoneNumberIdentifier(targetUuid));
|
||||
|
||||
assert (target.isPresent());
|
||||
OptionalAccess.verify(account, accessKey, maybeTarget, deviceId);
|
||||
|
||||
target = maybeTarget.orElseThrow();
|
||||
}
|
||||
|
||||
{
|
||||
final String sourceCountryCode = account.map(a -> Util.getCountryCode(a.getNumber())).orElse("0");
|
||||
final String targetCountryCode = target.map(a -> Util.getCountryCode(a.getNumber())).orElseThrow();
|
||||
final String targetCountryCode = Util.getCountryCode(target.getNumber());
|
||||
|
||||
Metrics.counter(PREKEY_REQUEST_COUNTER_NAME, Tags.of(
|
||||
SOURCE_COUNTRY_TAG_NAME, sourceCountryCode,
|
||||
@@ -146,7 +170,7 @@ public class KeysController {
|
||||
|
||||
if (account.isPresent()) {
|
||||
rateLimiters.getPreKeysLimiter().validate(
|
||||
account.get().getUuid() + "." + auth.get().getAuthenticatedDevice().getId() + "__" + target.get().getUuid()
|
||||
account.get().getUuid() + "." + auth.get().getAuthenticatedDevice().getId() + "__" + targetUuid
|
||||
+ "." + deviceId);
|
||||
|
||||
try {
|
||||
@@ -167,12 +191,14 @@ public class KeysController {
|
||||
}
|
||||
}
|
||||
|
||||
Map<Long, PreKey> preKeysByDeviceId = getLocalKeys(target.get(), deviceId);
|
||||
final boolean usePhoneNumberIdentity = target.getPhoneNumberIdentifier().equals(targetUuid);
|
||||
|
||||
Map<Long, PreKey> preKeysByDeviceId = getLocalKeys(target, deviceId, usePhoneNumberIdentity);
|
||||
List<PreKeyResponseItem> responseItems = new LinkedList<>();
|
||||
|
||||
for (Device device : target.get().getDevices()) {
|
||||
for (Device device : target.getDevices()) {
|
||||
if (device.isEnabled() && (deviceId.equals("*") || device.getId() == Long.parseLong(deviceId))) {
|
||||
SignedPreKey signedPreKey = device.getSignedPreKey();
|
||||
SignedPreKey signedPreKey = usePhoneNumberIdentity ? device.getPhoneNumberIdentitySignedPreKey() : device.getSignedPreKey();
|
||||
PreKey preKey = preKeysByDeviceId.get(device.getId());
|
||||
|
||||
if (signedPreKey != null || preKey != null) {
|
||||
@@ -181,48 +207,80 @@ public class KeysController {
|
||||
}
|
||||
}
|
||||
|
||||
final String identityKey = usePhoneNumberIdentity ? target.getPhoneNumberIdentityKey() : target.getIdentityKey();
|
||||
|
||||
if (responseItems.isEmpty()) return Response.status(404).build();
|
||||
else return Response.ok().entity(new PreKeyResponse(target.get().getIdentityKey(), responseItems)).build();
|
||||
else return Response.ok().entity(new PreKeyResponse(identityKey, responseItems)).build();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/signed")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setSignedKey(@Auth AuthenticatedAccount auth, @Valid SignedPreKey signedPreKey) {
|
||||
public void setSignedKey(@Auth final AuthenticatedAccount auth,
|
||||
@Valid final SignedPreKey signedPreKey,
|
||||
@QueryParam("identity") final Optional<String> identityType) {
|
||||
|
||||
Device device = auth.getAuthenticatedDevice();
|
||||
|
||||
accounts.updateDevice(auth.getAccount(), device.getId(), d -> d.setSignedPreKey(signedPreKey));
|
||||
accounts.updateDevice(auth.getAccount(), device.getId(), d -> {
|
||||
if (usePhoneNumberIdentity(identityType)) {
|
||||
d.setPhoneNumberIdentitySignedPreKey(signedPreKey);
|
||||
} else {
|
||||
d.setSignedPreKey(signedPreKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/signed")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Optional<SignedPreKey> getSignedKey(@Auth AuthenticatedAccount auth) {
|
||||
Device device = auth.getAuthenticatedDevice();
|
||||
SignedPreKey signedPreKey = device.getSignedPreKey();
|
||||
public Optional<SignedPreKey> getSignedKey(@Auth final AuthenticatedAccount auth,
|
||||
@QueryParam("identity") final Optional<String> identityType) {
|
||||
|
||||
if (signedPreKey != null) {
|
||||
return Optional.of(signedPreKey);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
Device device = auth.getAuthenticatedDevice();
|
||||
SignedPreKey signedPreKey = usePhoneNumberIdentity(identityType) ?
|
||||
device.getPhoneNumberIdentitySignedPreKey() : device.getSignedPreKey();
|
||||
|
||||
return Optional.ofNullable(signedPreKey);
|
||||
}
|
||||
|
||||
private Map<Long, PreKey> getLocalKeys(Account destination, String deviceIdSelector) {
|
||||
try {
|
||||
if (deviceIdSelector.equals("*")) {
|
||||
return keysDynamoDb.take(destination);
|
||||
private static boolean usePhoneNumberIdentity(final Optional<String> identityType) {
|
||||
return "pni".equals(identityType.map(String::toLowerCase).orElse("aci"));
|
||||
}
|
||||
|
||||
private static UUID getIdentifier(final Account account, final Optional<String> identityType) {
|
||||
return usePhoneNumberIdentity(identityType) ?
|
||||
account.getPhoneNumberIdentifier() :
|
||||
account.getUuid();
|
||||
}
|
||||
|
||||
private Map<Long, PreKey> getLocalKeys(Account destination, String deviceIdSelector, final boolean usePhoneNumberIdentity) {
|
||||
final Map<Long, PreKey> preKeys;
|
||||
|
||||
final UUID identifier = usePhoneNumberIdentity ?
|
||||
destination.getPhoneNumberIdentifier() :
|
||||
destination.getUuid();
|
||||
|
||||
if (deviceIdSelector.equals("*")) {
|
||||
preKeys = new HashMap<>();
|
||||
|
||||
for (final Device device : destination.getDevices()) {
|
||||
keys.take(identifier, device.getId()).ifPresent(preKey -> preKeys.put(device.getId(), preKey));
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
long deviceId = Long.parseLong(deviceIdSelector);
|
||||
|
||||
long deviceId = Long.parseLong(deviceIdSelector);
|
||||
|
||||
return keysDynamoDb.take(destination, deviceId)
|
||||
.map(preKey -> Map.of(deviceId, preKey))
|
||||
.orElse(Collections.emptyMap());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
preKeys = keys.take(identifier, deviceId)
|
||||
.map(preKey -> Map.of(deviceId, preKey))
|
||||
.orElse(Collections.emptyMap());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
}
|
||||
}
|
||||
|
||||
return preKeys;
|
||||
}
|
||||
}
|
||||
|
||||
+84
-56
@@ -6,14 +6,10 @@ package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.codahale.metrics.Histogram;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.Timer;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.dropwizard.util.DataSize;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
@@ -42,6 +38,7 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.BadRequestException;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
@@ -93,10 +90,11 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
|
||||
|
||||
@@ -104,14 +102,7 @@ import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
|
||||
@Path("/v1/messages")
|
||||
public class MessageController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MessageController.class);
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Meter unidentifiedMeter = metricRegistry.meter(name(getClass(), "delivery", "unidentified"));
|
||||
private final Meter identifiedMeter = metricRegistry.meter(name(getClass(), "delivery", "identified" ));
|
||||
private final Meter rejectOver256kibMessageMeter = metricRegistry.meter(name(getClass(), "rejectOver256kibMessage"));
|
||||
private final Timer sendMessageInternalTimer = metricRegistry.timer(name(getClass(), "sendMessageInternal"));
|
||||
private final Timer sendCommonMessageInternalTimer = metricRegistry.timer(name(getClass(), "sendCommonMessageInternal"));
|
||||
private final Histogram outgoingMessageListSizeHistogram = metricRegistry.histogram(name(getClass(), "outgoingMessageListSize"));
|
||||
private static final Logger logger = LoggerFactory.getLogger(MessageController.class);
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final MessageSender messageSender;
|
||||
@@ -124,6 +115,13 @@ public class MessageController {
|
||||
private final ReportMessageManager reportMessageManager;
|
||||
private final ExecutorService multiRecipientMessageExecutor;
|
||||
|
||||
@VisibleForTesting
|
||||
static final Semver FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE = new Semver("5.22.0.32");
|
||||
|
||||
@VisibleForTesting
|
||||
static final Semver IOS_VERSION_WITH_FIXED_ENVELOPE_TYPE = new Semver("5.25.0.0");
|
||||
|
||||
private static final String REJECT_OVERSIZE_MESSAGE_COUNTER = name(MessageController.class, "rejectOversizeMessage");
|
||||
private static final String LEGACY_MESSAGE_SENT_COUNTER = name(MessageController.class, "legacyMessageSent");
|
||||
private static final String SENT_MESSAGE_COUNTER_NAME = name(MessageController.class, "sentMessages");
|
||||
private static final String CONTENT_SIZE_DISTRIBUTION_NAME = name(MessageController.class, "messageContentSize");
|
||||
@@ -135,6 +133,10 @@ public class MessageController {
|
||||
private static final String SENDER_COUNTRY_TAG_NAME = "senderCountry";
|
||||
private static final String RATE_LIMIT_REASON_TAG_NAME = "rateLimitReason";
|
||||
|
||||
private static final String SENDER_TYPE_IDENTIFIED = "identified";
|
||||
private static final String SENDER_TYPE_UNIDENTIFIED = "unidentified";
|
||||
private static final String SENDER_TYPE_SELF = "self";
|
||||
|
||||
private static final long MAX_MESSAGE_SIZE = DataSize.kibibytes(256).toBytes();
|
||||
|
||||
public MessageController(
|
||||
@@ -180,14 +182,14 @@ public class MessageController {
|
||||
|
||||
final String senderType;
|
||||
|
||||
if (source.isPresent() && !source.get().getAccount().getUuid().equals(destinationUuid)) {
|
||||
identifiedMeter.mark();
|
||||
senderType = "identified";
|
||||
} else if (source.isEmpty()) {
|
||||
unidentifiedMeter.mark();
|
||||
senderType = "unidentified";
|
||||
if (source.isPresent()) {
|
||||
if (source.get().getAccount().isIdentifiedBy(destinationUuid)) {
|
||||
senderType = SENDER_TYPE_SELF;
|
||||
} else {
|
||||
senderType = SENDER_TYPE_IDENTIFIED;
|
||||
}
|
||||
} else {
|
||||
senderType = "self";
|
||||
senderType = SENDER_TYPE_UNIDENTIFIED;
|
||||
}
|
||||
|
||||
for (final IncomingMessage message : messages.getMessages()) {
|
||||
@@ -204,18 +206,19 @@ public class MessageController {
|
||||
Metrics.summary(CONTENT_SIZE_DISTRIBUTION_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).record(contentLength);
|
||||
|
||||
if (contentLength > MAX_MESSAGE_SIZE) {
|
||||
rejectOver256kibMessageMeter.mark();
|
||||
Metrics.counter(REJECT_OVERSIZE_MESSAGE_COUNTER, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment();
|
||||
return Response.status(Response.Status.REQUEST_ENTITY_TOO_LARGE).build();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
boolean isSyncMessage = source.isPresent() && source.get().getAccount().getUuid().equals(destinationUuid);
|
||||
boolean isSyncMessage = source.isPresent() && source.get().getAccount().isIdentifiedBy(destinationUuid);
|
||||
|
||||
Optional<Account> destination;
|
||||
|
||||
if (!isSyncMessage) {
|
||||
destination = accountsManager.get(destinationUuid);
|
||||
destination = accountsManager.getByAccountIdentifier(destinationUuid)
|
||||
.or(() -> accountsManager.getByPhoneNumberIdentifier(destinationUuid));
|
||||
} else {
|
||||
destination = source.map(AuthenticatedAccount::getAccount);
|
||||
}
|
||||
@@ -223,7 +226,7 @@ public class MessageController {
|
||||
OptionalAccess.verify(source.map(AuthenticatedAccount::getAccount), accessKey, destination);
|
||||
assert (destination.isPresent());
|
||||
|
||||
if (source.isPresent() && !source.get().getAccount().getUuid().equals(destinationUuid)) {
|
||||
if (source.isPresent() && !source.get().getAccount().isIdentifiedBy(destinationUuid)) {
|
||||
final String senderCountryCode = Util.getCountryCode(source.get().getAccount().getNumber());
|
||||
|
||||
try {
|
||||
@@ -268,8 +271,8 @@ public class MessageController {
|
||||
|
||||
if (destinationDevice.isPresent()) {
|
||||
Metrics.counter(SENT_MESSAGE_COUNTER_NAME, tags).increment();
|
||||
sendMessage(source, destination.get(), destinationDevice.get(), messages.getTimestamp(), messages.isOnline(),
|
||||
incomingMessage);
|
||||
sendMessage(source, destination.get(), destinationDevice.get(), destinationUuid, messages.getTimestamp(),
|
||||
messages.isOnline(), incomingMessage, userAgent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,13 +308,11 @@ public class MessageController {
|
||||
@QueryParam("ts") long timestamp,
|
||||
@Valid MultiRecipientMessage multiRecipientMessage) {
|
||||
|
||||
unidentifiedMeter.mark(multiRecipientMessage.getRecipients().length);
|
||||
|
||||
Map<UUID, Account> uuidToAccountMap = Arrays.stream(multiRecipientMessage.getRecipients())
|
||||
.map(Recipient::getUuid)
|
||||
.distinct()
|
||||
.collect(Collectors.toUnmodifiableMap(Function.identity(), uuid -> {
|
||||
Optional<Account> account = accountsManager.get(uuid);
|
||||
Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
|
||||
if (account.isEmpty()) {
|
||||
throw new WebApplicationException(Status.NOT_FOUND);
|
||||
}
|
||||
@@ -362,20 +363,21 @@ public class MessageController {
|
||||
.build();
|
||||
}
|
||||
|
||||
List<Tag> tags = List.of(
|
||||
UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(online)),
|
||||
Tag.of(SENDER_TYPE_TAG_NAME, "unidentified"));
|
||||
List<UUID> uuids404 = Collections.synchronizedList(new ArrayList<>());
|
||||
final Counter counter = Metrics.counter(SENT_MESSAGE_COUNTER_NAME, tags);
|
||||
|
||||
try {
|
||||
final Counter sentMessageCounter = Metrics.counter(SENT_MESSAGE_COUNTER_NAME, Tags.of(
|
||||
UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(online)),
|
||||
Tag.of(SENDER_TYPE_TAG_NAME, SENDER_TYPE_UNIDENTIFIED)));
|
||||
|
||||
multiRecipientMessageExecutor.invokeAll(Arrays.stream(multiRecipientMessage.getRecipients())
|
||||
.map(recipient -> (Callable<Void>) () -> {
|
||||
Account destinationAccount = uuidToAccountMap.get(recipient.getUuid());
|
||||
|
||||
// we asserted this must exist in validateCompleteDeviceList
|
||||
Device destinationDevice = destinationAccount.getDevice(recipient.getDeviceId()).orElseThrow();
|
||||
counter.increment();
|
||||
sentMessageCounter.increment();
|
||||
try {
|
||||
sendMessage(destinationAccount, destinationDevice, timestamp, online, recipient,
|
||||
multiRecipientMessage.getCommonPayload());
|
||||
@@ -444,8 +446,6 @@ public class MessageController {
|
||||
userAgent,
|
||||
false);
|
||||
|
||||
outgoingMessageListSizeHistogram.update(outgoingMessages.getMessages().size());
|
||||
|
||||
{
|
||||
String platform;
|
||||
|
||||
@@ -512,33 +512,56 @@ public class MessageController {
|
||||
private void sendMessage(Optional<AuthenticatedAccount> source,
|
||||
Account destinationAccount,
|
||||
Device destinationDevice,
|
||||
UUID destinationUuid,
|
||||
long timestamp,
|
||||
boolean online,
|
||||
IncomingMessage incomingMessage)
|
||||
IncomingMessage incomingMessage,
|
||||
String userAgentString)
|
||||
throws NoSuchUserException {
|
||||
try (final Timer.Context ignored = sendMessageInternalTimer.time()) {
|
||||
try {
|
||||
Optional<byte[]> messageBody = getMessageBody(incomingMessage);
|
||||
Optional<byte[]> messageContent = getMessageContent(incomingMessage);
|
||||
Envelope.Builder messageBuilder = Envelope.newBuilder();
|
||||
|
||||
messageBuilder.setType(Envelope.Type.forNumber(incomingMessage.getType()))
|
||||
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
|
||||
.setServerTimestamp(System.currentTimeMillis());
|
||||
int envelopeTypeNumber = incomingMessage.getType();
|
||||
|
||||
if (source.isPresent()) {
|
||||
messageBuilder.setSource(source.get().getAccount().getNumber())
|
||||
.setSourceUuid(source.get().getAccount().getUuid().toString())
|
||||
.setSourceDevice((int) source.get().getAuthenticatedDevice().getId());
|
||||
// Some versions of the iOS app incorrectly use the reserved envelope type 7 for PLAINTEXT_CONTENT instead of type
|
||||
// 8. This check can be removed safely after 2022-03-01.
|
||||
if (envelopeTypeNumber == 7) {
|
||||
try {
|
||||
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
|
||||
if (userAgent.getPlatform() == ClientPlatform.IOS &&
|
||||
userAgent.getVersion().isGreaterThanOrEqualTo(FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE) &&
|
||||
userAgent.getVersion().isLowerThan(IOS_VERSION_WITH_FIXED_ENVELOPE_TYPE)) {
|
||||
envelopeTypeNumber = Type.PLAINTEXT_CONTENT.getNumber();
|
||||
}
|
||||
} catch (final UnrecognizedUserAgentException ignored2) {
|
||||
}
|
||||
}
|
||||
|
||||
if (messageBody.isPresent()) {
|
||||
final Envelope.Type envelopeType = Envelope.Type.forNumber(envelopeTypeNumber);
|
||||
|
||||
if (envelopeType == null) {
|
||||
logger.warn("Received bad envelope type {} from {}", incomingMessage.getType(), userAgentString);
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
messageBuilder.setType(envelopeType)
|
||||
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
|
||||
.setServerTimestamp(System.currentTimeMillis())
|
||||
.setDestinationUuid(destinationUuid.toString());
|
||||
|
||||
source.ifPresent(authenticatedAccount ->
|
||||
messageBuilder.setSource(authenticatedAccount.getAccount().getNumber())
|
||||
.setSourceUuid(authenticatedAccount.getAccount().getUuid().toString())
|
||||
.setSourceDevice((int) authenticatedAccount.getAuthenticatedDevice().getId()));
|
||||
|
||||
messageBody.ifPresent(bytes -> {
|
||||
Metrics.counter(LEGACY_MESSAGE_SENT_COUNTER).increment();
|
||||
messageBuilder.setLegacyMessage(ByteString.copyFrom(messageBody.get()));
|
||||
}
|
||||
});
|
||||
|
||||
if (messageContent.isPresent()) {
|
||||
messageBuilder.setContent(ByteString.copyFrom(messageContent.get()));
|
||||
}
|
||||
messageContent.ifPresent(bytes -> messageBuilder.setContent(ByteString.copyFrom(bytes)));
|
||||
|
||||
messageSender.sendMessage(destinationAccount, destinationDevice, messageBuilder.build(), online);
|
||||
} catch (NotPushRegisteredException e) {
|
||||
@@ -547,9 +570,13 @@ public class MessageController {
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessage(Account destinationAccount, Device destinationDevice, long timestamp, boolean online,
|
||||
Recipient recipient, byte[] commonPayload) throws NoSuchUserException {
|
||||
try (final Timer.Context ignored = sendCommonMessageInternalTimer.time()) {
|
||||
private void sendMessage(Account destinationAccount,
|
||||
Device destinationDevice,
|
||||
long timestamp,
|
||||
boolean online,
|
||||
Recipient recipient,
|
||||
byte[] commonPayload) throws NoSuchUserException {
|
||||
try {
|
||||
Envelope.Builder messageBuilder = Envelope.newBuilder();
|
||||
long serverTimestamp = System.currentTimeMillis();
|
||||
byte[] recipientKeyMaterial = recipient.getPerRecipientKeyMaterial();
|
||||
@@ -563,7 +590,8 @@ public class MessageController {
|
||||
.setType(Type.UNIDENTIFIED_SENDER)
|
||||
.setTimestamp(timestamp == 0 ? serverTimestamp : timestamp)
|
||||
.setServerTimestamp(serverTimestamp)
|
||||
.setContent(ByteString.copyFrom(payload));
|
||||
.setContent(ByteString.copyFrom(payload))
|
||||
.setDestinationUuid(destinationAccount.getUuid().toString());
|
||||
|
||||
messageSender.sendMessage(destinationAccount, destinationDevice, messageBuilder.build(), online);
|
||||
} catch (NotPushRegisteredException e) {
|
||||
|
||||
+22
-14
@@ -19,7 +19,6 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -40,6 +39,8 @@ import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
@@ -65,6 +66,7 @@ import org.whispersystems.textsecuregcm.entities.Profile;
|
||||
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.UserCapabilities;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
@@ -76,10 +78,11 @@ import org.whispersystems.textsecuregcm.storage.UsernamesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@Path("/v1/profile")
|
||||
public class ProfileController {
|
||||
@@ -102,6 +105,8 @@ public class ProfileController {
|
||||
private final S3Client s3client;
|
||||
private final String bucket;
|
||||
|
||||
private static final String LEGACY_GET_PROFILE_COUNTER_NAME = name(ProfileController.class, "legacyGetProfileByPlatform");
|
||||
|
||||
public ProfileController(
|
||||
Clock clock,
|
||||
RateLimiters rateLimiters,
|
||||
@@ -137,13 +142,14 @@ public class ProfileController {
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response setProfile(@Auth AuthenticatedAccount auth, @Valid CreateProfileRequest request) {
|
||||
final Set<String> allowedPaymentsCountryCodes =
|
||||
dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getAllowedCountryCodes();
|
||||
if (StringUtils.isNotBlank(request.getPaymentAddress())) {
|
||||
final boolean hasDisallowedPrefix =
|
||||
dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getDisallowedPrefixes().stream()
|
||||
.anyMatch(prefix -> auth.getAccount().getNumber().startsWith(prefix));
|
||||
|
||||
if (StringUtils.isNotBlank(request.getPaymentAddress()) &&
|
||||
!allowedPaymentsCountryCodes.contains(Util.getCountryCode(auth.getAccount().getNumber()))) {
|
||||
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
if (hasDisallowedPrefix) {
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
}
|
||||
}
|
||||
|
||||
Optional<VersionedProfile> currentProfile = profilesManager.get(auth.getAccount().getUuid(), request.getVersion());
|
||||
@@ -251,7 +257,7 @@ public class ProfileController {
|
||||
isSelf = uuid.equals(authedUuid);
|
||||
}
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(uuid);
|
||||
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(uuid);
|
||||
OptionalAccess.verify(requestAccount, accessKey, accountProfile);
|
||||
|
||||
assert(accountProfile.isPresent());
|
||||
@@ -316,7 +322,7 @@ public class ProfileController {
|
||||
|
||||
final boolean isSelf = auth.getAccount().getUuid().equals(uuid.get());
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(uuid.get());
|
||||
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(uuid.get());
|
||||
|
||||
if (accountProfile.isEmpty()) {
|
||||
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
||||
@@ -368,7 +374,7 @@ public class ProfileController {
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/name/{name}")
|
||||
public void setProfile(@Auth AuthenticatedAccount auth,
|
||||
public void setLegacyProfile(@Auth AuthenticatedAccount auth,
|
||||
@PathParam("name") @ExactlySize(value = {72, 108}, payload = {Unwrapping.Unwrap.class}) Optional<String> name) {
|
||||
accountsManager.update(auth.getAccount(), a -> a.setProfileName(name.orElse(null)));
|
||||
}
|
||||
@@ -378,7 +384,7 @@ public class ProfileController {
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{identifier}")
|
||||
public Profile getProfile(
|
||||
public Profile getLegacyProfile(
|
||||
@Auth Optional<AuthenticatedAccount> auth,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@Context ContainerRequestContext containerRequestContext,
|
||||
@@ -391,6 +397,8 @@ public class ProfileController {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
Metrics.counter(LEGACY_GET_PROFILE_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment();
|
||||
|
||||
boolean isSelf = false;
|
||||
if (auth.isPresent()) {
|
||||
UUID authedUuid = auth.get().getAccount().getUuid();
|
||||
@@ -398,7 +406,7 @@ public class ProfileController {
|
||||
isSelf = authedUuid.equals(identifier);
|
||||
}
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(identifier);
|
||||
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(identifier);
|
||||
OptionalAccess.verify(auth.map(AuthenticatedAccount::getAccount), accessKey, accountProfile);
|
||||
|
||||
Optional<String> username = usernamesManager.get(accountProfile.get().getUuid());
|
||||
@@ -427,7 +435,7 @@ public class ProfileController {
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/form/avatar")
|
||||
public ProfileAvatarUploadAttributes getAvatarUploadForm(@Auth AuthenticatedAccount auth) {
|
||||
public ProfileAvatarUploadAttributes getLegacyAvatarUploadForm(@Auth AuthenticatedAccount auth) {
|
||||
String previousAvatar = auth.getAccount().getAvatar();
|
||||
String objectName = generateAvatarObjectName();
|
||||
ProfileAvatarUploadAttributes profileAvatarUploadAttributes = generateAvatarUploadForm(objectName);
|
||||
|
||||
+56
-43
@@ -21,7 +21,6 @@ import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -55,6 +54,7 @@ import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.receipts.ReceiptCredentialRequest;
|
||||
@@ -562,10 +562,13 @@ public class SubscriptionController {
|
||||
return stripeManager.getPaymentIntent(request.getPaymentIntentId())
|
||||
.thenCompose(paymentIntent -> {
|
||||
if (paymentIntent == null) {
|
||||
throw new WebApplicationException(Status.NOT_FOUND);
|
||||
}
|
||||
if (StringUtils.equalsIgnoreCase("processing", paymentIntent.getStatus())) {
|
||||
throw new WebApplicationException(Status.NO_CONTENT);
|
||||
}
|
||||
if (!"succeeded".equalsIgnoreCase(paymentIntent.getStatus())) {
|
||||
throw new WebApplicationException(Status.NO_CONTENT);
|
||||
if (!StringUtils.equalsIgnoreCase("succeeded", paymentIntent.getStatus())) {
|
||||
throw new WebApplicationException(Status.PAYMENT_REQUIRED);
|
||||
}
|
||||
ReceiptCredentialRequest receiptCredentialRequest;
|
||||
try {
|
||||
@@ -603,6 +606,7 @@ public class SubscriptionController {
|
||||
private final boolean cancelAtPeriodEnd;
|
||||
private final String currency;
|
||||
private final BigDecimal amount;
|
||||
private final String status;
|
||||
|
||||
public Subscription(
|
||||
@JsonProperty("level") long level,
|
||||
@@ -611,7 +615,8 @@ public class SubscriptionController {
|
||||
@JsonProperty("active") boolean active,
|
||||
@JsonProperty("cancelAtPeriodEnd") boolean cancelAtPeriodEnd,
|
||||
@JsonProperty("currency") String currency,
|
||||
@JsonProperty("amount") BigDecimal amount) {
|
||||
@JsonProperty("amount") BigDecimal amount,
|
||||
@JsonProperty("status") String status) {
|
||||
this.level = level;
|
||||
this.billingCycleAnchor = billingCycleAnchor;
|
||||
this.endOfCurrentPeriod = endOfCurrentPeriod;
|
||||
@@ -619,6 +624,7 @@ public class SubscriptionController {
|
||||
this.cancelAtPeriodEnd = cancelAtPeriodEnd;
|
||||
this.currency = currency;
|
||||
this.amount = amount;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public long getLevel() {
|
||||
@@ -648,6 +654,10 @@ public class SubscriptionController {
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
private final Subscription subscription;
|
||||
@@ -688,7 +698,8 @@ public class SubscriptionController {
|
||||
Objects.equals(subscription.getStatus(), "active"),
|
||||
subscription.getCancelAtPeriodEnd(),
|
||||
price.getCurrency().toUpperCase(Locale.ROOT),
|
||||
price.getUnitAmountDecimal()
|
||||
price.getUnitAmountDecimal(),
|
||||
subscription.getStatus()
|
||||
))).build())));
|
||||
});
|
||||
}
|
||||
@@ -730,7 +741,7 @@ public class SubscriptionController {
|
||||
@Path("/{subscriberId}/receipt_credentials")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> getReceiptCredentials(
|
||||
public CompletableFuture<Response> createSubscriptionReceiptCredentials(
|
||||
@Auth Optional<AuthenticatedAccount> authenticatedAccount,
|
||||
@PathParam("subscriberId") String subscriberId,
|
||||
@Valid GetReceiptCredentialsRequest request) {
|
||||
@@ -739,7 +750,7 @@ public class SubscriptionController {
|
||||
.thenApply(this::requireRecordFromGetResult)
|
||||
.thenCompose(record -> {
|
||||
if (record.subscriptionId == null) {
|
||||
return CompletableFuture.completedFuture(Response.noContent().build());
|
||||
return CompletableFuture.completedFuture(Response.status(Status.NOT_FOUND).build());
|
||||
}
|
||||
ReceiptCredentialRequest receiptCredentialRequest;
|
||||
try {
|
||||
@@ -747,29 +758,20 @@ public class SubscriptionController {
|
||||
} catch (InvalidInputException e) {
|
||||
throw new BadRequestException("invalid receipt credential request", e);
|
||||
}
|
||||
return stripeManager.getPaidInvoicesForSubscription(record.subscriptionId, requestData.now)
|
||||
.thenCompose(invoices -> checkNextInvoice(invoices.iterator(), record.subscriptionId))
|
||||
.thenCompose(receipt -> {
|
||||
if (receipt == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
return issuedReceiptsManager.recordIssuance(
|
||||
receipt.getInvoiceLineItemId(), receiptCredentialRequest, requestData.now)
|
||||
.thenApply(unused -> receipt);
|
||||
})
|
||||
return stripeManager.getLatestInvoiceForSubscription(record.subscriptionId)
|
||||
.thenCompose(invoice -> convertInvoiceToReceipt(invoice, record.subscriptionId))
|
||||
.thenCompose(receipt -> issuedReceiptsManager.recordIssuance(
|
||||
receipt.getInvoiceLineItemId(), receiptCredentialRequest, requestData.now)
|
||||
.thenApply(unused -> receipt))
|
||||
.thenApply(receipt -> {
|
||||
if (receipt == null) {
|
||||
return Response.noContent().build();
|
||||
} else {
|
||||
ReceiptCredentialResponse receiptCredentialResponse;
|
||||
try {
|
||||
receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(
|
||||
receiptCredentialRequest, receipt.getExpiration().getEpochSecond(), receipt.getLevel());
|
||||
} catch (VerificationFailedException e) {
|
||||
throw new BadRequestException("receipt credential request failed verification", e);
|
||||
}
|
||||
return Response.ok(new GetReceiptCredentialsResponse(receiptCredentialResponse.serialize())).build();
|
||||
ReceiptCredentialResponse receiptCredentialResponse;
|
||||
try {
|
||||
receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(
|
||||
receiptCredentialRequest, receipt.getExpiration().getEpochSecond(), receipt.getLevel());
|
||||
} catch (VerificationFailedException e) {
|
||||
throw new BadRequestException("receipt credential request failed verification", e);
|
||||
}
|
||||
return Response.ok(new GetReceiptCredentialsResponse(receiptCredentialResponse.serialize())).build();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -799,35 +801,46 @@ public class SubscriptionController {
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Receipt> checkNextInvoice(Iterator<Invoice> invoiceIterator, String subscriptionId) {
|
||||
if (!invoiceIterator.hasNext()) {
|
||||
return null;
|
||||
private CompletableFuture<Receipt> convertInvoiceToReceipt(Invoice latestSubscriptionInvoice, String subscriptionId) {
|
||||
if (latestSubscriptionInvoice == null) {
|
||||
throw new WebApplicationException(Status.NO_CONTENT);
|
||||
}
|
||||
if (StringUtils.equalsIgnoreCase("open", latestSubscriptionInvoice.getStatus())) {
|
||||
throw new WebApplicationException(Status.NO_CONTENT);
|
||||
}
|
||||
if (!StringUtils.equalsIgnoreCase("paid", latestSubscriptionInvoice.getStatus())) {
|
||||
throw new WebApplicationException(Status.PAYMENT_REQUIRED);
|
||||
}
|
||||
|
||||
Invoice invoice = invoiceIterator.next();
|
||||
return stripeManager.getInvoiceLineItemsForInvoice(invoice).thenCompose(invoiceLineItems -> {
|
||||
return stripeManager.getInvoiceLineItemsForInvoice(latestSubscriptionInvoice).thenCompose(invoiceLineItems -> {
|
||||
Collection<InvoiceLineItem> subscriptionLineItems = invoiceLineItems.stream()
|
||||
.filter(invoiceLineItem -> Objects.equals("subscription", invoiceLineItem.getType()))
|
||||
.collect(Collectors.toList());
|
||||
if (subscriptionLineItems.isEmpty()) {
|
||||
return checkNextInvoice(invoiceIterator, subscriptionId);
|
||||
throw new IllegalStateException("latest subscription invoice has no subscription line items; subscriptionId="
|
||||
+ subscriptionId + "; invoiceId=" + latestSubscriptionInvoice.getId());
|
||||
}
|
||||
if (subscriptionLineItems.size() > 1) {
|
||||
throw new IllegalStateException("invoice has more than one subscription; subscriptionId=" + subscriptionId
|
||||
+ "; count=" + subscriptionLineItems.size());
|
||||
throw new IllegalStateException(
|
||||
"latest subscription invoice has too many subscription line items; subscriptionId=" + subscriptionId
|
||||
+ "; invoiceId=" + latestSubscriptionInvoice.getId() + "; count=" + subscriptionLineItems.size());
|
||||
}
|
||||
|
||||
InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get();
|
||||
return stripeManager.getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new Receipt(
|
||||
Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd())
|
||||
.plus(subscriptionConfiguration.getBadgeGracePeriod())
|
||||
.truncatedTo(ChronoUnit.DAYS)
|
||||
.plus(1, ChronoUnit.DAYS),
|
||||
stripeManager.getLevelForProduct(product),
|
||||
subscriptionLineItem.getId()));
|
||||
return getReceiptForSubscriptionInvoiceLineItem(subscriptionLineItem);
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<Receipt> getReceiptForSubscriptionInvoiceLineItem(InvoiceLineItem subscriptionLineItem) {
|
||||
return stripeManager.getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new Receipt(
|
||||
Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd())
|
||||
.plus(subscriptionConfiguration.getBadgeGracePeriod())
|
||||
.truncatedTo(ChronoUnit.DAYS)
|
||||
.plus(1, ChronoUnit.DAYS),
|
||||
stripeManager.getLevelForProduct(product),
|
||||
subscriptionLineItem.getId()));
|
||||
}
|
||||
|
||||
private SubscriptionManager.Record requireRecordFromGetResult(SubscriptionManager.GetResult getResult) {
|
||||
if (getResult == GetResult.PASSWORD_MISMATCH) {
|
||||
throw new ForbiddenException("subscriberId mismatch");
|
||||
|
||||
+28
-26
@@ -23,32 +23,34 @@ import org.whispersystems.textsecuregcm.util.Util;
|
||||
@Path("/v1/voice/")
|
||||
public class VoiceVerificationController {
|
||||
|
||||
private static final String PLAY_TWIML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
"<Response>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Pause length=\"1\"/>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Pause length=\"1\"/>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
"</Response>";
|
||||
private static final String PLAY_TWIML = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Response>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Pause length="1"/>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Pause length="1"/>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
</Response>
|
||||
""";
|
||||
|
||||
private static final String DEFAULT_LOCALE = "en-US";
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import javax.validation.constraints.Size;
|
||||
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
|
||||
public class AccountAttributes {
|
||||
|
||||
@@ -25,6 +26,7 @@ public class AccountAttributes {
|
||||
private String registrationLock;
|
||||
|
||||
@JsonProperty
|
||||
@ExactlySize({0, 16})
|
||||
private byte[] unidentifiedAccessKey;
|
||||
|
||||
@JsonProperty
|
||||
@@ -80,4 +82,9 @@ public class AccountAttributes {
|
||||
public boolean isDiscoverableByPhoneNumber() {
|
||||
return discoverableByPhoneNumber;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setUnidentifiedAccessKey(final byte[] unidentifiedAccessKey) {
|
||||
this.unidentifiedAccessKey = unidentifiedAccessKey;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -8,29 +8,25 @@ package org.whispersystems.textsecuregcm.entities;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class AccountCreationResult {
|
||||
|
||||
@JsonProperty
|
||||
private final UUID uuid;
|
||||
|
||||
@JsonProperty
|
||||
private final String number;
|
||||
|
||||
@JsonProperty
|
||||
private final UUID pni;
|
||||
private final boolean storageCapable;
|
||||
|
||||
@JsonCreator
|
||||
public AccountCreationResult(
|
||||
@JsonProperty("uuid") final UUID uuid,
|
||||
@JsonProperty("number") final String number,
|
||||
@JsonProperty("pni") final UUID pni,
|
||||
@JsonProperty("storageCapable") final boolean storageCapable) {
|
||||
|
||||
this.uuid = uuid;
|
||||
this.number = number;
|
||||
this.pni = pni;
|
||||
this.storageCapable = storageCapable;
|
||||
}
|
||||
|
||||
@@ -42,6 +38,10 @@ public class AccountCreationResult {
|
||||
return number;
|
||||
}
|
||||
|
||||
public UUID getPni() {
|
||||
return pni;
|
||||
}
|
||||
|
||||
public boolean isStorageCapable() {
|
||||
return storageCapable;
|
||||
}
|
||||
|
||||
@@ -72,6 +72,15 @@ public class Badge {
|
||||
return svgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for old Android builds that expect this field to exist but don't care it's an empty string.
|
||||
*/
|
||||
@Deprecated
|
||||
@JsonProperty
|
||||
public String getImageUrl() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
|
||||
@@ -14,13 +14,11 @@ import javax.validation.constraints.NotEmpty;
|
||||
public class BadgeSvg {
|
||||
private final String light;
|
||||
private final String dark;
|
||||
private final String transparent;
|
||||
|
||||
@JsonCreator
|
||||
public BadgeSvg(
|
||||
@JsonProperty("light") final String light,
|
||||
@JsonProperty("dark") final String dark,
|
||||
@JsonProperty("transparent") final String transparent) {
|
||||
@JsonProperty("dark") final String dark) {
|
||||
if (Strings.isNullOrEmpty(light)) {
|
||||
throw new IllegalArgumentException("light cannot be empty");
|
||||
}
|
||||
@@ -29,10 +27,6 @@ public class BadgeSvg {
|
||||
throw new IllegalArgumentException("dark cannot be empty");
|
||||
}
|
||||
this.dark = dark;
|
||||
if (Strings.isNullOrEmpty(transparent)) {
|
||||
throw new IllegalArgumentException("transparent cannot be empty");
|
||||
}
|
||||
this.transparent = transparent;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
@@ -45,11 +39,6 @@ public class BadgeSvg {
|
||||
return dark;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getTransparent() {
|
||||
return transparent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
@@ -60,13 +49,12 @@ public class BadgeSvg {
|
||||
}
|
||||
BadgeSvg badgeSvg = (BadgeSvg) o;
|
||||
return Objects.equals(light, badgeSvg.light)
|
||||
&& Objects.equals(dark, badgeSvg.dark)
|
||||
&& Objects.equals(transparent, badgeSvg.transparent);
|
||||
&& Objects.equals(dark, badgeSvg.dark);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(light, dark, transparent);
|
||||
return Objects.hash(light, dark);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -74,7 +62,6 @@ public class BadgeSvg {
|
||||
return "BadgeSvg{" +
|
||||
"light='" + light + '\'' +
|
||||
", dark='" + dark + '\'' +
|
||||
", transparent='" + transparent + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
+33
-60
@@ -5,60 +5,39 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public class OutgoingMessageEntity {
|
||||
|
||||
@JsonIgnore
|
||||
private long id;
|
||||
private final UUID guid;
|
||||
private final int type;
|
||||
private final String relay;
|
||||
private final long timestamp;
|
||||
private final String source;
|
||||
private final UUID sourceUuid;
|
||||
private final int sourceDevice;
|
||||
private final UUID destinationUuid;
|
||||
private final byte[] message;
|
||||
private final byte[] content;
|
||||
private final long serverTimestamp;
|
||||
|
||||
@JsonIgnore
|
||||
private boolean cached;
|
||||
|
||||
@JsonProperty
|
||||
private UUID guid;
|
||||
|
||||
@JsonProperty
|
||||
private int type;
|
||||
|
||||
@JsonProperty
|
||||
private String relay;
|
||||
|
||||
@JsonProperty
|
||||
private long timestamp;
|
||||
|
||||
@JsonProperty
|
||||
private String source;
|
||||
|
||||
@JsonProperty
|
||||
private UUID sourceUuid;
|
||||
|
||||
@JsonProperty
|
||||
private int sourceDevice;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] message;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] content;
|
||||
|
||||
@JsonProperty
|
||||
private long serverTimestamp;
|
||||
|
||||
public OutgoingMessageEntity() {}
|
||||
|
||||
public OutgoingMessageEntity(long id, boolean cached,
|
||||
UUID guid, int type, String relay, long timestamp,
|
||||
String source, UUID sourceUuid, int sourceDevice,
|
||||
byte[] message, byte[] content, long serverTimestamp)
|
||||
@JsonCreator
|
||||
public OutgoingMessageEntity(@JsonProperty("guid") final UUID guid,
|
||||
@JsonProperty("type") final int type,
|
||||
@JsonProperty("relay") final String relay,
|
||||
@JsonProperty("timestamp") final long timestamp,
|
||||
@JsonProperty("source") final String source,
|
||||
@JsonProperty("sourceUuid") final UUID sourceUuid,
|
||||
@JsonProperty("sourceDevice") final int sourceDevice,
|
||||
@JsonProperty("destinationUuid") final UUID destinationUuid,
|
||||
@JsonProperty("message") final byte[] message,
|
||||
@JsonProperty("content") final byte[] content,
|
||||
@JsonProperty("serverTimestamp") final long serverTimestamp)
|
||||
{
|
||||
this.id = id;
|
||||
this.cached = cached;
|
||||
this.guid = guid;
|
||||
this.type = type;
|
||||
this.relay = relay;
|
||||
@@ -66,6 +45,7 @@ public class OutgoingMessageEntity {
|
||||
this.source = source;
|
||||
this.sourceUuid = sourceUuid;
|
||||
this.sourceDevice = sourceDevice;
|
||||
this.destinationUuid = destinationUuid;
|
||||
this.message = message;
|
||||
this.content = content;
|
||||
this.serverTimestamp = serverTimestamp;
|
||||
@@ -99,6 +79,10 @@ public class OutgoingMessageEntity {
|
||||
return sourceDevice;
|
||||
}
|
||||
|
||||
public UUID getDestinationUuid() {
|
||||
return destinationUuid;
|
||||
}
|
||||
|
||||
public byte[] getMessage() {
|
||||
return message;
|
||||
}
|
||||
@@ -107,16 +91,6 @@ public class OutgoingMessageEntity {
|
||||
return content;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isCached() {
|
||||
return cached;
|
||||
}
|
||||
|
||||
public long getServerTimestamp() {
|
||||
return serverTimestamp;
|
||||
}
|
||||
@@ -126,23 +100,22 @@ public class OutgoingMessageEntity {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final OutgoingMessageEntity that = (OutgoingMessageEntity)o;
|
||||
return id == that.id &&
|
||||
cached == that.cached &&
|
||||
type == that.type &&
|
||||
return type == that.type &&
|
||||
timestamp == that.timestamp &&
|
||||
sourceDevice == that.sourceDevice &&
|
||||
serverTimestamp == that.serverTimestamp &&
|
||||
Objects.equals(guid, that.guid) &&
|
||||
guid.equals(that.guid) &&
|
||||
Objects.equals(relay, that.relay) &&
|
||||
Objects.equals(source, that.source) &&
|
||||
Objects.equals(sourceUuid, that.sourceUuid) &&
|
||||
destinationUuid.equals(that.destinationUuid) &&
|
||||
Arrays.equals(message, that.message) &&
|
||||
Arrays.equals(content, that.content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(id, cached, guid, type, relay, timestamp, source, sourceUuid, sourceDevice, serverTimestamp);
|
||||
int result = Objects.hash(guid, type, relay, timestamp, source, sourceUuid, sourceDevice, destinationUuid, serverTimestamp);
|
||||
result = 31 * result + Arrays.hashCode(message);
|
||||
result = 31 * result + Arrays.hashCode(content);
|
||||
return result;
|
||||
|
||||
@@ -39,6 +39,8 @@ public class RateLimiters {
|
||||
private final RateLimiter usernameLookupLimiter;
|
||||
private final RateLimiter usernameSetLimiter;
|
||||
|
||||
private final RateLimiter checkAccountExistenceLimiter;
|
||||
|
||||
private final AtomicReference<CardinalityRateLimiter> unsealedSenderCardinalityLimiter;
|
||||
private final AtomicReference<RateLimiter> unsealedIpLimiter;
|
||||
private final AtomicReference<RateLimiter> rateLimitResetLimiter;
|
||||
@@ -127,6 +129,10 @@ public class RateLimiters {
|
||||
config.getUsernameSet().getBucketSize(),
|
||||
config.getUsernameSet().getLeakRatePerMinute());
|
||||
|
||||
this.checkAccountExistenceLimiter = new RateLimiter(cacheCluster, "checkAccountExistence",
|
||||
config.getCheckAccountExistence().getBucketSize(),
|
||||
config.getCheckAccountExistence().getLeakRatePerMinute());
|
||||
|
||||
this.dailyPreKeysLimiter = new AtomicReference<>(createDailyPreKeysLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getDailyPreKeys()));
|
||||
|
||||
this.unsealedSenderCardinalityLimiter = new AtomicReference<>(createUnsealedSenderCardinalityLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getUnsealedSenderNumber()));
|
||||
@@ -287,6 +293,10 @@ public class RateLimiters {
|
||||
return usernameSetLimiter;
|
||||
}
|
||||
|
||||
public RateLimiter getCheckAccountExistenceLimiter() {
|
||||
return checkAccountExistenceLimiter;
|
||||
}
|
||||
|
||||
private CardinalityRateLimiter createUnsealedSenderCardinalityLimiter(FaultTolerantRedisCluster cacheCluster, CardinalityRateLimitConfiguration configuration) {
|
||||
return new CardinalityRateLimiter(cacheCluster, "unsealedSender", configuration.getTtl(), configuration.getMaxCardinality());
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.mappers;
|
||||
|
||||
import java.util.concurrent.CompletionException;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
import javax.ws.rs.ext.Providers;
|
||||
|
||||
@Provider
|
||||
public class CompletionExceptionMapper implements ExceptionMapper<CompletionException> {
|
||||
|
||||
@Context
|
||||
private Providers providers;
|
||||
|
||||
@Override
|
||||
public Response toResponse(final CompletionException exception) {
|
||||
final Throwable cause = exception.getCause();
|
||||
|
||||
if (cause != null) {
|
||||
final Class type = cause.getClass();
|
||||
return providers.getExceptionMapper(type).toResponse(cause);
|
||||
}
|
||||
|
||||
return Response.serverError().build();
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,6 @@ public class CpuUsageGauge extends CachedGauge<Integer> {
|
||||
|
||||
@Override
|
||||
protected Integer loadValue() {
|
||||
return (int) Math.ceil(operatingSystemMXBean.getSystemCpuLoad() * 100);
|
||||
return (int) Math.ceil(operatingSystemMXBean.getCpuLoad() * 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ package org.whispersystems.textsecuregcm.metrics;
|
||||
|
||||
import com.codahale.metrics.Gauge;
|
||||
import com.sun.management.OperatingSystemMXBean;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
|
||||
public class FreeMemoryGauge implements Gauge<Long> {
|
||||
@@ -21,6 +20,6 @@ public class FreeMemoryGauge implements Gauge<Long> {
|
||||
|
||||
@Override
|
||||
public Long getValue() {
|
||||
return operatingSystemMXBean.getFreePhysicalMemorySize();
|
||||
return operatingSystemMXBean.getFreeMemorySize();
|
||||
}
|
||||
}
|
||||
|
||||
+145
-37
@@ -6,17 +6,34 @@
|
||||
package org.whispersystems.textsecuregcm.metrics;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import io.lettuce.core.SetArgs;
|
||||
import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
|
||||
/**
|
||||
* Measures and records the latency between sending a push notification to a device and that device draining its queue
|
||||
@@ -28,48 +45,139 @@ import java.util.concurrent.TimeUnit;
|
||||
* device and records the time elapsed since the push notification timestamp as a latency observation.
|
||||
*/
|
||||
public class PushLatencyManager {
|
||||
private static final String TIMER_NAME = MetricRegistry.name(PushLatencyManager.class, "latency");
|
||||
private static final int TTL = (int)Duration.ofDays(1).toSeconds();
|
||||
|
||||
private final FaultTolerantRedisCluster redisCluster;
|
||||
private final FaultTolerantRedisCluster redisCluster;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
|
||||
public PushLatencyManager(final FaultTolerantRedisCluster redisCluster) {
|
||||
this.redisCluster = redisCluster;
|
||||
private final Clock clock;
|
||||
|
||||
private static final String TIMER_NAME = MetricRegistry.name(PushLatencyManager.class, "latency");
|
||||
private static final int TTL = (int) Duration.ofDays(1).toSeconds();
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PushLatencyManager.class);
|
||||
|
||||
@VisibleForTesting
|
||||
enum PushType {
|
||||
STANDARD,
|
||||
VOIP
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class PushRecord {
|
||||
private final Instant timestamp;
|
||||
|
||||
@Nullable
|
||||
private final PushType pushType;
|
||||
|
||||
@JsonCreator
|
||||
PushRecord(@JsonProperty("timestamp") final Instant timestamp,
|
||||
@JsonProperty("pushType") @Nullable final PushType pushType) {
|
||||
|
||||
this.timestamp = timestamp;
|
||||
this.pushType = pushType;
|
||||
}
|
||||
|
||||
public void recordPushSent(final UUID accountUuid, final long deviceId) {
|
||||
recordPushSent(accountUuid, deviceId, System.currentTimeMillis());
|
||||
public Instant getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void recordPushSent(final UUID accountUuid, final long deviceId, final long currentTime) {
|
||||
redisCluster.useCluster(connection ->
|
||||
connection.async().set(getFirstUnacknowledgedPushKey(accountUuid, deviceId), String.valueOf(currentTime), SetArgs.Builder.nx().ex(TTL)));
|
||||
@Nullable
|
||||
public PushType getPushType() {
|
||||
return pushType;
|
||||
}
|
||||
}
|
||||
|
||||
public void recordQueueRead(final UUID accountUuid, final long deviceId, final String userAgent) {
|
||||
getLatencyAndClearTimestamp(accountUuid, deviceId, System.currentTimeMillis()).thenAccept(latency -> {
|
||||
if (latency != null) {
|
||||
Metrics.timer(TIMER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).record(latency, TimeUnit.MILLISECONDS);
|
||||
public PushLatencyManager(final FaultTolerantRedisCluster redisCluster,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
||||
|
||||
this(redisCluster, dynamicConfigurationManager, Clock.systemUTC());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
PushLatencyManager(final FaultTolerantRedisCluster redisCluster,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||
final Clock clock) {
|
||||
|
||||
this.redisCluster = redisCluster;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public void recordPushSent(final UUID accountUuid, final long deviceId, final boolean isVoip) {
|
||||
try {
|
||||
final String recordJson = SystemMapper.getMapper().writeValueAsString(
|
||||
new PushRecord(Instant.now(clock), isVoip ? PushType.VOIP : PushType.STANDARD));
|
||||
|
||||
redisCluster.useCluster(connection ->
|
||||
connection.async().set(getFirstUnacknowledgedPushKey(accountUuid, deviceId),
|
||||
recordJson,
|
||||
SetArgs.Builder.nx().ex(TTL)));
|
||||
} catch (final JsonProcessingException e) {
|
||||
// This should never happen
|
||||
log.error("Failed to write push latency record JSON", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void recordQueueRead(final UUID accountUuid, final long deviceId, final String userAgentString) {
|
||||
takePushRecord(accountUuid, deviceId).thenAccept(pushRecord -> {
|
||||
if (pushRecord != null) {
|
||||
final Duration latency = Duration.between(pushRecord.getTimestamp(), Instant.now());
|
||||
|
||||
final List<Tag> tags = new ArrayList<>(2);
|
||||
|
||||
tags.add(UserAgentTagUtil.getPlatformTag(userAgentString));
|
||||
|
||||
if (pushRecord.getPushType() != null) {
|
||||
tags.add(Tag.of("pushType", pushRecord.getPushType().name().toLowerCase()));
|
||||
}
|
||||
|
||||
try {
|
||||
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
|
||||
|
||||
final Set<Semver> instrumentedVersions =
|
||||
dynamicConfigurationManager.getConfiguration().getPushLatencyConfiguration().getInstrumentedVersions()
|
||||
.getOrDefault(userAgent.getPlatform(), Collections.emptySet());
|
||||
|
||||
if (instrumentedVersions.contains(userAgent.getVersion())) {
|
||||
tags.add(Tag.of("clientVersion", userAgent.getVersion().toString()));
|
||||
}
|
||||
} catch (UnrecognizedUserAgentException ignored) {
|
||||
}
|
||||
|
||||
Metrics.timer(TIMER_NAME, tags).record(latency);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
CompletableFuture<PushRecord> takePushRecord(final UUID accountUuid, final long deviceId) {
|
||||
final String key = getFirstUnacknowledgedPushKey(accountUuid, deviceId);
|
||||
|
||||
return redisCluster.withCluster(connection -> {
|
||||
final CompletableFuture<PushRecord> getFuture = connection.async().get(key).toCompletableFuture()
|
||||
.thenApply(recordJson -> {
|
||||
if (StringUtils.isNotEmpty(recordJson)) {
|
||||
try {
|
||||
return SystemMapper.getMapper().readValue(recordJson, PushRecord.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@VisibleForTesting
|
||||
CompletableFuture<Long> getLatencyAndClearTimestamp(final UUID accountUuid, final long deviceId, final long currentTimeMillis) {
|
||||
final String key = getFirstUnacknowledgedPushKey(accountUuid, deviceId);
|
||||
getFuture.whenComplete((record, cause) -> {
|
||||
if (cause == null) {
|
||||
connection.async().del(key);
|
||||
}
|
||||
});
|
||||
|
||||
return redisCluster.withCluster(connection -> {
|
||||
final RedisAdvancedClusterAsyncCommands<String, String> commands = connection.async();
|
||||
return getFuture;
|
||||
});
|
||||
}
|
||||
|
||||
final CompletableFuture<String> getFuture = commands.get(key).toCompletableFuture();
|
||||
commands.del(key);
|
||||
|
||||
return getFuture.thenApply(timestampString -> timestampString != null ? currentTimeMillis - Long.parseLong(timestampString, 10) : null);
|
||||
});
|
||||
}
|
||||
|
||||
private static String getFirstUnacknowledgedPushKey(final UUID accountUuid, final long deviceId) {
|
||||
return "push_latency::" + accountUuid.toString() + "::" + deviceId;
|
||||
}
|
||||
private static String getFirstUnacknowledgedPushKey(final UUID accountUuid, final long deviceId) {
|
||||
return "push_latency::v2::" + accountUuid.toString() + "::" + deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ public class APNSender implements Managed {
|
||||
private void handleUnregisteredUser(String registrationId, UUID uuid, long deviceId) {
|
||||
// logger.info("Got APN Unregistered: " + number + "," + deviceId);
|
||||
|
||||
Optional<Account> account = accountsManager.get(uuid);
|
||||
Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
logger.info("No account found: {}", uuid);
|
||||
|
||||
@@ -99,7 +99,7 @@ public class ApnFallbackManager implements Managed {
|
||||
|
||||
final Optional<Account> maybeAccount = separated.map(Pair::first)
|
||||
.map(UUID::fromString)
|
||||
.flatMap(accountsManager::get);
|
||||
.flatMap(accountsManager::getByAccountIdentifier);
|
||||
|
||||
final Optional<Device> maybeDevice = separated.map(Pair::second)
|
||||
.flatMap(deviceId -> maybeAccount.flatMap(account -> account.getDevice(deviceId)));
|
||||
|
||||
@@ -139,7 +139,7 @@ public class GCMSender {
|
||||
}
|
||||
|
||||
private Optional<Account> getAccountForEvent(GcmMessage message) {
|
||||
Optional<Account> account = message.getUuid().flatMap(accountsManager::get);
|
||||
Optional<Account> account = message.getUuid().flatMap(accountsManager::getByAccountIdentifier);
|
||||
|
||||
if (account.isPresent()) {
|
||||
Optional<Device> device = account.get().getDevice(message.getDeviceId());
|
||||
|
||||
@@ -124,22 +124,24 @@ public class MessageSender implements Managed {
|
||||
|
||||
gcmSender.sendMessage(gcmMessage);
|
||||
|
||||
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId()));
|
||||
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId(), false));
|
||||
}
|
||||
|
||||
private void sendApnNotification(Account account, Device device) {
|
||||
ApnMessage apnMessage;
|
||||
|
||||
if (!Util.isEmpty(device.getVoipApnId())) {
|
||||
apnMessage = new ApnMessage(device.getVoipApnId(), account.getUuid(), device.getId(), true, Type.NOTIFICATION, Optional.empty());
|
||||
final boolean useVoip = !Util.isEmpty(device.getVoipApnId());
|
||||
|
||||
if (useVoip) {
|
||||
apnMessage = new ApnMessage(device.getVoipApnId(), account.getUuid(), device.getId(), useVoip, Type.NOTIFICATION, Optional.empty());
|
||||
RedisOperation.unchecked(() -> apnFallbackManager.schedule(account, device));
|
||||
} else {
|
||||
apnMessage = new ApnMessage(device.getApnId(), account.getUuid(), device.getId(), false, Type.NOTIFICATION, Optional.empty());
|
||||
apnMessage = new ApnMessage(device.getApnId(), account.getUuid(), device.getId(), useVoip, Type.NOTIFICATION, Optional.empty());
|
||||
}
|
||||
|
||||
apnSender.sendMessage(apnMessage);
|
||||
|
||||
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId()));
|
||||
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId(), useVoip));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -33,7 +33,7 @@ public class ReceiptSender {
|
||||
return;
|
||||
}
|
||||
|
||||
final Account destinationAccount = accountManager.get(destinationUuid)
|
||||
final Account destinationAccount = accountManager.getByAccountIdentifier(destinationUuid)
|
||||
.orElseThrow(() -> new NoSuchUserException(destinationUuid));
|
||||
|
||||
final Envelope.Builder message = Envelope.newBuilder()
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ public class LegacyRecaptchaClient implements RecaptchaClient {
|
||||
.post(Entity.form(formData), VerifyResponse.class);
|
||||
|
||||
if (response.success) {
|
||||
logger.info("Got successful captcha time: " + response.challenge_ts + ", current time: " + System.currentTimeMillis());
|
||||
logger.debug("Got successful captcha time: " + response.challenge_ts + ", current time: " + System.currentTimeMillis());
|
||||
}
|
||||
|
||||
return response.success;
|
||||
|
||||
@@ -32,6 +32,9 @@ public class Account {
|
||||
@JsonIgnore
|
||||
private UUID uuid;
|
||||
|
||||
@JsonProperty("pni")
|
||||
private UUID phoneNumberIdentifier;
|
||||
|
||||
@JsonProperty
|
||||
private String number;
|
||||
|
||||
@@ -41,6 +44,9 @@ public class Account {
|
||||
@JsonProperty
|
||||
private String identityKey;
|
||||
|
||||
@JsonProperty("pniIdentityKey")
|
||||
private String phoneNumberIdentityKey;
|
||||
|
||||
@JsonProperty("cpv")
|
||||
private String currentProfileVersion;
|
||||
|
||||
@@ -80,9 +86,10 @@ public class Account {
|
||||
public Account() {}
|
||||
|
||||
@VisibleForTesting
|
||||
public Account(String number, UUID uuid, Set<Device> devices, byte[] unidentifiedAccessKey) {
|
||||
public Account(String number, UUID uuid, final UUID phoneNumberIdentifier, Set<Device> devices, byte[] unidentifiedAccessKey) {
|
||||
this.number = number;
|
||||
this.uuid = uuid;
|
||||
this.phoneNumberIdentifier = phoneNumberIdentifier;
|
||||
this.devices = devices;
|
||||
this.unidentifiedAccessKey = unidentifiedAccessKey;
|
||||
}
|
||||
@@ -98,16 +105,20 @@ public class Account {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public void setNumber(String number) {
|
||||
public UUID getPhoneNumberIdentifier() {
|
||||
requireNotStale();
|
||||
|
||||
this.number = number;
|
||||
return phoneNumberIdentifier;
|
||||
}
|
||||
|
||||
public void setCanonicallyDiscoverable(boolean canonicallyDiscoverable) {
|
||||
requireNotStale();
|
||||
|
||||
this.canonicallyDiscoverable = canonicallyDiscoverable;
|
||||
/**
|
||||
* Tests whether this account's account identifier or phone number identifier matches the given UUID.
|
||||
*
|
||||
* @param identifier the identifier to test
|
||||
* @return {@code true} if this account's identifier or phone number identifier matches
|
||||
*/
|
||||
public boolean isIdentifiedBy(final UUID identifier) {
|
||||
return uuid.equals(identifier) || (phoneNumberIdentifier != null && phoneNumberIdentifier.equals(identifier));
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
@@ -116,6 +127,13 @@ public class Account {
|
||||
return number;
|
||||
}
|
||||
|
||||
public void setNumber(String number, UUID phoneNumberIdentifier) {
|
||||
requireNotStale();
|
||||
|
||||
this.number = number;
|
||||
this.phoneNumberIdentifier = phoneNumberIdentifier;
|
||||
}
|
||||
|
||||
public void addDevice(Device device) {
|
||||
requireNotStale();
|
||||
|
||||
@@ -247,6 +265,12 @@ public class Account {
|
||||
return canonicallyDiscoverable;
|
||||
}
|
||||
|
||||
public void setCanonicallyDiscoverable(boolean canonicallyDiscoverable) {
|
||||
requireNotStale();
|
||||
|
||||
this.canonicallyDiscoverable = canonicallyDiscoverable;
|
||||
}
|
||||
|
||||
public Optional<String> getRelay() {
|
||||
requireNotStale();
|
||||
|
||||
@@ -265,6 +289,14 @@ public class Account {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public String getPhoneNumberIdentityKey() {
|
||||
return phoneNumberIdentityKey;
|
||||
}
|
||||
|
||||
public void setPhoneNumberIdentityKey(final String phoneNumberIdentityKey) {
|
||||
this.phoneNumberIdentityKey = phoneNumberIdentityKey;
|
||||
}
|
||||
|
||||
public long getLastSeen() {
|
||||
requireNotStale();
|
||||
|
||||
@@ -481,5 +513,4 @@ public class Account {
|
||||
logger.error("Accessor called on stale account", new RuntimeException());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+24
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
@@ -16,6 +16,9 @@ import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class AccountDatabaseCrawlerCache {
|
||||
|
||||
public static final String GENERAL_PURPOSE_PREFIX = "";
|
||||
public static final String DIRECTORY_RECONCILER_PREFIX = "directory-reconciler";
|
||||
|
||||
private static final String ACTIVE_WORKER_KEY = "account_database_crawler_cache_active_worker";
|
||||
private static final String LAST_UUID_KEY = "account_database_crawler_cache_last_uuid";
|
||||
private static final String ACCELERATE_KEY = "account_database_crawler_cache_accelerate";
|
||||
@@ -27,17 +30,21 @@ public class AccountDatabaseCrawlerCache {
|
||||
private final FaultTolerantRedisCluster cacheCluster;
|
||||
private final ClusterLuaScript unlockClusterScript;
|
||||
|
||||
public AccountDatabaseCrawlerCache(FaultTolerantRedisCluster cacheCluster) throws IOException {
|
||||
private final String prefix;
|
||||
|
||||
public AccountDatabaseCrawlerCache(FaultTolerantRedisCluster cacheCluster, String prefix) throws IOException {
|
||||
this.cacheCluster = cacheCluster;
|
||||
this.unlockClusterScript = ClusterLuaScript.fromResource(cacheCluster, "lua/account_database_crawler/unlock.lua",
|
||||
ScriptOutputType.INTEGER);
|
||||
|
||||
this.prefix = prefix + "::";
|
||||
}
|
||||
|
||||
public void setAccelerated(final boolean accelerated) {
|
||||
if (accelerated) {
|
||||
cacheCluster.useCluster(connection -> connection.sync().set(ACCELERATE_KEY, "1"));
|
||||
cacheCluster.useCluster(connection -> connection.sync().set(getPrefixedKey(ACCELERATE_KEY), "1"));
|
||||
} else {
|
||||
cacheCluster.useCluster(connection -> connection.sync().del(ACCELERATE_KEY));
|
||||
cacheCluster.useCluster(connection -> connection.sync().del(getPrefixedKey(ACCELERATE_KEY)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,16 +54,16 @@ public class AccountDatabaseCrawlerCache {
|
||||
|
||||
public boolean claimActiveWork(String workerId, long ttlMs) {
|
||||
return "OK".equals(cacheCluster.withCluster(connection -> connection.sync()
|
||||
.set(ACTIVE_WORKER_KEY, workerId, SetArgs.Builder.nx().px(ttlMs))));
|
||||
.set(getPrefixedKey(ACTIVE_WORKER_KEY), workerId, SetArgs.Builder.nx().px(ttlMs))));
|
||||
}
|
||||
|
||||
public void releaseActiveWork(String workerId) {
|
||||
unlockClusterScript.execute(List.of(ACTIVE_WORKER_KEY), List.of(workerId));
|
||||
unlockClusterScript.execute(List.of(getPrefixedKey(ACTIVE_WORKER_KEY)), List.of(workerId));
|
||||
}
|
||||
|
||||
public Optional<UUID> getLastUuid() {
|
||||
final String lastUuidString = cacheCluster.withCluster(
|
||||
connection -> connection.sync().get(LAST_UUID_KEY));
|
||||
connection -> connection.sync().get(getPrefixedKey(LAST_UUID_KEY)));
|
||||
|
||||
if (lastUuidString == null) {
|
||||
return Optional.empty();
|
||||
@@ -68,15 +75,15 @@ public class AccountDatabaseCrawlerCache {
|
||||
public void setLastUuid(Optional<UUID> lastUuid) {
|
||||
if (lastUuid.isPresent()) {
|
||||
cacheCluster.useCluster(connection -> connection.sync()
|
||||
.psetex(LAST_UUID_KEY, LAST_NUMBER_TTL_MS, lastUuid.get().toString()));
|
||||
.psetex(getPrefixedKey(LAST_UUID_KEY), LAST_NUMBER_TTL_MS, lastUuid.get().toString()));
|
||||
} else {
|
||||
cacheCluster.useCluster(connection -> connection.sync().del(LAST_UUID_KEY));
|
||||
cacheCluster.useCluster(connection -> connection.sync().del(getPrefixedKey(LAST_UUID_KEY)));
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<UUID> getLastUuidDynamo() {
|
||||
final String lastUuidString = cacheCluster.withCluster(
|
||||
connection -> connection.sync().get(LAST_UUID_DYNAMO_KEY));
|
||||
connection -> connection.sync().get(getPrefixedKey(LAST_UUID_DYNAMO_KEY)));
|
||||
|
||||
if (lastUuidString == null) {
|
||||
return Optional.empty();
|
||||
@@ -89,9 +96,14 @@ public class AccountDatabaseCrawlerCache {
|
||||
if (lastUuid.isPresent()) {
|
||||
cacheCluster.useCluster(
|
||||
connection -> connection.sync()
|
||||
.psetex(LAST_UUID_DYNAMO_KEY, LAST_NUMBER_TTL_MS, lastUuid.get().toString()));
|
||||
.psetex(getPrefixedKey(LAST_UUID_DYNAMO_KEY), LAST_NUMBER_TTL_MS, lastUuid.get().toString()));
|
||||
} else {
|
||||
cacheCluster.useCluster(connection -> connection.sync().del(LAST_UUID_DYNAMO_KEY));
|
||||
cacheCluster.useCluster(connection -> connection.sync().del(getPrefixedKey(LAST_UUID_DYNAMO_KEY)));
|
||||
}
|
||||
}
|
||||
|
||||
private String getPrefixedKey(final String key) {
|
||||
return prefix + key;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import io.micrometer.core.instrument.Timer;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -29,7 +30,6 @@ import software.amazon.awssdk.services.dynamodb.model.Delete;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
|
||||
import software.amazon.awssdk.services.dynamodb.model.Put;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
|
||||
@@ -44,6 +44,8 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
|
||||
// uuid, primary key
|
||||
static final String KEY_ACCOUNT_UUID = "U";
|
||||
// uuid, attribute on account table, primary key for PNI table
|
||||
static final String ATTR_PNI_UUID = "PNI";
|
||||
// phone number
|
||||
static final String ATTR_ACCOUNT_E164 = "P";
|
||||
// account, serialized to JSON
|
||||
@@ -55,7 +57,8 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
|
||||
private final DynamoDbClient client;
|
||||
|
||||
private final String phoneNumbersTableName;
|
||||
private final String phoneNumberConstraintTableName;
|
||||
private final String phoneNumberIdentifierConstraintTableName;
|
||||
private final String accountsTableName;
|
||||
|
||||
private final int scanPageSize;
|
||||
@@ -64,19 +67,20 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
private static final Timer CHANGE_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "changeNumber"));
|
||||
private static final Timer UPDATE_TIMER = Metrics.timer(name(Accounts.class, "update"));
|
||||
private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "getByNumber"));
|
||||
private static final Timer GET_BY_PNI_TIMER = Metrics.timer(name(Accounts.class, "getByPni"));
|
||||
private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(Accounts.class, "getByUuid"));
|
||||
private static final Timer GET_ALL_FROM_START_TIMER = Metrics.timer(name(Accounts.class, "getAllFrom"));
|
||||
private static final Timer GET_ALL_FROM_OFFSET_TIMER = Metrics.timer(name(Accounts.class, "getAllFromOffset"));
|
||||
private static final Timer DELETE_TIMER = Metrics.timer(name(Accounts.class, "delete"));
|
||||
|
||||
|
||||
public Accounts(DynamoDbClient client, String accountsTableName, String phoneNumbersTableName,
|
||||
final int scanPageSize) {
|
||||
public Accounts(DynamoDbClient client, String accountsTableName, String phoneNumberConstraintTableName,
|
||||
String phoneNumberIdentifierConstraintTableName, final int scanPageSize) {
|
||||
|
||||
super(client);
|
||||
|
||||
this.client = client;
|
||||
this.phoneNumbersTableName = phoneNumbersTableName;
|
||||
this.phoneNumberConstraintTableName = phoneNumberConstraintTableName;
|
||||
this.phoneNumberIdentifierConstraintTableName = phoneNumberIdentifierConstraintTableName;
|
||||
this.accountsTableName = accountsTableName;
|
||||
this.scanPageSize = scanPageSize;
|
||||
}
|
||||
@@ -85,35 +89,91 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
return CREATE_TIMER.record(() -> {
|
||||
|
||||
try {
|
||||
TransactWriteItem phoneNumberConstraintPut = buildPutWriteItemForPhoneNumberConstraint(account, account.getUuid());
|
||||
TransactWriteItem accountPut = buildPutWriteItemForAccount(account, account.getUuid(), Put.builder()
|
||||
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
|
||||
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber()))));
|
||||
TransactWriteItem phoneNumberConstraintPut = TransactWriteItem.builder()
|
||||
.put(
|
||||
Put.builder()
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.item(Map.of(
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.conditionExpression(
|
||||
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
|
||||
.expressionAttributeNames(
|
||||
Map.of("#uuid", KEY_ACCOUNT_UUID,
|
||||
"#number", ATTR_ACCOUNT_E164))
|
||||
.expressionAttributeValues(
|
||||
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
TransactWriteItem phoneNumberIdentifierConstraintPut = TransactWriteItem.builder()
|
||||
.put(
|
||||
Put.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.item(Map.of(
|
||||
ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier()),
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.conditionExpression(
|
||||
"attribute_not_exists(#pni) OR (attribute_exists(#pni) AND #uuid = :uuid)")
|
||||
.expressionAttributeNames(
|
||||
Map.of("#uuid", KEY_ACCOUNT_UUID,
|
||||
"#pni", ATTR_PNI_UUID))
|
||||
.expressionAttributeValues(
|
||||
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
final Map<String, AttributeValue> item = new HashMap<>(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
|
||||
ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier()),
|
||||
ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
|
||||
ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())));
|
||||
|
||||
TransactWriteItem accountPut = TransactWriteItem.builder()
|
||||
.put(Put.builder()
|
||||
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
|
||||
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber())))
|
||||
.tableName(accountsTableName)
|
||||
.item(item)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
|
||||
.transactItems(phoneNumberConstraintPut, accountPut)
|
||||
.transactItems(phoneNumberConstraintPut, phoneNumberIdentifierConstraintPut, accountPut)
|
||||
.build();
|
||||
|
||||
try {
|
||||
client.transactWriteItems(request);
|
||||
} catch (TransactionCanceledException e) {
|
||||
|
||||
final CancellationReason accountCancellationReason = e.cancellationReasons().get(1);
|
||||
final CancellationReason accountCancellationReason = e.cancellationReasons().get(2);
|
||||
|
||||
if ("ConditionalCheckFailed".equals(accountCancellationReason.code())) {
|
||||
throw new IllegalArgumentException("uuid present with different phone number");
|
||||
throw new IllegalArgumentException("account identifier present with different phone number");
|
||||
}
|
||||
|
||||
final CancellationReason phoneNumberConstraintCancellationReason = e.cancellationReasons().get(0);
|
||||
final CancellationReason phoneNumberIdentifierConstraintCancellationReason = e.cancellationReasons().get(1);
|
||||
|
||||
if ("ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code())) {
|
||||
if ("ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code()) ||
|
||||
"ConditionalCheckFailed".equals(phoneNumberIdentifierConstraintCancellationReason.code())) {
|
||||
|
||||
ByteBuffer actualAccountUuid = phoneNumberConstraintCancellationReason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer();
|
||||
// In theory, both reasons should trip in tandem and either should give us the information we need. Even so,
|
||||
// we'll be cautious here and make sure we're choosing a condition check that really failed.
|
||||
final CancellationReason reason = "ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code()) ?
|
||||
phoneNumberConstraintCancellationReason : phoneNumberIdentifierConstraintCancellationReason;
|
||||
|
||||
ByteBuffer actualAccountUuid = reason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer();
|
||||
account.setUuid(UUIDUtil.fromByteBuffer(actualAccountUuid));
|
||||
|
||||
final int version = get(account.getUuid()).get().getVersion();
|
||||
account.setVersion(version);
|
||||
final Account existingAccount = getByAccountIdentifier(account.getUuid()).orElseThrow();
|
||||
account.setNumber(existingAccount.getNumber(), existingAccount.getPhoneNumberIdentifier());
|
||||
account.setVersion(existingAccount.getVersion());
|
||||
|
||||
update(account);
|
||||
|
||||
@@ -125,7 +185,7 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
throw new ContestedOptimisticLockException();
|
||||
}
|
||||
|
||||
// this shouldn’t happen
|
||||
// this shouldn't happen
|
||||
throw new RuntimeException("could not create account: " + extractCancellationReasonCodes(e));
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
@@ -136,44 +196,11 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
});
|
||||
}
|
||||
|
||||
private TransactWriteItem buildPutWriteItemForAccount(Account account, UUID uuid, Put.Builder putBuilder) throws JsonProcessingException {
|
||||
return TransactWriteItem.builder()
|
||||
.put(putBuilder
|
||||
.tableName(accountsTableName)
|
||||
.item(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid),
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
|
||||
ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
|
||||
ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private TransactWriteItem buildPutWriteItemForPhoneNumberConstraint(Account account, UUID uuid) {
|
||||
return TransactWriteItem.builder()
|
||||
.put(
|
||||
Put.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.item(Map.of(
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid)))
|
||||
.conditionExpression(
|
||||
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
|
||||
.expressionAttributeNames(
|
||||
Map.of("#uuid", KEY_ACCOUNT_UUID,
|
||||
"#number", ATTR_ACCOUNT_E164))
|
||||
.expressionAttributeValues(
|
||||
Map.of(":uuid", AttributeValues.fromUUID(uuid)))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the phone number for the given account. The given account's number should be its current, pre-change
|
||||
* number. If this method succeeds, the account's number will be changed to the new number. If the update fails for
|
||||
* any reason, the account's number will be unchanged.
|
||||
* number. If this method succeeds, the account's number will be changed to the new number and its phone number
|
||||
* identifier will be changed to the given phone number identifier. If the update fails for any reason, the account's
|
||||
* number and PNI will be unchanged.
|
||||
* <p/>
|
||||
* This method expects that any accounts with conflicting numbers will have been removed by the time this method is
|
||||
* called. This method may fail with an unspecified {@link RuntimeException} if another account with the same number
|
||||
@@ -182,26 +209,28 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
* @param account the account for which to change the phone number
|
||||
* @param number the new phone number
|
||||
*/
|
||||
public void changeNumber(final Account account, final String number) {
|
||||
public void changeNumber(final Account account, final String number, final UUID phoneNumberIdentifier) {
|
||||
CHANGE_NUMBER_TIMER.record(() -> {
|
||||
final String originalNumber = account.getNumber();
|
||||
final UUID originalPni = account.getPhoneNumberIdentifier();
|
||||
|
||||
boolean succeeded = false;
|
||||
|
||||
account.setNumber(number);
|
||||
account.setNumber(number, phoneNumberIdentifier);
|
||||
|
||||
try {
|
||||
final List<TransactWriteItem> writeItems = new ArrayList<>();
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(originalNumber)))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.put(Put.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.item(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
|
||||
@@ -211,20 +240,41 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
.build())
|
||||
.build());
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(originalPni)))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.put(Put.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.item(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
|
||||
ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
|
||||
.conditionExpression("attribute_not_exists(#pni)")
|
||||
.expressionAttributeNames(Map.of("#pni", ATTR_PNI_UUID))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build());
|
||||
|
||||
writeItems.add(
|
||||
TransactWriteItem.builder()
|
||||
.update(Update.builder()
|
||||
.tableName(accountsTableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.updateExpression("SET #data = :data, #number = :number, #cds = :cds ADD #version :version_increment")
|
||||
.updateExpression("SET #data = :data, #number = :number, #pni = :pni, #cds = :cds ADD #version :version_increment")
|
||||
.conditionExpression("attribute_exists(#number) AND #version = :version")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
|
||||
"#data", ATTR_ACCOUNT_DATA,
|
||||
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
|
||||
"#pni", ATTR_PNI_UUID,
|
||||
"#version", ATTR_VERSION))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
":number", AttributeValues.fromString(number),
|
||||
":pni", AttributeValues.fromUUID(phoneNumberIdentifier),
|
||||
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
|
||||
":version", AttributeValues.fromInt(account.getVersion()),
|
||||
":version_increment", AttributeValues.fromInt(1)))
|
||||
@@ -243,7 +293,7 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
throw new IllegalArgumentException(e);
|
||||
} finally {
|
||||
if (!succeeded) {
|
||||
account.setNumber(originalNumber);
|
||||
account.setNumber(originalNumber, originalPni);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -251,57 +301,69 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
|
||||
public void update(Account account) throws ContestedOptimisticLockException {
|
||||
UPDATE_TIMER.record(() -> {
|
||||
UpdateItemRequest updateItemRequest;
|
||||
final UpdateItemRequest updateItemRequest;
|
||||
|
||||
try {
|
||||
updateItemRequest = UpdateItemRequest.builder()
|
||||
.tableName(accountsTableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.updateExpression("SET #data = :data, #cds = :cds ADD #version :version_increment")
|
||||
.conditionExpression("attribute_exists(#number) AND #version = :version")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
|
||||
"#data", ATTR_ACCOUNT_DATA,
|
||||
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
|
||||
"#version", ATTR_VERSION))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
|
||||
":version", AttributeValues.fromInt(account.getVersion()),
|
||||
":version_increment", AttributeValues.fromInt(1)))
|
||||
.returnValues(ReturnValue.UPDATED_NEW)
|
||||
.build();
|
||||
|
||||
.tableName(accountsTableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.updateExpression("SET #data = :data, #cds = :cds ADD #version :version_increment")
|
||||
.conditionExpression("attribute_exists(#number) AND #version = :version")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
|
||||
"#data", ATTR_ACCOUNT_DATA,
|
||||
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
|
||||
"#version", ATTR_VERSION))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
|
||||
":version", AttributeValues.fromInt(account.getVersion()),
|
||||
":version_increment", AttributeValues.fromInt(1)))
|
||||
.build();
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
UpdateItemResponse response = client.updateItem(updateItemRequest);
|
||||
|
||||
final UpdateItemResponse response = client.updateItem(updateItemRequest);
|
||||
account.setVersion(AttributeValues.getInt(response.attributes(), "V", account.getVersion() + 1));
|
||||
} catch (final TransactionConflictException e) {
|
||||
|
||||
throw new ContestedOptimisticLockException();
|
||||
|
||||
} catch (final ConditionalCheckFailedException e) {
|
||||
|
||||
// the exception doesn’t give details about which condition failed,
|
||||
// the exception doesn't give details about which condition failed,
|
||||
// but we can infer it was an optimistic locking failure if the UUID is known
|
||||
throw get(account.getUuid()).isPresent() ? new ContestedOptimisticLockException() : e;
|
||||
throw getByAccountIdentifier(account.getUuid()).isPresent() ? new ContestedOptimisticLockException() : e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Account> get(String number) {
|
||||
public Optional<Account> getByE164(String number) {
|
||||
return GET_BY_NUMBER_TIMER.record(() -> {
|
||||
|
||||
final GetItemResponse response = client.getItem(GetItemRequest.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
|
||||
.build());
|
||||
|
||||
return Optional.ofNullable(response.item())
|
||||
.map(item -> item.get(KEY_ACCOUNT_UUID))
|
||||
.map(uuid -> accountByUuid(uuid))
|
||||
.map(this::accountByUuid)
|
||||
.map(Accounts::fromItem);
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Account> getByPhoneNumberIdentifier(final UUID phoneNumberIdentifier) {
|
||||
return GET_BY_PNI_TIMER.record(() -> {
|
||||
|
||||
final GetItemResponse response = client.getItem(GetItemRequest.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
|
||||
.build());
|
||||
|
||||
return Optional.ofNullable(response.item())
|
||||
.map(item -> item.get(KEY_ACCOUNT_UUID))
|
||||
.map(this::accountByUuid)
|
||||
.map(Accounts::fromItem);
|
||||
});
|
||||
}
|
||||
@@ -315,7 +377,7 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
return r.item().isEmpty() ? null : r.item();
|
||||
}
|
||||
|
||||
public Optional<Account> get(UUID uuid) {
|
||||
public Optional<Account> getByAccountIdentifier(UUID uuid) {
|
||||
return GET_BY_UUID_TIMER.record(() ->
|
||||
Optional.ofNullable(accountByUuid(AttributeValues.fromUUID(uuid)))
|
||||
.map(Accounts::fromItem));
|
||||
@@ -324,13 +386,11 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
public void delete(UUID uuid) {
|
||||
DELETE_TIMER.record(() -> {
|
||||
|
||||
Optional<Account> maybeAccount = get(uuid);
|
||||
|
||||
maybeAccount.ifPresent(account -> {
|
||||
getByAccountIdentifier(uuid).ifPresent(account -> {
|
||||
|
||||
TransactWriteItem phoneNumberDelete = TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber())))
|
||||
.build())
|
||||
.build();
|
||||
@@ -342,8 +402,17 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
.build())
|
||||
.build();
|
||||
|
||||
final List<TransactWriteItem> transactWriteItems = new ArrayList<>(List.of(phoneNumberDelete, accountDelete));
|
||||
|
||||
transactWriteItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier())))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
|
||||
.transactItems(phoneNumberDelete, accountDelete).build();
|
||||
.transactItems(transactWriteItems).build();
|
||||
|
||||
client.transactWriteItems(request);
|
||||
});
|
||||
@@ -393,7 +462,7 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
}
|
||||
try {
|
||||
Account account = SystemMapper.getMapper().readValue(item.get(ATTR_ACCOUNT_DATA).b().asByteArray(), Account.class);
|
||||
account.setNumber(item.get(ATTR_ACCOUNT_E164).s());
|
||||
account.setNumber(item.get(ATTR_ACCOUNT_E164).s(), AttributeValues.getUUID(item, ATTR_PNI_UUID, null));
|
||||
account.setUuid(UUIDUtil.fromByteBuffer(item.get(KEY_ACCOUNT_UUID).b().asByteBuffer()));
|
||||
account.setVersion(Integer.parseInt(item.get(ATTR_VERSION).n()));
|
||||
account.setCanonicallyDiscoverable(Optional.ofNullable(item.get(ATTR_CANONICALLY_DISCOVERABLE)).map(av -> av.bool()).orElse(false));
|
||||
|
||||
+89
-60
@@ -17,6 +17,9 @@ import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import java.io.IOException;
|
||||
import java.time.Clock;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -49,6 +52,7 @@ public class AccountsManager {
|
||||
|
||||
private static final Timer redisSetTimer = metricRegistry.timer(name(AccountsManager.class, "redisSet" ));
|
||||
private static final Timer redisNumberGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisNumberGet"));
|
||||
private static final Timer redisPniGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisPniGet"));
|
||||
private static final Timer redisUuidGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUuidGet" ));
|
||||
private static final Timer redisDeleteTimer = metricRegistry.timer(name(AccountsManager.class, "redisDelete" ));
|
||||
|
||||
@@ -60,18 +64,20 @@ public class AccountsManager {
|
||||
private final Logger logger = LoggerFactory.getLogger(AccountsManager.class);
|
||||
|
||||
private final Accounts accounts;
|
||||
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
|
||||
private final FaultTolerantRedisCluster cacheCluster;
|
||||
private final DeletedAccountsManager deletedAccountsManager;
|
||||
private final DirectoryQueue directoryQueue;
|
||||
private final KeysDynamoDb keysDynamoDb;
|
||||
private final DirectoryQueue directoryQueue;
|
||||
private final Keys keys;
|
||||
private final MessagesManager messagesManager;
|
||||
private final UsernamesManager usernamesManager;
|
||||
private final ProfilesManager profilesManager;
|
||||
private final ProfilesManager profilesManager;
|
||||
private final StoredVerificationCodeManager pendingAccounts;
|
||||
private final SecureStorageClient secureStorageClient;
|
||||
private final SecureBackupClient secureBackupClient;
|
||||
private final SecureStorageClient secureStorageClient;
|
||||
private final SecureBackupClient secureBackupClient;
|
||||
private final ClientPresenceManager clientPresenceManager;
|
||||
private final ObjectMapper mapper;
|
||||
private final ObjectMapper mapper;
|
||||
private final Clock clock;
|
||||
|
||||
public enum DeletionReason {
|
||||
ADMIN_DELETED("admin"),
|
||||
@@ -85,21 +91,26 @@ public class AccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
public AccountsManager(Accounts accounts, FaultTolerantRedisCluster cacheCluster,
|
||||
public AccountsManager(final Accounts accounts,
|
||||
final PhoneNumberIdentifiers phoneNumberIdentifiers,
|
||||
final FaultTolerantRedisCluster cacheCluster,
|
||||
final DeletedAccountsManager deletedAccountsManager,
|
||||
final DirectoryQueue directoryQueue,
|
||||
final KeysDynamoDb keysDynamoDb, final MessagesManager messagesManager,
|
||||
final Keys keys,
|
||||
final MessagesManager messagesManager,
|
||||
final UsernamesManager usernamesManager,
|
||||
final ProfilesManager profilesManager,
|
||||
final StoredVerificationCodeManager pendingAccounts,
|
||||
final SecureStorageClient secureStorageClient,
|
||||
final SecureBackupClient secureBackupClient,
|
||||
final ClientPresenceManager clientPresenceManager) {
|
||||
final ClientPresenceManager clientPresenceManager,
|
||||
final Clock clock) {
|
||||
this.accounts = accounts;
|
||||
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
|
||||
this.cacheCluster = cacheCluster;
|
||||
this.deletedAccountsManager = deletedAccountsManager;
|
||||
this.directoryQueue = directoryQueue;
|
||||
this.keysDynamoDb = keysDynamoDb;
|
||||
this.keys = keys;
|
||||
this.messagesManager = messagesManager;
|
||||
this.usernamesManager = usernamesManager;
|
||||
this.profilesManager = profilesManager;
|
||||
@@ -108,12 +119,14 @@ public class AccountsManager {
|
||||
this.secureBackupClient = secureBackupClient;
|
||||
this.clientPresenceManager = clientPresenceManager;
|
||||
this.mapper = SystemMapper.getMapper();
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
}
|
||||
|
||||
public Account create(final String number,
|
||||
final String password,
|
||||
final String signalAgent,
|
||||
final AccountAttributes accountAttributes) throws InterruptedException {
|
||||
final AccountAttributes accountAttributes,
|
||||
final List<AccountBadge> accountBadges) throws InterruptedException {
|
||||
|
||||
try (Timer.Context ignored = createTimer.time()) {
|
||||
final Account account = new Account();
|
||||
@@ -130,17 +143,18 @@ public class AccountsManager {
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setUserAgent(signalAgent);
|
||||
|
||||
account.setNumber(number);
|
||||
account.setNumber(number, phoneNumberIdentifiers.getPhoneNumberIdentifier(number));
|
||||
account.setUuid(maybeRecentlyDeletedUuid.orElseGet(UUID::randomUUID));
|
||||
account.addDevice(device);
|
||||
account.setRegistrationLockFromAttributes(accountAttributes);
|
||||
account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
|
||||
account.setUnrestrictedUnidentifiedAccess(accountAttributes.isUnrestrictedUnidentifiedAccess());
|
||||
account.setDiscoverableByPhoneNumber(accountAttributes.isDiscoverableByPhoneNumber());
|
||||
account.setBadges(clock, accountBadges);
|
||||
|
||||
final UUID originalUuid = account.getUuid();
|
||||
|
||||
boolean freshUser = dynamoCreate(account);
|
||||
boolean freshUser = accounts.create(account);
|
||||
|
||||
// create() sometimes updates the UUID, if there was a number conflict.
|
||||
// for metrics, we want secondary to run with the same original UUID
|
||||
@@ -164,7 +178,7 @@ public class AccountsManager {
|
||||
// account and need to clear out messages and keys that may have been stored for the old account.
|
||||
if (!originalUuid.equals(actualUuid)) {
|
||||
messagesManager.clear(actualUuid);
|
||||
keysDynamoDb.delete(actualUuid);
|
||||
keys.delete(actualUuid);
|
||||
profilesManager.deleteAll(actualUuid);
|
||||
}
|
||||
|
||||
@@ -202,7 +216,7 @@ public class AccountsManager {
|
||||
deletedAccountsManager.lockAndPut(account.getNumber(), number, () -> {
|
||||
redisDelete(account);
|
||||
|
||||
final Optional<Account> maybeExistingAccount = get(number);
|
||||
final Optional<Account> maybeExistingAccount = getByE164(number);
|
||||
final Optional<UUID> displacedUuid;
|
||||
|
||||
if (maybeExistingAccount.isPresent()) {
|
||||
@@ -213,12 +227,13 @@ public class AccountsManager {
|
||||
}
|
||||
|
||||
final UUID uuid = account.getUuid();
|
||||
final UUID phoneNumberIdentifier = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
|
||||
|
||||
final Account numberChangedAccount = updateWithRetries(
|
||||
account,
|
||||
a -> true,
|
||||
a -> dynamoChangeNumber(a, number),
|
||||
() -> dynamoGet(uuid).orElseThrow());
|
||||
a -> accounts.changeNumber(a, number, phoneNumberIdentifier),
|
||||
() -> accounts.getByAccountIdentifier(uuid).orElseThrow());
|
||||
|
||||
updatedAccount.set(numberChangedAccount);
|
||||
directoryQueue.changePhoneNumber(numberChangedAccount, originalNumber, number);
|
||||
@@ -277,8 +292,12 @@ public class AccountsManager {
|
||||
|
||||
final UUID uuid = account.getUuid();
|
||||
final String originalNumber = account.getNumber();
|
||||
final UUID originalPhoneNumberIdentifier = account.getPhoneNumberIdentifier();
|
||||
|
||||
updatedAccount = updateWithRetries(account, updater, this::dynamoUpdate, () -> dynamoGet(uuid).get());
|
||||
updatedAccount = updateWithRetries(account,
|
||||
updater,
|
||||
accounts::update,
|
||||
() -> accounts.getByAccountIdentifier(uuid).orElseThrow());
|
||||
|
||||
assert updatedAccount.getNumber().equals(originalNumber);
|
||||
|
||||
@@ -287,6 +306,13 @@ public class AccountsManager {
|
||||
new RuntimeException());
|
||||
}
|
||||
|
||||
assert updatedAccount.getPhoneNumberIdentifier().equals(originalPhoneNumberIdentifier);
|
||||
|
||||
if (!updatedAccount.getPhoneNumberIdentifier().equals(originalPhoneNumberIdentifier)) {
|
||||
logger.error("Phone number identifier changed via \"normal\" update; PNIs must be changed via changeNumber method",
|
||||
new RuntimeException());
|
||||
}
|
||||
|
||||
redisSet(updatedAccount);
|
||||
}
|
||||
|
||||
@@ -347,12 +373,12 @@ public class AccountsManager {
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Account> get(String number) {
|
||||
public Optional<Account> getByE164(String number) {
|
||||
try (Timer.Context ignored = getByNumberTimer.time()) {
|
||||
Optional<Account> account = redisGet(number);
|
||||
Optional<Account> account = redisGetByE164(number);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
account = dynamoGet(number);
|
||||
account = accounts.getByE164(number);
|
||||
account.ifPresent(this::redisSet);
|
||||
}
|
||||
|
||||
@@ -360,12 +386,25 @@ public class AccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Account> get(UUID uuid) {
|
||||
try (Timer.Context ignored = getByUuidTimer.time()) {
|
||||
Optional<Account> account = redisGet(uuid);
|
||||
public Optional<Account> getByPhoneNumberIdentifier(UUID pni) {
|
||||
try (Timer.Context ignored = getByNumberTimer.time()) {
|
||||
Optional<Account> account = redisGetByPhoneNumberIdentifier(pni);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
account = dynamoGet(uuid);
|
||||
account = accounts.getByPhoneNumberIdentifier(pni);
|
||||
account.ifPresent(this::redisSet);
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Account> getByAccountIdentifier(UUID uuid) {
|
||||
try (Timer.Context ignored = getByUuidTimer.time()) {
|
||||
Optional<Account> account = redisGetByAccountIdentifier(uuid);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
account = accounts.getByAccountIdentifier(uuid);
|
||||
account.ifPresent(this::redisSet);
|
||||
}
|
||||
|
||||
@@ -406,22 +445,24 @@ public class AccountsManager {
|
||||
|
||||
usernamesManager.delete(account.getUuid());
|
||||
profilesManager.deleteAll(account.getUuid());
|
||||
keysDynamoDb.delete(account.getUuid());
|
||||
keys.delete(account.getUuid());
|
||||
keys.delete(account.getPhoneNumberIdentifier());
|
||||
messagesManager.clear(account.getUuid());
|
||||
messagesManager.clear(account.getPhoneNumberIdentifier());
|
||||
|
||||
deleteStorageServiceDataFuture.join();
|
||||
deleteBackupServiceDataFuture.join();
|
||||
|
||||
redisDelete(account);
|
||||
dynamoDelete(account);
|
||||
accounts.delete(account.getUuid());
|
||||
|
||||
RedisOperation.unchecked(() ->
|
||||
account.getDevices().forEach(device ->
|
||||
clientPresenceManager.displacePresence(account.getUuid(), device.getId())));
|
||||
}
|
||||
|
||||
private String getAccountMapKey(String number) {
|
||||
return "AccountMap::" + number;
|
||||
private String getAccountMapKey(String key) {
|
||||
return "AccountMap::" + key;
|
||||
}
|
||||
|
||||
private String getAccountEntityKey(UUID uuid) {
|
||||
@@ -435,6 +476,8 @@ public class AccountsManager {
|
||||
cacheCluster.useCluster(connection -> {
|
||||
final RedisAdvancedClusterCommands<String, String> commands = connection.sync();
|
||||
|
||||
|
||||
commands.set(getAccountMapKey(account.getPhoneNumberIdentifier().toString()), account.getUuid().toString());
|
||||
commands.set(getAccountMapKey(account.getNumber()), account.getUuid().toString());
|
||||
commands.set(getAccountEntityKey(account.getUuid()), accountJson);
|
||||
});
|
||||
@@ -443,11 +486,19 @@ public class AccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Account> redisGet(String number) {
|
||||
try (Timer.Context ignored = redisNumberGetTimer.time()) {
|
||||
final String uuid = cacheCluster.withCluster(connection -> connection.sync().get(getAccountMapKey(number)));
|
||||
private Optional<Account> redisGetByPhoneNumberIdentifier(UUID uuid) {
|
||||
return redisGetBySecondaryKey(uuid.toString(), redisPniGetTimer);
|
||||
}
|
||||
|
||||
if (uuid != null) return redisGet(UUID.fromString(uuid));
|
||||
private Optional<Account> redisGetByE164(String e164) {
|
||||
return redisGetBySecondaryKey(e164, redisNumberGetTimer);
|
||||
}
|
||||
|
||||
private Optional<Account> redisGetBySecondaryKey(String secondaryKey, Timer timer) {
|
||||
try (Timer.Context ignored = timer.time()) {
|
||||
final String uuid = cacheCluster.withCluster(connection -> connection.sync().get(getAccountMapKey(secondaryKey)));
|
||||
|
||||
if (uuid != null) return redisGetByAccountIdentifier(UUID.fromString(uuid));
|
||||
else return Optional.empty();
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Deserialization error", e);
|
||||
@@ -458,7 +509,7 @@ public class AccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Account> redisGet(UUID uuid) {
|
||||
private Optional<Account> redisGetByAccountIdentifier(UUID uuid) {
|
||||
try (Timer.Context ignored = redisUuidGetTimer.time()) {
|
||||
final String json = cacheCluster.withCluster(connection -> connection.sync().get(getAccountEntityKey(uuid)));
|
||||
|
||||
@@ -481,32 +532,10 @@ public class AccountsManager {
|
||||
|
||||
private void redisDelete(final Account account) {
|
||||
try (final Timer.Context ignored = redisDeleteTimer.time()) {
|
||||
cacheCluster.useCluster(connection -> connection.sync()
|
||||
.del(getAccountMapKey(account.getNumber()), getAccountEntityKey(account.getUuid())));
|
||||
cacheCluster.useCluster(connection -> connection.sync().del(
|
||||
getAccountMapKey(account.getNumber()),
|
||||
getAccountMapKey(account.getPhoneNumberIdentifier().toString()),
|
||||
getAccountEntityKey(account.getUuid())));
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Account> dynamoGet(String number) {
|
||||
return accounts.get(number);
|
||||
}
|
||||
|
||||
private Optional<Account> dynamoGet(UUID uuid) {
|
||||
return accounts.get(uuid);
|
||||
}
|
||||
|
||||
private boolean dynamoCreate(Account account) {
|
||||
return accounts.create(account);
|
||||
}
|
||||
|
||||
private void dynamoUpdate(Account account) {
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
private void dynamoDelete(final Account account) {
|
||||
accounts.delete(account.getUuid());
|
||||
}
|
||||
|
||||
private void dynamoChangeNumber(final Account account, final String number) {
|
||||
accounts.changeNumber(account, number);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ public class ContactDiscoveryWriter extends AccountDatabaseCrawlerListener {
|
||||
// It’s less than ideal, but crawler listeners currently must not call update()
|
||||
// with the accounts from the chunk, because updates cause the account instance to become stale. Instead, they
|
||||
// must get a new copy, which they are free to update.
|
||||
accounts.get(account.getUuid()).ifPresent(a -> accounts.update(a, NOOP_UPDATER));
|
||||
accounts.getByAccountIdentifier(account.getUuid()).ifPresent(a -> accounts.update(a, NOOP_UPDATER));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ public class Device {
|
||||
@JsonProperty
|
||||
private SignedPreKey signedPreKey;
|
||||
|
||||
@JsonProperty("pniSignedPreKey")
|
||||
private SignedPreKey phoneNumberIdentitySignedPreKey;
|
||||
|
||||
@JsonProperty
|
||||
private long lastSeen;
|
||||
|
||||
@@ -215,6 +218,14 @@ public class Device {
|
||||
this.signedPreKey = signedPreKey;
|
||||
}
|
||||
|
||||
public SignedPreKey getPhoneNumberIdentitySignedPreKey() {
|
||||
return phoneNumberIdentitySignedPreKey;
|
||||
}
|
||||
|
||||
public void setPhoneNumberIdentitySignedPreKey(final SignedPreKey phoneNumberIdentitySignedPreKey) {
|
||||
this.phoneNumberIdentitySignedPreKey = phoneNumberIdentitySignedPreKey;
|
||||
}
|
||||
|
||||
public long getPushTimestamp() {
|
||||
return pushTimestamp;
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
class Key {
|
||||
|
||||
private final byte[] userMessageQueue;
|
||||
private final byte[] userMessageQueueMetadata;
|
||||
private final byte[] userMessageQueuePersistInProgress;
|
||||
|
||||
private final String address;
|
||||
private final long deviceId;
|
||||
|
||||
Key(String address, long deviceId) {
|
||||
this.address = address;
|
||||
this.deviceId = deviceId;
|
||||
this.userMessageQueue = ("user_queue::" + address + "::" + deviceId).getBytes();
|
||||
this.userMessageQueueMetadata = ("user_queue_metadata::" + address + "::" + deviceId).getBytes();
|
||||
this.userMessageQueuePersistInProgress = ("user_queue_persisting::" + address + "::" + deviceId).getBytes();
|
||||
}
|
||||
|
||||
String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
long getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
byte[] getUserMessageQueue() {
|
||||
return userMessageQueue;
|
||||
}
|
||||
|
||||
byte[] getUserMessageQueueMetadata() {
|
||||
return userMessageQueueMetadata;
|
||||
}
|
||||
|
||||
byte[] getUserMessageQueuePersistInProgress() {
|
||||
return userMessageQueuePersistInProgress;
|
||||
}
|
||||
|
||||
static byte[] getUserMessageQueueIndex() {
|
||||
return "user_queue_index".getBytes();
|
||||
}
|
||||
|
||||
static Key fromUserMessageQueue(byte[] userMessageQueue) throws IOException {
|
||||
try {
|
||||
String[] parts = new String(userMessageQueue).split("::");
|
||||
|
||||
if (parts.length != 3) {
|
||||
throw new IOException("Malformed key: " + new String(userMessageQueue));
|
||||
}
|
||||
|
||||
return new Key(parts[1], Long.parseLong(parts[2]));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-32
@@ -5,15 +5,12 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.micrometer.core.instrument.DistributionSummary;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -32,7 +29,9 @@ import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.Select;
|
||||
import software.amazon.awssdk.services.dynamodb.model.WriteRequest;
|
||||
|
||||
public class KeysDynamoDb extends AbstractDynamoDbStore {
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
public class Keys extends AbstractDynamoDbStore {
|
||||
|
||||
private final String tableName;
|
||||
|
||||
@@ -40,30 +39,29 @@ public class KeysDynamoDb extends AbstractDynamoDbStore {
|
||||
static final String KEY_DEVICE_ID_KEY_ID = "DK";
|
||||
static final String KEY_PUBLIC_KEY = "P";
|
||||
|
||||
private static final Timer STORE_KEYS_TIMER = Metrics.timer(name(KeysDynamoDb.class, "storeKeys"));
|
||||
private static final Timer TAKE_KEY_FOR_DEVICE_TIMER = Metrics.timer(name(KeysDynamoDb.class, "takeKeyForDevice"));
|
||||
private static final Timer TAKE_KEYS_FOR_ACCOUNT_TIMER = Metrics.timer(name(KeysDynamoDb.class, "takeKeyForAccount"));
|
||||
private static final Timer GET_KEY_COUNT_TIMER = Metrics.timer(name(KeysDynamoDb.class, "getKeyCount"));
|
||||
private static final Timer DELETE_KEYS_FOR_DEVICE_TIMER = Metrics.timer(name(KeysDynamoDb.class, "deleteKeysForDevice"));
|
||||
private static final Timer DELETE_KEYS_FOR_ACCOUNT_TIMER = Metrics.timer(name(KeysDynamoDb.class, "deleteKeysForAccount"));
|
||||
private static final DistributionSummary CONTESTED_KEY_DISTRIBUTION = Metrics.summary(name(KeysDynamoDb.class, "contestedKeys"));
|
||||
private static final DistributionSummary KEY_COUNT_DISTRIBUTION = Metrics.summary(name(KeysDynamoDb.class, "keyCount"));
|
||||
private static final Timer STORE_KEYS_TIMER = Metrics.timer(name(Keys.class, "storeKeys"));
|
||||
private static final Timer TAKE_KEY_FOR_DEVICE_TIMER = Metrics.timer(name(Keys.class, "takeKeyForDevice"));
|
||||
private static final Timer GET_KEY_COUNT_TIMER = Metrics.timer(name(Keys.class, "getKeyCount"));
|
||||
private static final Timer DELETE_KEYS_FOR_DEVICE_TIMER = Metrics.timer(name(Keys.class, "deleteKeysForDevice"));
|
||||
private static final Timer DELETE_KEYS_FOR_ACCOUNT_TIMER = Metrics.timer(name(Keys.class, "deleteKeysForAccount"));
|
||||
private static final DistributionSummary CONTESTED_KEY_DISTRIBUTION = Metrics.summary(name(Keys.class, "contestedKeys"));
|
||||
private static final DistributionSummary KEY_COUNT_DISTRIBUTION = Metrics.summary(name(Keys.class, "keyCount"));
|
||||
|
||||
public KeysDynamoDb(final DynamoDbClient dynamoDB, final String tableName) {
|
||||
public Keys(final DynamoDbClient dynamoDB, final String tableName) {
|
||||
super(dynamoDB);
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
public void store(final Account account, final long deviceId, final List<PreKey> keys) {
|
||||
public void store(final UUID identifier, final long deviceId, final List<PreKey> keys) {
|
||||
STORE_KEYS_TIMER.record(() -> {
|
||||
delete(account.getUuid(), deviceId);
|
||||
delete(identifier, deviceId);
|
||||
|
||||
writeInBatches(keys, batch -> {
|
||||
List<WriteRequest> items = new ArrayList<>();
|
||||
for (final PreKey preKey : batch) {
|
||||
items.add(WriteRequest.builder()
|
||||
.putRequest(PutRequest.builder()
|
||||
.item(getItemFromPreKey(account.getUuid(), deviceId, preKey))
|
||||
.item(getItemFromPreKey(identifier, deviceId, preKey))
|
||||
.build())
|
||||
.build());
|
||||
}
|
||||
@@ -72,9 +70,9 @@ public class KeysDynamoDb extends AbstractDynamoDbStore {
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<PreKey> take(final Account account, final long deviceId) {
|
||||
public Optional<PreKey> take(final UUID identifier, final long deviceId) {
|
||||
return TAKE_KEY_FOR_DEVICE_TIMER.record(() -> {
|
||||
final AttributeValue partitionKey = getPartitionKey(account.getUuid());
|
||||
final AttributeValue partitionKey = getPartitionKey(identifier);
|
||||
QueryRequest queryRequest = QueryRequest.builder()
|
||||
.tableName(tableName)
|
||||
.keyConditionExpression("#uuid = :uuid AND begins_with (#sort, :sortprefix)")
|
||||
@@ -113,26 +111,14 @@ public class KeysDynamoDb extends AbstractDynamoDbStore {
|
||||
});
|
||||
}
|
||||
|
||||
public Map<Long, PreKey> take(final Account account) {
|
||||
return TAKE_KEYS_FOR_ACCOUNT_TIMER.record(() -> {
|
||||
final Map<Long, PreKey> preKeysByDeviceId = new HashMap<>();
|
||||
|
||||
for (final Device device : account.getDevices()) {
|
||||
take(account, device.getId()).ifPresent(preKey -> preKeysByDeviceId.put(device.getId(), preKey));
|
||||
}
|
||||
|
||||
return preKeysByDeviceId;
|
||||
});
|
||||
}
|
||||
|
||||
public int getCount(final Account account, final long deviceId) {
|
||||
public int getCount(final UUID identifier, final long deviceId) {
|
||||
return GET_KEY_COUNT_TIMER.record(() -> {
|
||||
QueryRequest queryRequest = QueryRequest.builder()
|
||||
.tableName(tableName)
|
||||
.keyConditionExpression("#uuid = :uuid AND begins_with (#sort, :sortprefix)")
|
||||
.expressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID, "#sort", KEY_DEVICE_ID_KEY_ID))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":uuid", getPartitionKey(account.getUuid()),
|
||||
":uuid", getPartitionKey(identifier),
|
||||
":sortprefix", getSortKeyPrefix(deviceId)))
|
||||
.select(Select.COUNT)
|
||||
.consistentRead(false)
|
||||
+1
-1
@@ -146,7 +146,7 @@ public class MessagePersister implements Managed {
|
||||
|
||||
@VisibleForTesting
|
||||
void persistQueue(final UUID accountUuid, final long deviceId) {
|
||||
final Optional<Account> maybeAccount = accountsManager.get(accountUuid);
|
||||
final Optional<Account> maybeAccount = accountsManager.getByAccountIdentifier(accountUuid);
|
||||
|
||||
if (maybeAccount.isEmpty()) {
|
||||
logger.error("No account record found for account {}", accountUuid);
|
||||
|
||||
@@ -167,7 +167,7 @@ public class MessagesCache extends RedisClusterPubSubAdapter<String, String> imp
|
||||
|
||||
for (final byte[] bytes : serialized) {
|
||||
try {
|
||||
removedMessages.add(constructEntityFromEnvelope(0, MessageProtos.Envelope.parseFrom(bytes)));
|
||||
removedMessages.add(constructEntityFromEnvelope(MessageProtos.Envelope.parseFrom(bytes)));
|
||||
} catch (final InvalidProtocolBufferException e) {
|
||||
logger.warn("Failed to parse envelope", e);
|
||||
}
|
||||
@@ -208,7 +208,7 @@ public class MessagesCache extends RedisClusterPubSubAdapter<String, String> imp
|
||||
|
||||
final long id = Long.parseLong(new String(queueItems.get(i + 1), StandardCharsets.UTF_8));
|
||||
|
||||
messageEntities.add(constructEntityFromEnvelope(id, message));
|
||||
messageEntities.add(constructEntityFromEnvelope(message));
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
logger.warn("Failed to parse envelope", e);
|
||||
}
|
||||
@@ -376,8 +376,8 @@ public class MessagesCache extends RedisClusterPubSubAdapter<String, String> imp
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static OutgoingMessageEntity constructEntityFromEnvelope(long id, MessageProtos.Envelope envelope) {
|
||||
return new OutgoingMessageEntity(id, true,
|
||||
static OutgoingMessageEntity constructEntityFromEnvelope(MessageProtos.Envelope envelope) {
|
||||
return new OutgoingMessageEntity(
|
||||
envelope.hasServerGuid() ? UUID.fromString(envelope.getServerGuid()) : null,
|
||||
envelope.getType().getNumber(),
|
||||
envelope.getRelay(),
|
||||
@@ -385,6 +385,7 @@ public class MessagesCache extends RedisClusterPubSubAdapter<String, String> imp
|
||||
envelope.getSource(),
|
||||
envelope.hasSourceUuid() ? UUID.fromString(envelope.getSourceUuid()) : null,
|
||||
envelope.getSourceDevice(),
|
||||
envelope.hasDestinationUuid() ? UUID.fromString(envelope.getDestinationUuid()) : null,
|
||||
envelope.hasLegacyMessage() ? envelope.getLegacyMessage().toByteArray() : null,
|
||||
envelope.hasContent() ? envelope.getContent().toByteArray() : null,
|
||||
envelope.hasServerTimestamp() ? envelope.getServerTimestamp() : 0);
|
||||
|
||||
+3
-1
@@ -46,6 +46,7 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
|
||||
private static final String KEY_SOURCE = "SN";
|
||||
private static final String KEY_SOURCE_UUID = "SU";
|
||||
private static final String KEY_SOURCE_DEVICE = "SD";
|
||||
private static final String KEY_DESTINATION_UUID = "DU";
|
||||
private static final String KEY_MESSAGE = "M";
|
||||
private static final String KEY_CONTENT = "C";
|
||||
private static final String KEY_TTL = "E";
|
||||
@@ -224,9 +225,10 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
|
||||
final String source = AttributeValues.getString(message, KEY_SOURCE, null);
|
||||
final UUID sourceUuid = AttributeValues.getUUID(message, KEY_SOURCE_UUID, null);
|
||||
final int sourceDevice = AttributeValues.getInt(message, KEY_SOURCE_DEVICE, 0);
|
||||
final UUID destinationUuid = AttributeValues.getUUID(message, KEY_DESTINATION_UUID, null);
|
||||
final byte[] messageBytes = AttributeValues.getByteArray(message, KEY_MESSAGE, null);
|
||||
final byte[] content = AttributeValues.getByteArray(message, KEY_CONTENT, null);
|
||||
return new OutgoingMessageEntity(-1L, false, messageUuid, type, relay, timestamp, source, sourceUuid, sourceDevice, messageBytes, content, sortKey.getServerTimestamp());
|
||||
return new OutgoingMessageEntity(messageUuid, type, relay, timestamp, source, sourceUuid, sourceDevice, destinationUuid, messageBytes, content, sortKey.getServerTimestamp());
|
||||
}
|
||||
|
||||
private void deleteRowsMatchingQuery(AttributeValue partitionKey, QueryRequest querySpec) {
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ public class NonNormalizedAccountCrawlerListener extends AccountDatabaseCrawlerL
|
||||
workingNonNormalizedNumbers++;
|
||||
|
||||
try {
|
||||
final Optional<Account> maybeConflictingAccount = accountsManager.get(getNormalizedNumber(account));
|
||||
final Optional<Account> maybeConflictingAccount = accountsManager.getByE164(getNormalizedNumber(account));
|
||||
|
||||
if (maybeConflictingAccount.isPresent()) {
|
||||
workingConflictingNumbers++;
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
/**
|
||||
* Manages a global, persistent mapping of phone numbers to phone number identifiers regardless of whether those
|
||||
* numbers/identifiers are actually associated with an account.
|
||||
*/
|
||||
public class PhoneNumberIdentifiers {
|
||||
|
||||
private final DynamoDbClient dynamoDbClient;
|
||||
private final String tableName;
|
||||
|
||||
@VisibleForTesting
|
||||
static final String KEY_E164 = "P";
|
||||
private static final String ATTR_PHONE_NUMBER_IDENTIFIER = "PNI";
|
||||
|
||||
private static final Timer GET_PNI_TIMER = Metrics.timer(name(PhoneNumberIdentifiers.class, "get"));
|
||||
private static final Timer SET_PNI_TIMER = Metrics.timer(name(PhoneNumberIdentifiers.class, "set"));
|
||||
|
||||
public PhoneNumberIdentifiers(final DynamoDbClient dynamoDbClient, final String tableName) {
|
||||
this.dynamoDbClient = dynamoDbClient;
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the phone number identifier (PNI) associated with the given phone number.
|
||||
*
|
||||
* @param phoneNumber the phone number for which to retrieve a phone number identifier
|
||||
* @return the phone number identifier associated with the given phone number
|
||||
*/
|
||||
public UUID getPhoneNumberIdentifier(final String phoneNumber) {
|
||||
final GetItemResponse response = GET_PNI_TIMER.record(() -> dynamoDbClient.getItem(GetItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(Map.of(KEY_E164, AttributeValues.fromString(phoneNumber)))
|
||||
.projectionExpression(ATTR_PHONE_NUMBER_IDENTIFIER)
|
||||
.build()));
|
||||
|
||||
final UUID phoneNumberIdentifier;
|
||||
|
||||
if (response.hasItem()) {
|
||||
phoneNumberIdentifier = AttributeValues.getUUID(response.item(), ATTR_PHONE_NUMBER_IDENTIFIER, null);
|
||||
} else {
|
||||
phoneNumberIdentifier = generatePhoneNumberIdentifierIfNotExists(phoneNumber);
|
||||
}
|
||||
|
||||
if (phoneNumberIdentifier == null) {
|
||||
throw new RuntimeException("Could not retrieve phone number identifier from stored item");
|
||||
}
|
||||
|
||||
return phoneNumberIdentifier;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
UUID generatePhoneNumberIdentifierIfNotExists(final String phoneNumber) {
|
||||
final UpdateItemResponse response = SET_PNI_TIMER.record(() -> dynamoDbClient.updateItem(UpdateItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(Map.of(KEY_E164, AttributeValues.fromString(phoneNumber)))
|
||||
.updateExpression("SET #pni = if_not_exists(#pni, :pni)")
|
||||
.expressionAttributeNames(Map.of("#pni", ATTR_PHONE_NUMBER_IDENTIFIER))
|
||||
.expressionAttributeValues(Map.of(":pni", AttributeValues.fromUUID(UUID.randomUUID())))
|
||||
.returnValues(ReturnValue.ALL_NEW)
|
||||
.build()));
|
||||
|
||||
return AttributeValues.getUUID(response.attributes(), ATTR_PHONE_NUMBER_IDENTIFIER, null);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -58,7 +58,7 @@ public class PushFeedbackProcessor extends AccountDatabaseCrawlerListener {
|
||||
|
||||
if (update) {
|
||||
// fetch a new version, since the chunk is shared and implicitly read-only
|
||||
accountsManager.get(account.getUuid()).ifPresent(accountToUpdate -> {
|
||||
accountsManager.getByAccountIdentifier(account.getUuid()).ifPresent(accountToUpdate -> {
|
||||
accountsManager.update(accountToUpdate, a -> {
|
||||
for (Device device : a.getDevices()) {
|
||||
if (deviceNeedsUpdate(device)) {
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ public class RefreshingAccountAndDeviceSupplier implements Supplier<Pair<Account
|
||||
@Override
|
||||
public Pair<Account, Device> get() {
|
||||
if (account.isStale()) {
|
||||
account = accountsManager.get(account.getUuid())
|
||||
account = accountsManager.getByAccountIdentifier(account.getUuid())
|
||||
.orElseThrow(() -> new RuntimeException("Could not find account"));
|
||||
device = account.getDevice(device.getId())
|
||||
.orElseThrow(() -> new RefreshingAccountAndDeviceNotFoundException("Could not find device"));
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.stripe.param.SetupIntentCreateParams;
|
||||
import com.stripe.param.SubscriptionCancelParams;
|
||||
import com.stripe.param.SubscriptionCreateParams;
|
||||
import com.stripe.param.SubscriptionListParams;
|
||||
import com.stripe.param.SubscriptionRetrieveParams;
|
||||
import com.stripe.param.SubscriptionUpdateParams;
|
||||
import com.stripe.param.SubscriptionUpdateParams.BillingCycleAnchor;
|
||||
import com.stripe.param.SubscriptionUpdateParams.ProrationBehavior;
|
||||
@@ -62,11 +63,13 @@ public class StripeManager {
|
||||
private final String apiKey;
|
||||
private final Executor executor;
|
||||
private final byte[] idempotencyKeyGenerator;
|
||||
private final String boostDescription;
|
||||
|
||||
public StripeManager(
|
||||
@Nonnull String apiKey,
|
||||
@Nonnull Executor executor,
|
||||
@Nonnull byte[] idempotencyKeyGenerator) {
|
||||
@Nonnull byte[] idempotencyKeyGenerator,
|
||||
@Nonnull String boostDescription) {
|
||||
this.apiKey = Objects.requireNonNull(apiKey);
|
||||
if (Strings.isNullOrEmpty(apiKey)) {
|
||||
throw new IllegalArgumentException("apiKey cannot be empty");
|
||||
@@ -76,6 +79,7 @@ public class StripeManager {
|
||||
if (idempotencyKeyGenerator.length == 0) {
|
||||
throw new IllegalArgumentException("idempotencyKeyGenerator cannot be empty");
|
||||
}
|
||||
this.boostDescription = Objects.requireNonNull(boostDescription);
|
||||
}
|
||||
|
||||
private RequestOptions commonOptions() {
|
||||
@@ -148,6 +152,7 @@ public class StripeManager {
|
||||
PaymentIntentCreateParams params = PaymentIntentCreateParams.builder()
|
||||
.setAmount(amount)
|
||||
.setCurrency(currency.toLowerCase(Locale.ROOT))
|
||||
.setDescription(boostDescription)
|
||||
.build();
|
||||
try {
|
||||
return PaymentIntent.create(params, commonOptions());
|
||||
@@ -341,6 +346,19 @@ public class StripeManager {
|
||||
}, executor);
|
||||
}
|
||||
|
||||
public CompletableFuture<Invoice> getLatestInvoiceForSubscription(String subscriptionId) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
SubscriptionRetrieveParams params = SubscriptionRetrieveParams.builder()
|
||||
.addExpand("latest_invoice")
|
||||
.build();
|
||||
try {
|
||||
return Subscription.retrieve(subscriptionId, params, commonOptions()).getLatestInvoiceObject();
|
||||
} catch (StripeException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}, executor);
|
||||
}
|
||||
|
||||
public CompletableFuture<Collection<InvoiceLineItem>> getInvoiceLineItemsForInvoice(Invoice invoice) {
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> Lists.newArrayList(invoice.getLines().autoPagingIterable(null, commonOptions())), executor);
|
||||
|
||||
@@ -119,6 +119,6 @@ public class AttributeValues {
|
||||
}
|
||||
|
||||
public static UUID getUUID(Map<String, AttributeValue> item, String key, UUID defaultValue) {
|
||||
return AttributeValues.get(item, key).map(AttributeValues::toUUID).orElse(defaultValue);
|
||||
return AttributeValues.get(item, key).filter(av -> av.b() != null).map(AttributeValues::toUUID).orElse(defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
|
||||
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
@@ -107,16 +108,6 @@ public class Util {
|
||||
return param == null || param.length() == 0;
|
||||
}
|
||||
|
||||
public static byte[] combine(byte[] one, byte[] two, byte[] three, byte[] four) {
|
||||
byte[] combined = new byte[one.length + two.length + three.length + four.length];
|
||||
System.arraycopy(one, 0, combined, 0, one.length);
|
||||
System.arraycopy(two, 0, combined, one.length, two.length);
|
||||
System.arraycopy(three, 0, combined, one.length + two.length, three.length);
|
||||
System.arraycopy(four, 0, combined, one.length + two.length + three.length, four.length);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
public static byte[] truncate(byte[] element, int length) {
|
||||
byte[] result = new byte[length];
|
||||
System.arraycopy(element, 0, result, 0, result.length);
|
||||
@@ -124,7 +115,6 @@ public class Util {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static byte[][] split(byte[] input, int firstLength, int secondLength) {
|
||||
byte[][] parts = new byte[2][];
|
||||
|
||||
@@ -161,11 +151,19 @@ public class Util {
|
||||
return data;
|
||||
}
|
||||
|
||||
public static byte[] longToByteArray(long value) {
|
||||
final ByteBuffer longBuffer = ByteBuffer.allocate(Long.BYTES);
|
||||
|
||||
longBuffer.putLong(value);
|
||||
|
||||
return longBuffer.array();
|
||||
}
|
||||
|
||||
public static int toIntExact(long value) {
|
||||
if ((int)value != value) {
|
||||
if ((int) value != value) {
|
||||
throw new ArithmeticException("integer overflow");
|
||||
}
|
||||
return (int)value;
|
||||
return (int) value;
|
||||
}
|
||||
|
||||
public static int currentDaysSinceEpoch() {
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ public class DeadLetterHandler implements DispatchChannel {
|
||||
switch (pubSubMessage.getType().getNumber()) {
|
||||
case PubSubMessage.Type.DELIVER_VALUE:
|
||||
Envelope message = Envelope.parseFrom(pubSubMessage.getContent());
|
||||
Optional<Account> maybeAccount = accountsManager.get(address.getNumber());
|
||||
Optional<Account> maybeAccount = accountsManager.getByE164(address.getNumber());
|
||||
|
||||
if (maybeAccount.isPresent()) {
|
||||
messagesManager.insert(maybeAccount.get().getUuid(), address.getDeviceId(), message);
|
||||
|
||||
+1
-1
@@ -289,7 +289,7 @@ public class WebSocketConnection implements MessageAvailabilityListener, Displac
|
||||
for (int i = 0; i < messages.getMessages().size(); i++) {
|
||||
final OutgoingMessageEntity message = messages.getMessages().get(i);
|
||||
final Envelope.Builder builder = Envelope.newBuilder()
|
||||
.setType(Envelope.Type.valueOf(message.getType()))
|
||||
.setType(Envelope.Type.forNumber(message.getType()))
|
||||
.setTimestamp(message.getTimestamp())
|
||||
.setServerTimestamp(message.getServerTimestamp());
|
||||
|
||||
|
||||
+18
-11
@@ -18,6 +18,7 @@ import io.dropwizard.jdbi3.JdbiFactory;
|
||||
import io.dropwizard.setup.Environment;
|
||||
import io.lettuce.core.resource.ClientResources;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.time.Clock;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -43,10 +44,11 @@ import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
|
||||
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesCache;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
|
||||
import org.whispersystems.textsecuregcm.storage.Profiles;
|
||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
|
||||
@@ -88,6 +90,7 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
|
||||
throws Exception
|
||||
{
|
||||
try {
|
||||
Clock clock = Clock.systemUTC();
|
||||
String[] users = namespace.getString("user").split(",");
|
||||
|
||||
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
@@ -110,6 +113,9 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
|
||||
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client(
|
||||
configuration.getDeletedAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
DynamoDbClient phoneNumberIdentifiersDynamoDbClient =
|
||||
DynamoDbFromConfig.client(configuration.getPhoneNumberIdentifiersDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster",
|
||||
configuration.getCacheClusterConfiguration(), redisClusterClientResources);
|
||||
@@ -122,11 +128,9 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
|
||||
.executorService(name(getClass(), "storageService-%d")).maxThreads(8).minThreads(1).build();
|
||||
|
||||
ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
configuration.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0],
|
||||
false);
|
||||
configuration.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
|
||||
ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
configuration.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0],
|
||||
false);
|
||||
configuration.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
|
||||
|
||||
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager = new DynamicConfigurationManager<>(
|
||||
configuration.getAppConfig().getApplication(), configuration.getAppConfig().getEnvironment(),
|
||||
@@ -156,11 +160,14 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
|
||||
Accounts accounts = new Accounts(accountsDynamoDbClient,
|
||||
configuration.getAccountsDynamoDbConfiguration().getTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getScanPageSize());
|
||||
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
|
||||
configuration.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
|
||||
KeysDynamoDb keysDynamoDb = new KeysDynamoDb(preKeysDynamoDb,
|
||||
Keys keys = new Keys(preKeysDynamoDb,
|
||||
configuration.getKeysDynamoDbConfiguration().getTableName());
|
||||
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messageDynamoDb,
|
||||
configuration.getMessageDynamoDbConfiguration().getTableName(),
|
||||
@@ -183,7 +190,7 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
|
||||
Executors.newSingleThreadScheduledExecutor(), keyspaceNotificationDispatchExecutor);
|
||||
MessagesCache messagesCache = new MessagesCache(messageInsertCacheCluster, messageReadDeleteCluster,
|
||||
keyspaceNotificationDispatchExecutor);
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster);
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, dynamicConfigurationManager);
|
||||
DirectoryQueue directoryQueue = new DirectoryQueue(
|
||||
configuration.getDirectoryConfiguration().getSqsConfiguration());
|
||||
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster);
|
||||
@@ -199,12 +206,12 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
|
||||
deletedAccountsLockDynamoDbClient,
|
||||
configuration.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
|
||||
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keys, messagesManager, usernamesManager, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
|
||||
|
||||
for (String user : users) {
|
||||
Optional<Account> account = accountsManager.get(user);
|
||||
Optional<Account> account = accountsManager.getByE164(user);
|
||||
|
||||
if (account.isPresent()) {
|
||||
accountsManager.delete(account.get(), DeletionReason.ADMIN_DELETED);
|
||||
|
||||
+19
-12
@@ -18,6 +18,7 @@ import io.dropwizard.jdbi3.JdbiFactory;
|
||||
import io.dropwizard.setup.Environment;
|
||||
import io.lettuce.core.resource.ClientResources;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.time.Clock;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -41,10 +42,11 @@ import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
|
||||
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesCache;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
|
||||
import org.whispersystems.textsecuregcm.storage.Profiles;
|
||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
|
||||
@@ -91,6 +93,7 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
|
||||
final WhisperServerConfiguration configuration) throws Exception {
|
||||
|
||||
try {
|
||||
Clock clock = Clock.systemUTC();
|
||||
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
JdbiFactory jdbiFactory = new JdbiFactory();
|
||||
@@ -112,6 +115,9 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
|
||||
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig
|
||||
.client(configuration.getDeletedAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
DynamoDbClient phoneNumberIdentifiersDynamoDbClient =
|
||||
DynamoDbFromConfig.client(configuration.getPhoneNumberIdentifiersDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster",
|
||||
configuration.getCacheClusterConfiguration(), redisClusterClientResources);
|
||||
@@ -126,11 +132,9 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
|
||||
.executorService(name(getClass(), "storageService-%d")).maxThreads(8).minThreads(1).build();
|
||||
|
||||
ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
configuration.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0],
|
||||
false);
|
||||
configuration.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
|
||||
ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
configuration.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0],
|
||||
false);
|
||||
configuration.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
|
||||
|
||||
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager = new DynamicConfigurationManager<>(
|
||||
configuration.getAppConfig().getApplication(), configuration.getAppConfig().getEnvironment(),
|
||||
@@ -161,11 +165,14 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
|
||||
Accounts accounts = new Accounts(accountsDynamoDbClient,
|
||||
configuration.getAccountsDynamoDbConfiguration().getTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
|
||||
configuration.getAccountsDynamoDbConfiguration().getScanPageSize());
|
||||
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
|
||||
configuration.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
|
||||
KeysDynamoDb keysDynamoDb = new KeysDynamoDb(preKeysDynamoDb,
|
||||
Keys keys = new Keys(preKeysDynamoDb,
|
||||
configuration.getKeysDynamoDbConfiguration().getTableName());
|
||||
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messageDynamoDb,
|
||||
configuration.getMessageDynamoDbConfiguration().getTableName(),
|
||||
@@ -186,7 +193,7 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
|
||||
Executors.newSingleThreadScheduledExecutor(), keyspaceNotificationDispatchExecutor);
|
||||
MessagesCache messagesCache = new MessagesCache(messageInsertCacheCluster, messageReadDeleteCluster,
|
||||
keyspaceNotificationDispatchExecutor);
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster);
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, dynamicConfigurationManager);
|
||||
DirectoryQueue directoryQueue = new DirectoryQueue(
|
||||
configuration.getDirectoryConfiguration().getSqsConfiguration());
|
||||
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster);
|
||||
@@ -202,16 +209,16 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
|
||||
deletedAccountsLockDynamoDbClient,
|
||||
configuration.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
|
||||
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keys, messagesManager, usernamesManager, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
|
||||
|
||||
Optional<Account> maybeAccount;
|
||||
|
||||
try {
|
||||
maybeAccount = accountsManager.get(UUID.fromString(namespace.getString("user")));
|
||||
maybeAccount = accountsManager.getByAccountIdentifier(UUID.fromString(namespace.getString("user")));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
maybeAccount = accountsManager.get(namespace.getString("user"));
|
||||
maybeAccount = accountsManager.getByE164(namespace.getString("user"));
|
||||
}
|
||||
|
||||
maybeAccount.ifPresentOrElse(account -> {
|
||||
|
||||
@@ -42,6 +42,7 @@ message Envelope {
|
||||
optional string serverGuid = 9;
|
||||
optional uint64 server_timestamp = 10;
|
||||
optional bool ephemeral = 12; // indicates that the message should not be persisted if the recipient is offline
|
||||
optional string destinationUuid = 13;
|
||||
}
|
||||
|
||||
message ProvisioningUuid {
|
||||
|
||||
@@ -16,13 +16,13 @@ TEST3_name = Test Badge Gamma
|
||||
TEST3_description = {short_name} is testing the gamma test badge.
|
||||
|
||||
R_LOW_name = Signal Star
|
||||
R_LOW_description = {short_name} supports Signal by making a monthly donation.
|
||||
R_LOW_description = {short_name} is a Sustainer. Signal is a nonprofit with no advertisers or investors, supported only by people like you.
|
||||
|
||||
R_MED_name = Signal Planet
|
||||
R_MED_description = {short_name} supports Signal by making a monthly donation.
|
||||
R_MED_description = {short_name} is a Sustainer. Signal is a nonprofit with no advertisers or investors, supported only by people like you.
|
||||
|
||||
R_HIGH_name = Signal Sun
|
||||
R_HIGH_description = {short_name} supports Signal by making a monthly donation.
|
||||
R_HIGH_description = {short_name} is a Sustainer. Signal is a nonprofit with no advertisers or investors, supported only by people like you.
|
||||
|
||||
BOOST_name = Signal Boost
|
||||
BOOST_description = {short_name} supported Signal by making a donation.
|
||||
BOOST_description = {short_name} supported Signal with a boost. Signal is a nonprofit with no advertisers or investors, supported only by people like you.
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Checks whether all YAML configuration files in a given directory are valid.
|
||||
* <p>
|
||||
* Note: the current implementation fails fast, rather than reporting multiple invalid files
|
||||
*/
|
||||
public class CheckServiceConfigurations {
|
||||
|
||||
private void checkConfiguration(final File configDirectory) {
|
||||
|
||||
final File[] configFiles = configDirectory.listFiles(f ->
|
||||
!f.isDirectory()
|
||||
&& f.getPath().endsWith(".yml"));
|
||||
|
||||
if (configFiles == null || configFiles.length == 0) {
|
||||
throw new IllegalArgumentException("No .yml configuration files found at " + configDirectory.getPath());
|
||||
}
|
||||
|
||||
for (File configFile : configFiles) {
|
||||
String[] args = new String[]{"check", configFile.getAbsolutePath()};
|
||||
|
||||
try {
|
||||
new WhisperServerService().run(args);
|
||||
} catch (final Exception e) {
|
||||
// Invalid configuration will cause the "check" command to call `System.exit()`, rather than throwing,
|
||||
// so this is unexpected
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
if (args.length != 1) {
|
||||
throw new IllegalArgumentException("Expected single argument with config directory: " + Arrays.toString(args));
|
||||
}
|
||||
|
||||
final File configDirectory = new File(args[0]);
|
||||
|
||||
if (!(configDirectory.exists() && configDirectory.isDirectory())) {
|
||||
throw new IllegalArgumentException("No directory found at " + configDirectory.getPath());
|
||||
}
|
||||
|
||||
new CheckServiceConfigurations().checkConfiguration(configDirectory);
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -128,7 +128,7 @@ class AuthEnablementRefreshRequirementProviderTest {
|
||||
account.addDevice(authenticatedDevice);
|
||||
LongStream.range(2, 4).forEach(deviceId -> account.addDevice(DevicesHelper.createDevice(deviceId)));
|
||||
|
||||
when(accountsManager.get(uuid)).thenReturn(Optional.of(account));
|
||||
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
|
||||
|
||||
account.getDevices()
|
||||
.forEach(device -> when(clientPresenceManager.isPresent(uuid, device.getId())).thenReturn(true));
|
||||
@@ -310,7 +310,7 @@ class AuthEnablementRefreshRequirementProviderTest {
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
verify(accountsManager, never()).get(any(UUID.class));
|
||||
verify(accountsManager, never()).getByAccountIdentifier(any(UUID.class));
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
||||
+12
-12
@@ -31,9 +31,6 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.BaseAccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
@@ -62,11 +59,14 @@ class BaseAccountAuthenticatorTest {
|
||||
clock = mock(Clock.class);
|
||||
baseAccountAuthenticator = new BaseAccountAuthenticator(accountsManager, clock);
|
||||
|
||||
acct1 = new Account("+14088675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null,
|
||||
acct1 = new Account("+14088675309", AuthHelper.getRandomUUID(random), UUID.randomUUID(),
|
||||
Set.of(new Device(1, null, null, null,
|
||||
null, null, null, false, 0, null, yesterday, 0, null, 0, null)), null);
|
||||
acct2 = new Account("+14098675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null,
|
||||
acct2 = new Account("+14098675309", AuthHelper.getRandomUUID(random), UUID.randomUUID(),
|
||||
Set.of(new Device(1, null, null, null,
|
||||
null, null, null, false, 0, null, yesterday, 0, null, 0, null)), null);
|
||||
oldAccount = new Account("+14108675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null,
|
||||
oldAccount = new Account("+14108675309", AuthHelper.getRandomUUID(random), UUID.randomUUID(),
|
||||
Set.of(new Device(1, null, null, null,
|
||||
null, null, null, false, 0, null, oldTime, 0, null, 0, null)), null);
|
||||
|
||||
AccountsHelper.setupMockUpdate(accountsManager);
|
||||
@@ -156,7 +156,7 @@ class BaseAccountAuthenticatorTest {
|
||||
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
|
||||
|
||||
when(clock.instant()).thenReturn(Instant.now());
|
||||
when(accountsManager.get(uuid)).thenReturn(Optional.of(account));
|
||||
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
|
||||
when(account.getUuid()).thenReturn(uuid);
|
||||
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
|
||||
when(account.isEnabled()).thenReturn(true);
|
||||
@@ -184,7 +184,7 @@ class BaseAccountAuthenticatorTest {
|
||||
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
|
||||
|
||||
when(clock.instant()).thenReturn(Instant.now());
|
||||
when(accountsManager.get(uuid)).thenReturn(Optional.of(account));
|
||||
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
|
||||
when(account.getUuid()).thenReturn(uuid);
|
||||
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
|
||||
when(account.isEnabled()).thenReturn(true);
|
||||
@@ -213,7 +213,7 @@ class BaseAccountAuthenticatorTest {
|
||||
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
|
||||
|
||||
when(clock.instant()).thenReturn(Instant.now());
|
||||
when(accountsManager.get(uuid)).thenReturn(Optional.of(account));
|
||||
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
|
||||
when(account.getUuid()).thenReturn(uuid);
|
||||
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
|
||||
when(account.isEnabled()).thenReturn(false);
|
||||
@@ -251,7 +251,7 @@ class BaseAccountAuthenticatorTest {
|
||||
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
|
||||
|
||||
when(clock.instant()).thenReturn(Instant.now());
|
||||
when(accountsManager.get(uuid)).thenReturn(Optional.of(account));
|
||||
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
|
||||
when(account.getUuid()).thenReturn(uuid);
|
||||
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
|
||||
when(account.isEnabled()).thenReturn(true);
|
||||
@@ -278,7 +278,7 @@ class BaseAccountAuthenticatorTest {
|
||||
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
|
||||
|
||||
when(clock.instant()).thenReturn(Instant.now());
|
||||
when(accountsManager.get(uuid)).thenReturn(Optional.of(account));
|
||||
when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
|
||||
when(account.getUuid()).thenReturn(uuid);
|
||||
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
|
||||
when(account.isEnabled()).thenReturn(true);
|
||||
@@ -303,7 +303,7 @@ class BaseAccountAuthenticatorTest {
|
||||
() -> baseAccountAuthenticator.authenticate(new BasicCredentials(username, "password"), true));
|
||||
|
||||
assertThat(maybeAuthenticatedAccount).isEmpty();
|
||||
verify(accountsManager, never()).get(any(UUID.class));
|
||||
verify(accountsManager, never()).getByAccountIdentifier(any(UUID.class));
|
||||
}
|
||||
|
||||
private static Stream<String> testAuthenticateMalformedCredentials() {
|
||||
|
||||
+4
-5
@@ -69,7 +69,7 @@ public class ConfiguredProfileBadgeConverterTest {
|
||||
private static BadgeConfiguration newBadge(int i) {
|
||||
return new BadgeConfiguration(
|
||||
idFor(i), "other", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
|
||||
List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt")));
|
||||
List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")));
|
||||
}
|
||||
|
||||
private BadgesConfiguration createBadges(int count) {
|
||||
@@ -142,8 +142,7 @@ public class ConfiguredProfileBadgeConverterTest {
|
||||
arguments(idFor(0), expired, true, false, null),
|
||||
arguments(idFor(0), notExpired, true, false,
|
||||
new Badge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
|
||||
List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"),
|
||||
new BadgeSvg("ll", "ld", "lt")))),
|
||||
List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")))),
|
||||
arguments(idFor(1), expired, false, false, null),
|
||||
arguments(idFor(1), notExpired, false, false, null),
|
||||
arguments(idFor(1), expired, true, false, null),
|
||||
@@ -152,13 +151,13 @@ public class ConfiguredProfileBadgeConverterTest {
|
||||
arguments(idFor(0), notExpired, false, true,
|
||||
new SelfBadge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"),
|
||||
"SVG",
|
||||
List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt")),
|
||||
List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")),
|
||||
notExpired, false)),
|
||||
arguments(idFor(0), expired, true, true, null),
|
||||
arguments(idFor(0), notExpired, true, true,
|
||||
new SelfBadge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"),
|
||||
"SVG",
|
||||
List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt")),
|
||||
List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")),
|
||||
notExpired, true)),
|
||||
arguments(idFor(1), expired, false, true, null),
|
||||
arguments(idFor(1), notExpired, false, true, null),
|
||||
|
||||
+90
-81
@@ -38,18 +38,19 @@ class DynamicConfigurationTest {
|
||||
}
|
||||
|
||||
{
|
||||
final String experimentConfigYaml =
|
||||
"experiments:\n" +
|
||||
" percentageOnly:\n" +
|
||||
" enrollmentPercentage: 12\n" +
|
||||
" uuidsAndPercentage:\n" +
|
||||
" enrolledUuids:\n" +
|
||||
" - 717b1c09-ed0b-4120-bb0e-f4697534b8e1\n" +
|
||||
" - 279f264c-56d7-4bbf-b9da-de718ff90903\n" +
|
||||
" enrollmentPercentage: 77\n" +
|
||||
" uuidsOnly:\n" +
|
||||
" enrolledUuids:\n" +
|
||||
" - 71618739-114c-4b1f-bb0d-6478a44eb600";
|
||||
final String experimentConfigYaml = """
|
||||
experiments:
|
||||
percentageOnly:
|
||||
enrollmentPercentage: 12
|
||||
uuidsAndPercentage:
|
||||
enrolledUuids:
|
||||
- 717b1c09-ed0b-4120-bb0e-f4697534b8e1
|
||||
- 279f264c-56d7-4bbf-b9da-de718ff90903
|
||||
enrollmentPercentage: 77
|
||||
uuidsOnly:
|
||||
enrolledUuids:
|
||||
- 71618739-114c-4b1f-bb0d-6478a44eb600
|
||||
""";
|
||||
|
||||
final DynamicConfiguration config =
|
||||
DynamicConfigurationManager.parseConfiguration(experimentConfigYaml, DynamicConfiguration.class).orElseThrow();
|
||||
@@ -86,27 +87,28 @@ class DynamicConfigurationTest {
|
||||
}
|
||||
|
||||
{
|
||||
final String experimentConfigYaml =
|
||||
"preRegistrationExperiments:\n" +
|
||||
" percentageOnly:\n" +
|
||||
" enrollmentPercentage: 17\n" +
|
||||
" e164sCountryCodesAndPercentage:\n" +
|
||||
" enrolledE164s:\n" +
|
||||
" - +120255551212\n" +
|
||||
" - +3655323174\n" +
|
||||
" excludedE164s:\n" +
|
||||
" - +120255551213\n" +
|
||||
" - +3655323175\n" +
|
||||
" enrollmentPercentage: 46\n" +
|
||||
" excludedCountryCodes:\n" +
|
||||
" - 47\n" +
|
||||
" includedCountryCodes:\n" +
|
||||
" - 56\n" +
|
||||
" e164sAndExcludedCodes:\n" +
|
||||
" enrolledE164s:\n" +
|
||||
" - +120255551212\n" +
|
||||
" excludedCountryCodes:\n" +
|
||||
" - 47";
|
||||
final String experimentConfigYaml = """
|
||||
preRegistrationExperiments:
|
||||
percentageOnly:
|
||||
enrollmentPercentage: 17
|
||||
e164sCountryCodesAndPercentage:
|
||||
enrolledE164s:
|
||||
- +120255551212
|
||||
- +3655323174
|
||||
excludedE164s:
|
||||
- +120255551213
|
||||
- +3655323175
|
||||
enrollmentPercentage: 46
|
||||
excludedCountryCodes:
|
||||
- 47
|
||||
includedCountryCodes:
|
||||
- 56
|
||||
e164sAndExcludedCodes:
|
||||
enrolledE164s:
|
||||
- +120255551212
|
||||
excludedCountryCodes:
|
||||
- 47
|
||||
""";
|
||||
|
||||
final DynamicConfiguration config =
|
||||
DynamicConfigurationManager.parseConfiguration(experimentConfigYaml, DynamicConfiguration.class).orElseThrow();
|
||||
@@ -167,18 +169,17 @@ class DynamicConfigurationTest {
|
||||
}
|
||||
|
||||
{
|
||||
final String remoteDeprecationConfig =
|
||||
"remoteDeprecation:\n" +
|
||||
" minimumVersions:\n" +
|
||||
" IOS: 1.2.3\n" +
|
||||
" ANDROID: 4.5.6\n" +
|
||||
|
||||
" versionsPendingDeprecation:\n" +
|
||||
" DESKTOP: 7.8.9\n" +
|
||||
|
||||
" blockedVersions:\n" +
|
||||
" DESKTOP:\n" +
|
||||
" - 1.4.0-beta.2";
|
||||
final String remoteDeprecationConfig = """
|
||||
remoteDeprecation:
|
||||
minimumVersions:
|
||||
IOS: 1.2.3
|
||||
ANDROID: 4.5.6
|
||||
versionsPendingDeprecation:
|
||||
DESKTOP: 7.8.9
|
||||
blockedVersions:
|
||||
DESKTOP:
|
||||
- 1.4.0-beta.2
|
||||
""";
|
||||
|
||||
final DynamicConfiguration config =
|
||||
DynamicConfigurationManager.parseConfiguration(remoteDeprecationConfig, DynamicConfiguration.class).orElseThrow();
|
||||
@@ -207,9 +208,10 @@ class DynamicConfigurationTest {
|
||||
}
|
||||
|
||||
{
|
||||
final String messageRateConfigYaml =
|
||||
"messageRate:\n" +
|
||||
" enforceUnsealedSenderRateLimit: true";
|
||||
final String messageRateConfigYaml = """
|
||||
messageRate:
|
||||
enforceUnsealedSenderRateLimit: true
|
||||
""";
|
||||
|
||||
final DynamicConfiguration emptyConfig =
|
||||
DynamicConfigurationManager.parseConfiguration(messageRateConfigYaml, DynamicConfiguration.class).orElseThrow();
|
||||
@@ -229,9 +231,10 @@ class DynamicConfigurationTest {
|
||||
}
|
||||
|
||||
{
|
||||
final String featureFlagYaml =
|
||||
"featureFlags:\n"
|
||||
+ " - testFlag";
|
||||
final String featureFlagYaml = """
|
||||
featureFlags:
|
||||
- testFlag
|
||||
""";
|
||||
|
||||
final DynamicConfiguration emptyConfig =
|
||||
DynamicConfigurationManager.parseConfiguration(featureFlagYaml, DynamicConfiguration.class).orElseThrow();
|
||||
@@ -251,11 +254,12 @@ class DynamicConfigurationTest {
|
||||
}
|
||||
|
||||
{
|
||||
final String twilioConfigYaml =
|
||||
"twilio:\n"
|
||||
+ " numbers:\n"
|
||||
+ " - 2135551212\n"
|
||||
+ " - 2135551313";
|
||||
final String twilioConfigYaml = """
|
||||
twilio:
|
||||
numbers:
|
||||
- 2135551212
|
||||
- 2135551313
|
||||
""";
|
||||
|
||||
final DynamicTwilioConfiguration config =
|
||||
DynamicConfigurationManager.parseConfiguration(twilioConfigYaml, DynamicConfiguration.class).orElseThrow()
|
||||
@@ -272,20 +276,21 @@ class DynamicConfigurationTest {
|
||||
final DynamicConfiguration emptyConfig =
|
||||
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow();
|
||||
|
||||
assertTrue(emptyConfig.getPaymentsConfiguration().getAllowedCountryCodes().isEmpty());
|
||||
assertTrue(emptyConfig.getPaymentsConfiguration().getDisallowedPrefixes().isEmpty());
|
||||
}
|
||||
|
||||
{
|
||||
final String paymentsConfigYaml =
|
||||
"payments:\n"
|
||||
+ " allowedCountryCodes:\n"
|
||||
+ " - 44";
|
||||
final String paymentsConfigYaml = """
|
||||
payments:
|
||||
disallowedPrefixes:
|
||||
- +44
|
||||
""";
|
||||
|
||||
final DynamicPaymentsConfiguration config =
|
||||
DynamicConfigurationManager.parseConfiguration(paymentsConfigYaml, DynamicConfiguration.class).orElseThrow()
|
||||
.getPaymentsConfiguration();
|
||||
|
||||
assertEquals(Set.of("44"), config.getAllowedCountryCodes());
|
||||
assertEquals(List.of("+44"), config.getDisallowedPrefixes());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,10 +305,11 @@ class DynamicConfigurationTest {
|
||||
}
|
||||
|
||||
{
|
||||
final String signupCaptchaConfig =
|
||||
"signupCaptcha:\n"
|
||||
+ " countryCodes:\n"
|
||||
+ " - 1";
|
||||
final String signupCaptchaConfig = """
|
||||
signupCaptcha:
|
||||
countryCodes:
|
||||
- 1
|
||||
""";
|
||||
|
||||
final DynamicSignupCaptchaConfiguration config =
|
||||
DynamicConfigurationManager.parseConfiguration(signupCaptchaConfig, DynamicConfiguration.class).orElseThrow()
|
||||
@@ -325,11 +331,12 @@ class DynamicConfigurationTest {
|
||||
}
|
||||
|
||||
{
|
||||
final String limitsConfig =
|
||||
"limits:\n"
|
||||
+ " unsealedSenderNumber:\n"
|
||||
+ " maxCardinality: 99\n"
|
||||
+ " ttl: PT23H";
|
||||
final String limitsConfig = """
|
||||
limits:
|
||||
unsealedSenderNumber:
|
||||
maxCardinality: 99
|
||||
ttl: PT23H
|
||||
""";
|
||||
|
||||
final CardinalityRateLimitConfiguration unsealedSenderNumber =
|
||||
DynamicConfigurationManager.parseConfiguration(limitsConfig, DynamicConfiguration.class).orElseThrow()
|
||||
@@ -353,13 +360,14 @@ class DynamicConfigurationTest {
|
||||
}
|
||||
|
||||
{
|
||||
final String rateLimitChallengeConfig =
|
||||
"rateLimitChallenge:\n"
|
||||
+ " preKeyLimitEnforced: true\n"
|
||||
+ " clientSupportedVersions:\n"
|
||||
+ " IOS: 5.1.0\n"
|
||||
+ " ANDROID: 5.2.0\n"
|
||||
+ " DESKTOP: 5.0.0";
|
||||
final String rateLimitChallengeConfig = """
|
||||
rateLimitChallenge:
|
||||
preKeyLimitEnforced: true
|
||||
clientSupportedVersions:
|
||||
IOS: 5.1.0
|
||||
ANDROID: 5.2.0
|
||||
DESKTOP: 5.0.0
|
||||
""";
|
||||
|
||||
DynamicRateLimitChallengeConfiguration rateLimitChallengeConfiguration =
|
||||
DynamicConfigurationManager.parseConfiguration(rateLimitChallengeConfig, DynamicConfiguration.class).orElseThrow()
|
||||
@@ -386,9 +394,10 @@ class DynamicConfigurationTest {
|
||||
}
|
||||
|
||||
{
|
||||
final String directoryReconcilerConfig =
|
||||
"directoryReconciler:\n"
|
||||
+ " enabled: false";
|
||||
final String directoryReconcilerConfig = """
|
||||
directoryReconciler:
|
||||
enabled: false
|
||||
""";
|
||||
|
||||
DynamicDirectoryReconcilerConfiguration directoryReconcilerConfiguration =
|
||||
DynamicConfigurationManager.parseConfiguration(directoryReconcilerConfig, DynamicConfiguration.class).orElseThrow()
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class AcceptNumericOnlineFlagRequestFilterTest {
|
||||
|
||||
@Path("/test")
|
||||
public static class TestResource {
|
||||
|
||||
@Path("/convert-boolean")
|
||||
@GET
|
||||
public boolean convertBoolean(@QueryParam("online") final boolean online) {
|
||||
return online;
|
||||
}
|
||||
|
||||
@Path("/no-conversion")
|
||||
@GET
|
||||
public boolean noConversion(@QueryParam("online") final boolean online) {
|
||||
return online;
|
||||
}
|
||||
}
|
||||
|
||||
private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder()
|
||||
.addProvider(new AcceptNumericOnlineFlagRequestFilter("test/convert-boolean"))
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new TestResource())
|
||||
.build();
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"/test/convert-boolean, true, true",
|
||||
"/test/convert-boolean, false, false",
|
||||
"/test/convert-boolean, 1, true",
|
||||
"/test/convert-boolean, 0, false",
|
||||
"/test/no-conversion, true, true",
|
||||
"/test/no-conversion, false, false",
|
||||
"/test/no-conversion, 1, false",
|
||||
"/test/no-conversion, 0, false"
|
||||
})
|
||||
void filter(final String path, final String value, final boolean expected) {
|
||||
final boolean response =
|
||||
RESOURCE_EXTENSION.getJerseyTest()
|
||||
.target(path)
|
||||
.queryParam("online", value)
|
||||
.request()
|
||||
.get(boolean.class);
|
||||
|
||||
assertEquals(expected, response);
|
||||
}
|
||||
}
|
||||
+46
-32
@@ -11,7 +11,7 @@ import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
@@ -56,10 +56,12 @@ class ChallengeControllerTest {
|
||||
|
||||
@Test
|
||||
void testHandlePushChallenge() throws RateLimitExceededException {
|
||||
final String pushChallengeJson = "{\n"
|
||||
+ " \"type\": \"rateLimitPushChallenge\",\n"
|
||||
+ " \"challenge\": \"Hello I am a push challenge token\"\n"
|
||||
+ "}";
|
||||
final String pushChallengeJson = """
|
||||
{
|
||||
"type": "rateLimitPushChallenge",
|
||||
"challenge": "Hello I am a push challenge token"
|
||||
}
|
||||
""";
|
||||
|
||||
final Response response = EXTENSION.target("/v1/challenge")
|
||||
.request()
|
||||
@@ -72,10 +74,12 @@ class ChallengeControllerTest {
|
||||
|
||||
@Test
|
||||
void testHandlePushChallengeRateLimited() throws RateLimitExceededException {
|
||||
final String pushChallengeJson = "{\n"
|
||||
+ " \"type\": \"rateLimitPushChallenge\",\n"
|
||||
+ " \"challenge\": \"Hello I am a push challenge token\"\n"
|
||||
+ "}";
|
||||
final String pushChallengeJson = """
|
||||
{
|
||||
"type": "rateLimitPushChallenge",
|
||||
"challenge": "Hello I am a push challenge token"
|
||||
}
|
||||
""";
|
||||
|
||||
final Duration retryAfter = Duration.ofMinutes(17);
|
||||
doThrow(new RateLimitExceededException(retryAfter)).when(rateLimitChallengeManager).answerPushChallenge(any(), any());
|
||||
@@ -91,11 +95,13 @@ class ChallengeControllerTest {
|
||||
|
||||
@Test
|
||||
void testHandleRecaptcha() throws RateLimitExceededException {
|
||||
final String recaptchaChallengeJson = "{\n"
|
||||
+ " \"type\": \"recaptcha\",\n"
|
||||
+ " \"token\": \"A server-generated token\",\n"
|
||||
+ " \"captcha\": \"The value of the solved captcha token\"\n"
|
||||
+ "}";
|
||||
final String recaptchaChallengeJson = """
|
||||
{
|
||||
"type": "recaptcha",
|
||||
"token": "A server-generated token",
|
||||
"captcha": "The value of the solved captcha token"
|
||||
}
|
||||
""";
|
||||
|
||||
final Response response = EXTENSION.target("/v1/challenge")
|
||||
.request()
|
||||
@@ -109,11 +115,13 @@ class ChallengeControllerTest {
|
||||
|
||||
@Test
|
||||
void testHandleRecaptchaRateLimited() throws RateLimitExceededException {
|
||||
final String recaptchaChallengeJson = "{\n"
|
||||
+ " \"type\": \"recaptcha\",\n"
|
||||
+ " \"token\": \"A server-generated token\",\n"
|
||||
+ " \"captcha\": \"The value of the solved captcha token\"\n"
|
||||
+ "}";
|
||||
final String recaptchaChallengeJson = """
|
||||
{
|
||||
"type": "recaptcha",
|
||||
"token": "A server-generated token",
|
||||
"captcha": "The value of the solved captcha token"
|
||||
}
|
||||
""";
|
||||
|
||||
final Duration retryAfter = Duration.ofMinutes(17);
|
||||
doThrow(new RateLimitExceededException(retryAfter)).when(rateLimitChallengeManager).answerRecaptchaChallenge(any(), any(), any());
|
||||
@@ -130,11 +138,13 @@ class ChallengeControllerTest {
|
||||
|
||||
@Test
|
||||
void testHandleRecaptchaNoForwardedFor() {
|
||||
final String recaptchaChallengeJson = "{\n"
|
||||
+ " \"type\": \"recaptcha\",\n"
|
||||
+ " \"token\": \"A server-generated token\",\n"
|
||||
+ " \"captcha\": \"The value of the solved captcha token\"\n"
|
||||
+ "}";
|
||||
final String recaptchaChallengeJson = """
|
||||
{
|
||||
"type": "recaptcha",
|
||||
"token": "A server-generated token",
|
||||
"captcha": "The value of the solved captcha token"
|
||||
}
|
||||
""";
|
||||
|
||||
final Response response = EXTENSION.target("/v1/challenge")
|
||||
.request()
|
||||
@@ -142,14 +152,16 @@ class ChallengeControllerTest {
|
||||
.put(Entity.json(recaptchaChallengeJson));
|
||||
|
||||
assertEquals(400, response.getStatus());
|
||||
verifyZeroInteractions(rateLimitChallengeManager);
|
||||
verifyNoInteractions(rateLimitChallengeManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleUnrecognizedAnswer() {
|
||||
final String unrecognizedJson = "{\n"
|
||||
+ " \"type\": \"unrecognized\"\n"
|
||||
+ "}";
|
||||
final String unrecognizedJson = """
|
||||
{
|
||||
"type": "unrecognized"
|
||||
}
|
||||
""";
|
||||
|
||||
final Response response = EXTENSION.target("/v1/challenge")
|
||||
.request()
|
||||
@@ -159,7 +171,7 @@ class ChallengeControllerTest {
|
||||
|
||||
assertEquals(400, response.getStatus());
|
||||
|
||||
verifyZeroInteractions(rateLimitChallengeManager);
|
||||
verifyNoInteractions(rateLimitChallengeManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -187,9 +199,11 @@ class ChallengeControllerTest {
|
||||
|
||||
@Test
|
||||
void testValidationError() {
|
||||
final String unrecognizedJson = "{\n"
|
||||
+ " \"type\": \"rateLimitPushChallenge\"\n"
|
||||
+ "}";
|
||||
final String unrecognizedJson = """
|
||||
{
|
||||
"type": "rateLimitPushChallenge"
|
||||
}
|
||||
""";
|
||||
|
||||
final Response response = EXTENSION.target("/v1/challenge")
|
||||
.request()
|
||||
|
||||
+68
-22
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
@@ -64,10 +64,6 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.controllers.StaleDevicesException;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
|
||||
@@ -98,6 +94,7 @@ class MessageControllerTest {
|
||||
|
||||
private static final String SINGLE_DEVICE_RECIPIENT = "+14151111111";
|
||||
private static final UUID SINGLE_DEVICE_UUID = UUID.randomUUID();
|
||||
private static final UUID SINGLE_DEVICE_PNI = UUID.randomUUID();
|
||||
|
||||
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
|
||||
private static final UUID MULTI_DEVICE_UUID = UUID.randomUUID();
|
||||
@@ -160,13 +157,14 @@ class MessageControllerTest {
|
||||
false, false, false)));
|
||||
}};
|
||||
|
||||
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, SINGLE_DEVICE_UUID, singleDeviceList, "1234".getBytes());
|
||||
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, MULTI_DEVICE_UUID, multiDeviceList, "1234".getBytes());
|
||||
internationalAccount = new Account(INTERNATIONAL_RECIPIENT, INTERNATIONAL_UUID, singleDeviceList, "1234".getBytes());
|
||||
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, SINGLE_DEVICE_UUID, SINGLE_DEVICE_PNI, singleDeviceList, "1234".getBytes());
|
||||
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, MULTI_DEVICE_UUID, UUID.randomUUID(), multiDeviceList, "1234".getBytes());
|
||||
internationalAccount = new Account(INTERNATIONAL_RECIPIENT, INTERNATIONAL_UUID, UUID.randomUUID(), singleDeviceList, "1234".getBytes());
|
||||
|
||||
when(accountsManager.get(eq(SINGLE_DEVICE_UUID))).thenReturn(Optional.of(singleDeviceAccount));
|
||||
when(accountsManager.get(eq(MULTI_DEVICE_UUID))).thenReturn(Optional.of(multiDeviceAccount));
|
||||
when(accountsManager.get(INTERNATIONAL_UUID)).thenReturn(Optional.of(internationalAccount));
|
||||
when(accountsManager.getByAccountIdentifier(eq(SINGLE_DEVICE_UUID))).thenReturn(Optional.of(singleDeviceAccount));
|
||||
when(accountsManager.getByPhoneNumberIdentifier(SINGLE_DEVICE_PNI)).thenReturn(Optional.of(singleDeviceAccount));
|
||||
when(accountsManager.getByAccountIdentifier(eq(MULTI_DEVICE_UUID))).thenReturn(Optional.of(multiDeviceAccount));
|
||||
when(accountsManager.getByAccountIdentifier(INTERNATIONAL_UUID)).thenReturn(Optional.of(internationalAccount));
|
||||
|
||||
when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter);
|
||||
}
|
||||
@@ -220,6 +218,57 @@ class MessageControllerTest {
|
||||
assertTrue(captor.getValue().hasSourceDevice());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void testSingleDeviceCurrentBadType(final String userAgentString, final boolean expectAcceptMessage) throws Exception {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/messages/%s", SINGLE_DEVICE_UUID))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.header("User-Agent", userAgentString)
|
||||
.put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device_bad_type.json"), IncomingMessageList.class),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
if (expectAcceptMessage) {
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
final ArgumentCaptor<Envelope> captor = ArgumentCaptor.forClass(Envelope.class);
|
||||
verify(messageSender).sendMessage(any(Account.class), any(Device.class), captor.capture(), eq(false));
|
||||
} else {
|
||||
assertEquals(400, response.getStatus());
|
||||
verify(messageSender, never()).sendMessage(any(), any(), any(), anyBoolean());
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> testSingleDeviceCurrentBadType() {
|
||||
return Stream.of(
|
||||
Arguments.of(String.format("Signal-iOS/%s iOS/14.2", MessageController.FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE), true),
|
||||
Arguments.of(String.format("Signal-iOS/%s iOS/14.2", MessageController.FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE.nextPatch()), true),
|
||||
Arguments.of(String.format("Signal-iOS/%s iOS/14.2", MessageController.IOS_VERSION_WITH_FIXED_ENVELOPE_TYPE.withIncMinor(-1)), true),
|
||||
Arguments.of(String.format("Signal-iOS/%s iOS/14.2", MessageController.IOS_VERSION_WITH_FIXED_ENVELOPE_TYPE), false)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingleDeviceCurrentByPni() throws Exception {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/messages/%s", SINGLE_DEVICE_PNI))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
|
||||
|
||||
ArgumentCaptor<Envelope> captor = ArgumentCaptor.forClass(Envelope.class);
|
||||
verify(messageSender, times(1)).sendMessage(any(Account.class), any(Device.class), captor.capture(), eq(false));
|
||||
|
||||
assertTrue(captor.getValue().hasSource());
|
||||
assertTrue(captor.getValue().hasSourceDevice());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullMessageInList() throws Exception {
|
||||
Response response =
|
||||
@@ -410,8 +459,8 @@ class MessageControllerTest {
|
||||
final UUID sourceUuid = UUID.randomUUID();
|
||||
|
||||
List<OutgoingMessageEntity> messages = new LinkedList<>() {{
|
||||
add(new OutgoingMessageEntity(1L, false, messageGuidOne, Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", sourceUuid, 2, "hi there".getBytes(), null, 0));
|
||||
add(new OutgoingMessageEntity(2L, false, null, Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE, null, timestampTwo, "+14152222222", sourceUuid, 2, null, null, 0));
|
||||
add(new OutgoingMessageEntity(messageGuidOne, Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", sourceUuid, 2, AuthHelper.VALID_UUID, "hi there".getBytes(), null, 0));
|
||||
add(new OutgoingMessageEntity(null, Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE, null, timestampTwo, "+14152222222", sourceUuid, 2, AuthHelper.VALID_UUID, null, null, 0));
|
||||
}};
|
||||
|
||||
OutgoingMessageEntityList messagesList = new OutgoingMessageEntityList(messages, false);
|
||||
@@ -428,9 +477,6 @@ class MessageControllerTest {
|
||||
|
||||
assertEquals(response.getMessages().size(), 2);
|
||||
|
||||
assertEquals(response.getMessages().get(0).getId(), 0);
|
||||
assertEquals(response.getMessages().get(1).getId(), 0);
|
||||
|
||||
assertEquals(response.getMessages().get(0).getTimestamp(), timestampOne);
|
||||
assertEquals(response.getMessages().get(1).getTimestamp(), timestampTwo);
|
||||
|
||||
@@ -447,8 +493,8 @@ class MessageControllerTest {
|
||||
final long timestampTwo = 313388;
|
||||
|
||||
List<OutgoingMessageEntity> messages = new LinkedList<OutgoingMessageEntity>() {{
|
||||
add(new OutgoingMessageEntity(1L, false, UUID.randomUUID(), Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", UUID.randomUUID(), 2, "hi there".getBytes(), null, 0));
|
||||
add(new OutgoingMessageEntity(2L, false, UUID.randomUUID(), Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE, null, timestampTwo, "+14152222222", UUID.randomUUID(), 2, null, null, 0));
|
||||
add(new OutgoingMessageEntity(UUID.randomUUID(), Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", UUID.randomUUID(), 2, AuthHelper.VALID_UUID, "hi there".getBytes(), null, 0));
|
||||
add(new OutgoingMessageEntity(UUID.randomUUID(), Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE, null, timestampTwo, "+14152222222", UUID.randomUUID(), 2, AuthHelper.VALID_UUID, null, null, 0));
|
||||
}};
|
||||
|
||||
OutgoingMessageEntityList messagesList = new OutgoingMessageEntityList(messages, false);
|
||||
@@ -473,13 +519,13 @@ class MessageControllerTest {
|
||||
|
||||
UUID uuid1 = UUID.randomUUID();
|
||||
when(messagesManager.delete(AuthHelper.VALID_UUID, 1, uuid1)).thenReturn(Optional.of(new OutgoingMessageEntity(
|
||||
31337L, true, uuid1, Envelope.Type.CIPHERTEXT_VALUE,
|
||||
null, timestamp, "+14152222222", sourceUuid, 1, "hi".getBytes(), null, 0)));
|
||||
uuid1, Envelope.Type.CIPHERTEXT_VALUE,
|
||||
null, timestamp, "+14152222222", sourceUuid, 1, AuthHelper.VALID_UUID, "hi".getBytes(), null, 0)));
|
||||
|
||||
UUID uuid2 = UUID.randomUUID();
|
||||
when(messagesManager.delete(AuthHelper.VALID_UUID, 1, uuid2)).thenReturn(Optional.of(new OutgoingMessageEntity(
|
||||
31337L, true, uuid2, Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE,
|
||||
null, System.currentTimeMillis(), "+14152222222", sourceUuid, 1, null, null, 0)));
|
||||
uuid2, Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE,
|
||||
null, System.currentTimeMillis(), "+14152222222", sourceUuid, 1, AuthHelper.VALID_UUID, null, null, 0)));
|
||||
|
||||
|
||||
UUID uuid3 = UUID.randomUUID();
|
||||
+3
-3
@@ -82,11 +82,11 @@ class SubscriptionControllerTest {
|
||||
3L, new SubscriptionLevelConfiguration("B3", "P3", Map.of("USD", new SubscriptionPriceConfiguration("R3", BigDecimal.valueOf(300))))
|
||||
));
|
||||
when(BADGE_TRANSLATOR.translate(any(), eq("B1"))).thenReturn(new Badge("B1", "cat1", "name1", "desc1",
|
||||
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt"))));
|
||||
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
|
||||
when(BADGE_TRANSLATOR.translate(any(), eq("B2"))).thenReturn(new Badge("B2", "cat2", "name2", "desc2",
|
||||
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt"))));
|
||||
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
|
||||
when(BADGE_TRANSLATOR.translate(any(), eq("B3"))).thenReturn(new Badge("B3", "cat3", "name3", "desc3",
|
||||
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt"))));
|
||||
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
|
||||
when(LEVEL_TRANSLATOR.translate(any(), eq("B1"))).thenReturn("Z1");
|
||||
when(LEVEL_TRANSLATOR.translate(any(), eq("B2"))).thenReturn("Z2");
|
||||
when(LEVEL_TRANSLATOR.translate(any(), eq("B3"))).thenReturn("Z3");
|
||||
|
||||
+20
-14
@@ -17,10 +17,12 @@ class AnswerChallengeRequestTest {
|
||||
@Test
|
||||
void parse() throws JsonProcessingException {
|
||||
{
|
||||
final String pushChallengeJson = "{\n"
|
||||
+ " \"type\": \"rateLimitPushChallenge\",\n"
|
||||
+ " \"challenge\": \"Hello I am a push challenge token\"\n"
|
||||
+ "}";
|
||||
final String pushChallengeJson = """
|
||||
{
|
||||
"type": "rateLimitPushChallenge",
|
||||
"challenge": "Hello I am a push challenge token"
|
||||
}
|
||||
""";
|
||||
|
||||
final AnswerChallengeRequest answerChallengeRequest =
|
||||
SystemMapper.getMapper().readValue(pushChallengeJson, AnswerChallengeRequest.class);
|
||||
@@ -31,11 +33,13 @@ class AnswerChallengeRequestTest {
|
||||
}
|
||||
|
||||
{
|
||||
final String recaptchaChallengeJson = "{\n"
|
||||
+ " \"type\": \"recaptcha\",\n"
|
||||
+ " \"token\": \"A server-generated token\",\n"
|
||||
+ " \"captcha\": \"The value of the solved captcha token\"\n"
|
||||
+ "}";
|
||||
final String recaptchaChallengeJson = """
|
||||
{
|
||||
"type": "recaptcha",
|
||||
"token": "A server-generated token",
|
||||
"captcha": "The value of the solved captcha token"
|
||||
}
|
||||
""";
|
||||
|
||||
final AnswerChallengeRequest answerChallengeRequest =
|
||||
SystemMapper.getMapper().readValue(recaptchaChallengeJson, AnswerChallengeRequest.class);
|
||||
@@ -50,11 +54,13 @@ class AnswerChallengeRequestTest {
|
||||
}
|
||||
|
||||
{
|
||||
final String unrecognizedTypeJson = "{\n"
|
||||
+ " \"type\": \"unrecognized\",\n"
|
||||
+ " \"token\": \"A server-generated token\",\n"
|
||||
+ " \"captcha\": \"The value of the solved captcha token\"\n"
|
||||
+ "}";
|
||||
final String unrecognizedTypeJson = """
|
||||
{
|
||||
"type": "unrecognized",
|
||||
"token": "A server-generated token",
|
||||
"captcha": "The value of the solved captcha token"
|
||||
}
|
||||
""";
|
||||
|
||||
assertThrows(InvalidTypeIdException.class,
|
||||
() -> SystemMapper.getMapper().readValue(unrecognizedTypeJson, AnswerChallengeRequest.class));
|
||||
|
||||
+55
-53
@@ -18,59 +18,61 @@ import static org.junit.Assert.*;
|
||||
public class OperatingSystemMemoryGaugeTest {
|
||||
|
||||
private static final String MEMINFO =
|
||||
"MemTotal: 16052208 kB\n" +
|
||||
"MemFree: 4568468 kB\n" +
|
||||
"MemAvailable: 7702848 kB\n" +
|
||||
"Buffers: 636372 kB\n" +
|
||||
"Cached: 5019116 kB\n" +
|
||||
"SwapCached: 6692 kB\n" +
|
||||
"Active: 7746436 kB\n" +
|
||||
"Inactive: 2729876 kB\n" +
|
||||
"Active(anon): 5580980 kB\n" +
|
||||
"Inactive(anon): 1648108 kB\n" +
|
||||
"Active(file): 2165456 kB\n" +
|
||||
"Inactive(file): 1081768 kB\n" +
|
||||
"Unevictable: 443948 kB\n" +
|
||||
"Mlocked: 4924 kB\n" +
|
||||
"SwapTotal: 1003516 kB\n" +
|
||||
"SwapFree: 935932 kB\n" +
|
||||
"Dirty: 28308 kB\n" +
|
||||
"Writeback: 0 kB\n" +
|
||||
"AnonPages: 5258396 kB\n" +
|
||||
"Mapped: 1530740 kB\n" +
|
||||
"Shmem: 2419340 kB\n" +
|
||||
"KReclaimable: 229392 kB\n" +
|
||||
"Slab: 408156 kB\n" +
|
||||
"SReclaimable: 229392 kB\n" +
|
||||
"SUnreclaim: 178764 kB\n" +
|
||||
"KernelStack: 17360 kB\n" +
|
||||
"PageTables: 50436 kB\n" +
|
||||
"NFS_Unstable: 0 kB\n" +
|
||||
"Bounce: 0 kB\n" +
|
||||
"WritebackTmp: 0 kB\n" +
|
||||
"CommitLimit: 9029620 kB\n" +
|
||||
"Committed_AS: 16681884 kB\n" +
|
||||
"VmallocTotal: 34359738367 kB\n" +
|
||||
"VmallocUsed: 41944 kB\n" +
|
||||
"VmallocChunk: 0 kB\n" +
|
||||
"Percpu: 4240 kB\n" +
|
||||
"HardwareCorrupted: 0 kB\n" +
|
||||
"AnonHugePages: 0 kB\n" +
|
||||
"ShmemHugePages: 0 kB\n" +
|
||||
"ShmemPmdMapped: 0 kB\n" +
|
||||
"FileHugePages: 0 kB\n" +
|
||||
"FilePmdMapped: 0 kB\n" +
|
||||
"CmaTotal: 0 kB\n" +
|
||||
"CmaFree: 0 kB\n" +
|
||||
"HugePages_Total: 0\n" +
|
||||
"HugePages_Free: 7\n" +
|
||||
"HugePages_Rsvd: 0\n" +
|
||||
"HugePages_Surp: 0\n" +
|
||||
"Hugepagesize: 2048 kB\n" +
|
||||
"Hugetlb: 0 kB\n" +
|
||||
"DirectMap4k: 481804 kB\n" +
|
||||
"DirectMap2M: 14901248 kB\n" +
|
||||
"DirectMap1G: 2097152 kB\n";
|
||||
"""
|
||||
MemTotal: 16052208 kB
|
||||
MemFree: 4568468 kB
|
||||
MemAvailable: 7702848 kB
|
||||
Buffers: 636372 kB
|
||||
Cached: 5019116 kB
|
||||
SwapCached: 6692 kB
|
||||
Active: 7746436 kB
|
||||
Inactive: 2729876 kB
|
||||
Active(anon): 5580980 kB
|
||||
Inactive(anon): 1648108 kB
|
||||
Active(file): 2165456 kB
|
||||
Inactive(file): 1081768 kB
|
||||
Unevictable: 443948 kB
|
||||
Mlocked: 4924 kB
|
||||
SwapTotal: 1003516 kB
|
||||
SwapFree: 935932 kB
|
||||
Dirty: 28308 kB
|
||||
Writeback: 0 kB
|
||||
AnonPages: 5258396 kB
|
||||
Mapped: 1530740 kB
|
||||
Shmem: 2419340 kB
|
||||
KReclaimable: 229392 kB
|
||||
Slab: 408156 kB
|
||||
SReclaimable: 229392 kB
|
||||
SUnreclaim: 178764 kB
|
||||
KernelStack: 17360 kB
|
||||
PageTables: 50436 kB
|
||||
NFS_Unstable: 0 kB
|
||||
Bounce: 0 kB
|
||||
WritebackTmp: 0 kB
|
||||
CommitLimit: 9029620 kB
|
||||
Committed_AS: 16681884 kB
|
||||
VmallocTotal: 34359738367 kB
|
||||
VmallocUsed: 41944 kB
|
||||
VmallocChunk: 0 kB
|
||||
Percpu: 4240 kB
|
||||
HardwareCorrupted: 0 kB
|
||||
AnonHugePages: 0 kB
|
||||
ShmemHugePages: 0 kB
|
||||
ShmemPmdMapped: 0 kB
|
||||
FileHugePages: 0 kB
|
||||
FilePmdMapped: 0 kB
|
||||
CmaTotal: 0 kB
|
||||
CmaFree: 0 kB
|
||||
HugePages_Total: 0
|
||||
HugePages_Free: 7
|
||||
HugePages_Rsvd: 0
|
||||
HugePages_Surp: 0
|
||||
Hugepagesize: 2048 kB
|
||||
Hugetlb: 0 kB
|
||||
DirectMap4k: 481804 kB
|
||||
DirectMap2M: 14901248 kB
|
||||
DirectMap1G: 2097152 kB
|
||||
""";
|
||||
|
||||
@Test
|
||||
@Parameters(method = "argumentsForTestGetValue")
|
||||
|
||||
+45
-15
@@ -6,38 +6,68 @@
|
||||
package org.whispersystems.textsecuregcm.metrics;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicPushLatencyConfiguration;
|
||||
import org.whispersystems.textsecuregcm.metrics.PushLatencyManager.PushRecord;
|
||||
import org.whispersystems.textsecuregcm.metrics.PushLatencyManager.PushType;
|
||||
import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
|
||||
class PushLatencyManagerTest {
|
||||
|
||||
@RegisterExtension
|
||||
static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build();
|
||||
|
||||
@Test
|
||||
void testGetLatency() throws ExecutionException, InterruptedException {
|
||||
private DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
|
||||
final PushLatencyManager pushLatencyManager = new PushLatencyManager(REDIS_CLUSTER_EXTENSION.getRedisCluster());
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
//noinspection unchecked
|
||||
dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
|
||||
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
|
||||
final DynamicPushLatencyConfiguration dynamicPushLatencyConfiguration = mock(DynamicPushLatencyConfiguration.class);
|
||||
|
||||
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
|
||||
when(dynamicConfiguration.getPushLatencyConfiguration()).thenReturn(dynamicPushLatencyConfiguration);
|
||||
when(dynamicPushLatencyConfiguration.getInstrumentedVersions()).thenReturn(Collections.emptyMap());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
void testTakeRecord(final boolean isVoip) throws ExecutionException, InterruptedException {
|
||||
final UUID accountUuid = UUID.randomUUID();
|
||||
final long deviceId = 1;
|
||||
final long expectedLatency = 1234;
|
||||
final long pushSentTimestamp = System.currentTimeMillis();
|
||||
final long clearQueueTimestamp = pushSentTimestamp + expectedLatency;
|
||||
|
||||
assertNull(pushLatencyManager.getLatencyAndClearTimestamp(accountUuid, deviceId, System.currentTimeMillis()).get());
|
||||
final Instant pushTimestamp = Instant.now();
|
||||
|
||||
{
|
||||
pushLatencyManager.recordPushSent(accountUuid, deviceId, pushSentTimestamp);
|
||||
final PushLatencyManager pushLatencyManager = new PushLatencyManager(REDIS_CLUSTER_EXTENSION.getRedisCluster(),
|
||||
dynamicConfigurationManager, Clock.fixed(pushTimestamp, ZoneId.systemDefault()));
|
||||
|
||||
assertEquals(expectedLatency,
|
||||
(long) pushLatencyManager.getLatencyAndClearTimestamp(accountUuid, deviceId, clearQueueTimestamp).get());
|
||||
assertNull(
|
||||
pushLatencyManager.getLatencyAndClearTimestamp(accountUuid, deviceId, System.currentTimeMillis()).get());
|
||||
}
|
||||
assertNull(pushLatencyManager.takePushRecord(accountUuid, deviceId).get());
|
||||
|
||||
pushLatencyManager.recordPushSent(accountUuid, deviceId, isVoip);
|
||||
|
||||
final PushRecord pushRecord = pushLatencyManager.takePushRecord(accountUuid, deviceId).get();
|
||||
|
||||
assertNotNull(pushRecord);
|
||||
assertEquals(pushTimestamp, pushRecord.getTimestamp());
|
||||
assertEquals(isVoip ? PushType.VOIP : PushType.STANDARD, pushRecord.getPushType());
|
||||
|
||||
assertNull(pushLatencyManager.takePushRecord(accountUuid, deviceId).get());
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -57,8 +57,8 @@ class ApnFallbackManagerTest {
|
||||
when(account.getDevice(DEVICE_ID)).thenReturn(Optional.of(device));
|
||||
|
||||
final AccountsManager accountsManager = mock(AccountsManager.class);
|
||||
when(accountsManager.get(ACCOUNT_NUMBER)).thenReturn(Optional.of(account));
|
||||
when(accountsManager.get(ACCOUNT_UUID)).thenReturn(Optional.of(account));
|
||||
when(accountsManager.getByE164(ACCOUNT_NUMBER)).thenReturn(Optional.of(account));
|
||||
when(accountsManager.getByAccountIdentifier(ACCOUNT_UUID)).thenReturn(Optional.of(account));
|
||||
|
||||
apnSender = mock(APNSender.class);
|
||||
|
||||
|
||||
+17
-16
@@ -55,22 +55,23 @@ public class SecureBackupClientTest {
|
||||
config.setUri("http://localhost:" + wireMockRule.port());
|
||||
|
||||
// This is a randomly-generated, throwaway certificate that's not actually connected to anything
|
||||
config.setBackupCaCertificate(
|
||||
"-----BEGIN CERTIFICATE-----\n" +
|
||||
"MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEL\n" +
|
||||
"MAkGA1UECAwCVVMxHjAcBgNVBAoMFVNpZ25hbCBNZXNzZW5nZXIsIExMQzETMBEG\n" +
|
||||
"A1UEAwwKc2lnbmFsLm9yZzAeFw0yMDEyMjMyMjQ3NTlaFw0zMDEyMjEyMjQ3NTla\n" +
|
||||
"ME8xCzAJBgNVBAYTAnVzMQswCQYDVQQIDAJVUzEeMBwGA1UECgwVU2lnbmFsIE1l\n" +
|
||||
"c3NlbmdlciwgTExDMRMwEQYDVQQDDApzaWduYWwub3JnMIGfMA0GCSqGSIb3DQEB\n" +
|
||||
"AQUAA4GNADCBiQKBgQCfSLcZNHYqbxSsgWp4JvbPRHjQTrlsrKrgD2q7f/OY6O3Y\n" +
|
||||
"/X0QNcNSOJpliN8rmzwslfsrXHO3q1diGRw4xHogUJZ/7NQrHiP/zhN0VTDh49pD\n" +
|
||||
"ZpjXVyUbayLS/6qM5arKxBspzEFBb5v8cF6bPr76SO/rpGXiI0j6yJKX6fRiKwID\n" +
|
||||
"AQABo1AwTjAdBgNVHQ4EFgQU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwHwYDVR0jBBgw\n" +
|
||||
"FoAU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B\n" +
|
||||
"AQ0FAAOBgQB+5d5+NtzLILfrc9QmJdIO1YeDP64JmFwTER0kEUouRsb9UwknVWZa\n" +
|
||||
"y7MTM4NoBV1k0zb5LAk89SIDPr/maW5AsLtEomzjnEiomjoMBUdNe3YCgQReoLnr\n" +
|
||||
"R/QaUNbrCjTGYfBsjGbIzmkWPUyTec2ZdRyJ8JiVl386+6CZkxnndQ==\n" +
|
||||
"-----END CERTIFICATE-----");
|
||||
config.setBackupCaCertificate("""
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEL
|
||||
MAkGA1UECAwCVVMxHjAcBgNVBAoMFVNpZ25hbCBNZXNzZW5nZXIsIExMQzETMBEG
|
||||
A1UEAwwKc2lnbmFsLm9yZzAeFw0yMDEyMjMyMjQ3NTlaFw0zMDEyMjEyMjQ3NTla
|
||||
ME8xCzAJBgNVBAYTAnVzMQswCQYDVQQIDAJVUzEeMBwGA1UECgwVU2lnbmFsIE1l
|
||||
c3NlbmdlciwgTExDMRMwEQYDVQQDDApzaWduYWwub3JnMIGfMA0GCSqGSIb3DQEB
|
||||
AQUAA4GNADCBiQKBgQCfSLcZNHYqbxSsgWp4JvbPRHjQTrlsrKrgD2q7f/OY6O3Y
|
||||
/X0QNcNSOJpliN8rmzwslfsrXHO3q1diGRw4xHogUJZ/7NQrHiP/zhN0VTDh49pD
|
||||
ZpjXVyUbayLS/6qM5arKxBspzEFBb5v8cF6bPr76SO/rpGXiI0j6yJKX6fRiKwID
|
||||
AQABo1AwTjAdBgNVHQ4EFgQU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwHwYDVR0jBBgw
|
||||
FoAU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B
|
||||
AQ0FAAOBgQB+5d5+NtzLILfrc9QmJdIO1YeDP64JmFwTER0kEUouRsb9UwknVWZa
|
||||
y7MTM4NoBV1k0zb5LAk89SIDPr/maW5AsLtEomzjnEiomjoMBUdNe3YCgQReoLnr
|
||||
R/QaUNbrCjTGYfBsjGbIzmkWPUyTec2ZdRyJ8JiVl386+6CZkxnndQ==
|
||||
-----END CERTIFICATE-----
|
||||
""");
|
||||
|
||||
secureStorageClient = new SecureBackupClient(credentialGenerator, httpExecutor, config);
|
||||
}
|
||||
|
||||
+17
-16
@@ -55,22 +55,23 @@ public class SecureStorageClientTest {
|
||||
config.setUri("http://localhost:" + wireMockRule.port());
|
||||
|
||||
// This is a randomly-generated, throwaway certificate that's not actually connected to anything
|
||||
config.setStorageCaCertificate(
|
||||
"-----BEGIN CERTIFICATE-----\n" +
|
||||
"MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEL\n" +
|
||||
"MAkGA1UECAwCVVMxHjAcBgNVBAoMFVNpZ25hbCBNZXNzZW5nZXIsIExMQzETMBEG\n" +
|
||||
"A1UEAwwKc2lnbmFsLm9yZzAeFw0yMDEyMjMyMjQ3NTlaFw0zMDEyMjEyMjQ3NTla\n" +
|
||||
"ME8xCzAJBgNVBAYTAnVzMQswCQYDVQQIDAJVUzEeMBwGA1UECgwVU2lnbmFsIE1l\n" +
|
||||
"c3NlbmdlciwgTExDMRMwEQYDVQQDDApzaWduYWwub3JnMIGfMA0GCSqGSIb3DQEB\n" +
|
||||
"AQUAA4GNADCBiQKBgQCfSLcZNHYqbxSsgWp4JvbPRHjQTrlsrKrgD2q7f/OY6O3Y\n" +
|
||||
"/X0QNcNSOJpliN8rmzwslfsrXHO3q1diGRw4xHogUJZ/7NQrHiP/zhN0VTDh49pD\n" +
|
||||
"ZpjXVyUbayLS/6qM5arKxBspzEFBb5v8cF6bPr76SO/rpGXiI0j6yJKX6fRiKwID\n" +
|
||||
"AQABo1AwTjAdBgNVHQ4EFgQU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwHwYDVR0jBBgw\n" +
|
||||
"FoAU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B\n" +
|
||||
"AQ0FAAOBgQB+5d5+NtzLILfrc9QmJdIO1YeDP64JmFwTER0kEUouRsb9UwknVWZa\n" +
|
||||
"y7MTM4NoBV1k0zb5LAk89SIDPr/maW5AsLtEomzjnEiomjoMBUdNe3YCgQReoLnr\n" +
|
||||
"R/QaUNbrCjTGYfBsjGbIzmkWPUyTec2ZdRyJ8JiVl386+6CZkxnndQ==\n" +
|
||||
"-----END CERTIFICATE-----");
|
||||
config.setStorageCaCertificate("""
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEL
|
||||
MAkGA1UECAwCVVMxHjAcBgNVBAoMFVNpZ25hbCBNZXNzZW5nZXIsIExMQzETMBEG
|
||||
A1UEAwwKc2lnbmFsLm9yZzAeFw0yMDEyMjMyMjQ3NTlaFw0zMDEyMjEyMjQ3NTla
|
||||
ME8xCzAJBgNVBAYTAnVzMQswCQYDVQQIDAJVUzEeMBwGA1UECgwVU2lnbmFsIE1l
|
||||
c3NlbmdlciwgTExDMRMwEQYDVQQDDApzaWduYWwub3JnMIGfMA0GCSqGSIb3DQEB
|
||||
AQUAA4GNADCBiQKBgQCfSLcZNHYqbxSsgWp4JvbPRHjQTrlsrKrgD2q7f/OY6O3Y
|
||||
/X0QNcNSOJpliN8rmzwslfsrXHO3q1diGRw4xHogUJZ/7NQrHiP/zhN0VTDh49pD
|
||||
ZpjXVyUbayLS/6qM5arKxBspzEFBb5v8cF6bPr76SO/rpGXiI0j6yJKX6fRiKwID
|
||||
AQABo1AwTjAdBgNVHQ4EFgQU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwHwYDVR0jBBgw
|
||||
FoAU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B
|
||||
AQ0FAAOBgQB+5d5+NtzLILfrc9QmJdIO1YeDP64JmFwTER0kEUouRsb9UwknVWZa
|
||||
y7MTM4NoBV1k0zb5LAk89SIDPr/maW5AsLtEomzjnEiomjoMBUdNe3YCgQReoLnr
|
||||
R/QaUNbrCjTGYfBsjGbIzmkWPUyTec2ZdRyJ8JiVl386+6CZkxnndQ==
|
||||
-----END CERTIFICATE-----
|
||||
""");
|
||||
|
||||
secureStorageClient = new SecureStorageClient(credentialGenerator, httpExecutor, config);
|
||||
}
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ public class AccountDatabaseCrawlerIntegrationTest extends AbstractRedisClusterT
|
||||
.thenReturn(new AccountCrawlChunk(List.of(secondAccount), SECOND_UUID))
|
||||
.thenReturn(new AccountCrawlChunk(Collections.emptyList(), null));
|
||||
|
||||
final AccountDatabaseCrawlerCache crawlerCache = new AccountDatabaseCrawlerCache(getRedisCluster());
|
||||
final AccountDatabaseCrawlerCache crawlerCache = new AccountDatabaseCrawlerCache(getRedisCluster(), "test");
|
||||
accountDatabaseCrawler = new AccountDatabaseCrawler(accountsManager, crawlerCache, List.of(listener), CHUNK_SIZE,
|
||||
CHUNK_INTERVAL_MS);
|
||||
}
|
||||
|
||||
+71
-24
@@ -6,6 +6,7 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -15,6 +16,8 @@ import static org.mockito.Mockito.when;
|
||||
import com.opentable.db.postgres.embedded.LiquibasePreparer;
|
||||
import com.opentable.db.postgres.junit5.EmbeddedPostgresExtension;
|
||||
import com.opentable.db.postgres.junit5.PreparedDbExtension;
|
||||
import java.time.Clock;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -47,6 +50,8 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
|
||||
private static final String ACCOUNTS_TABLE_NAME = "accounts_test";
|
||||
private static final String NUMBERS_TABLE_NAME = "numbers_test";
|
||||
private static final String PNI_ASSIGNMENT_TABLE_NAME = "pni_assignment_test";
|
||||
private static final String PNI_TABLE_NAME = "pni_test";
|
||||
private static final String NEEDS_RECONCILIATION_INDEX_NAME = "needs_reconciliation_test";
|
||||
private static final String DELETED_ACCOUNTS_LOCK_TABLE_NAME = "deleted_accounts_lock_test";
|
||||
private static final int SCAN_PAGE_SIZE = 1;
|
||||
@@ -90,6 +95,16 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
.attributeType(ScalarAttributeType.S).build())
|
||||
.build();
|
||||
|
||||
@RegisterExtension
|
||||
static DynamoDbExtension PNI_DYNAMO_EXTENSION = DynamoDbExtension.builder()
|
||||
.tableName(PNI_TABLE_NAME)
|
||||
.hashKey(PhoneNumberIdentifiers.KEY_E164)
|
||||
.attributeDefinition(AttributeDefinition.builder()
|
||||
.attributeName(PhoneNumberIdentifiers.KEY_E164)
|
||||
.attributeType(ScalarAttributeType.S)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
@RegisterExtension
|
||||
static RedisClusterExtension CACHE_CLUSTER_EXTENSION = RedisClusterExtension.builder().build();
|
||||
|
||||
@@ -118,14 +133,33 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().createTable(createNumbersTableRequest);
|
||||
}
|
||||
|
||||
{
|
||||
CreateTableRequest createPhoneNumberIdentifierTableRequest = CreateTableRequest.builder()
|
||||
.tableName(PNI_ASSIGNMENT_TABLE_NAME)
|
||||
.keySchema(KeySchemaElement.builder()
|
||||
.attributeName(Accounts.ATTR_PNI_UUID)
|
||||
.keyType(KeyType.HASH)
|
||||
.build())
|
||||
.attributeDefinitions(AttributeDefinition.builder()
|
||||
.attributeName(Accounts.ATTR_PNI_UUID)
|
||||
.attributeType(ScalarAttributeType.B)
|
||||
.build())
|
||||
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
|
||||
.build();
|
||||
|
||||
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().createTable(createPhoneNumberIdentifierTableRequest);
|
||||
}
|
||||
|
||||
final Accounts accounts = new Accounts(
|
||||
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient(),
|
||||
ACCOUNTS_DYNAMO_EXTENSION.getTableName(),
|
||||
NUMBERS_TABLE_NAME,
|
||||
PNI_ASSIGNMENT_TABLE_NAME,
|
||||
SCAN_PAGE_SIZE);
|
||||
|
||||
{
|
||||
final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
|
||||
@SuppressWarnings("unchecked") final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager =
|
||||
mock(DynamicConfigurationManager.class);
|
||||
|
||||
DynamicConfiguration dynamicConfiguration = new DynamicConfiguration();
|
||||
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
|
||||
@@ -146,19 +180,24 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
|
||||
clientPresenceManager = mock(ClientPresenceManager.class);
|
||||
|
||||
final PhoneNumberIdentifiers phoneNumberIdentifiers =
|
||||
new PhoneNumberIdentifiers(PNI_DYNAMO_EXTENSION.getDynamoDbClient(), PNI_TABLE_NAME);
|
||||
|
||||
accountsManager = new AccountsManager(
|
||||
accounts,
|
||||
phoneNumberIdentifiers,
|
||||
CACHE_CLUSTER_EXTENSION.getRedisCluster(),
|
||||
deletedAccountsManager,
|
||||
mock(DirectoryQueue.class),
|
||||
mock(KeysDynamoDb.class),
|
||||
mock(Keys.class),
|
||||
mock(MessagesManager.class),
|
||||
mock(UsernamesManager.class),
|
||||
mock(ProfilesManager.class),
|
||||
mock(StoredVerificationCodeManager.class),
|
||||
secureStorageClient,
|
||||
secureBackupClient,
|
||||
clientPresenceManager);
|
||||
clientPresenceManager,
|
||||
mock(Clock.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,17 +206,19 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
final String originalNumber = "+18005551111";
|
||||
final String secondNumber = "+18005552222";
|
||||
|
||||
final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes());
|
||||
final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>());
|
||||
final UUID originalUuid = account.getUuid();
|
||||
final UUID originalPni = account.getPhoneNumberIdentifier();
|
||||
|
||||
accountsManager.changeNumber(account, secondNumber);
|
||||
|
||||
assertTrue(accountsManager.get(originalNumber).isEmpty());
|
||||
assertTrue(accountsManager.getByE164(originalNumber).isEmpty());
|
||||
|
||||
assertTrue(accountsManager.get(secondNumber).isPresent());
|
||||
assertEquals(Optional.of(originalUuid), accountsManager.get(secondNumber).map(Account::getUuid));
|
||||
assertTrue(accountsManager.getByE164(secondNumber).isPresent());
|
||||
assertEquals(originalUuid, accountsManager.getByE164(secondNumber).map(Account::getUuid).orElseThrow());
|
||||
assertNotEquals(originalPni, accountsManager.getByE164(secondNumber).map(Account::getPhoneNumberIdentifier).orElseThrow());
|
||||
|
||||
assertEquals(secondNumber, accountsManager.get(originalUuid).map(Account::getNumber).orElseThrow());
|
||||
assertEquals(secondNumber, accountsManager.getByAccountIdentifier(originalUuid).map(Account::getNumber).orElseThrow());
|
||||
|
||||
assertEquals(Optional.empty(), deletedAccounts.findUuid(originalNumber));
|
||||
assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber));
|
||||
@@ -188,18 +229,20 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
final String originalNumber = "+18005551111";
|
||||
final String secondNumber = "+18005552222";
|
||||
|
||||
Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes());
|
||||
Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>());
|
||||
final UUID originalUuid = account.getUuid();
|
||||
final UUID originalPni = account.getPhoneNumberIdentifier();
|
||||
|
||||
account = accountsManager.changeNumber(account, secondNumber);
|
||||
accountsManager.changeNumber(account, originalNumber);
|
||||
|
||||
assertTrue(accountsManager.get(originalNumber).isPresent());
|
||||
assertEquals(Optional.of(originalUuid), accountsManager.get(originalNumber).map(Account::getUuid));
|
||||
assertTrue(accountsManager.getByE164(originalNumber).isPresent());
|
||||
assertEquals(originalUuid, accountsManager.getByE164(originalNumber).map(Account::getUuid).orElseThrow());
|
||||
assertEquals(originalPni, accountsManager.getByE164(originalNumber).map(Account::getPhoneNumberIdentifier).orElseThrow());
|
||||
|
||||
assertTrue(accountsManager.get(secondNumber).isEmpty());
|
||||
assertTrue(accountsManager.getByE164(secondNumber).isEmpty());
|
||||
|
||||
assertEquals(originalNumber, accountsManager.get(originalUuid).map(Account::getNumber).orElseThrow());
|
||||
assertEquals(originalNumber, accountsManager.getByAccountIdentifier(originalUuid).map(Account::getNumber).orElseThrow());
|
||||
|
||||
assertEquals(Optional.empty(), deletedAccounts.findUuid(originalNumber));
|
||||
assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber));
|
||||
@@ -210,22 +253,22 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
final String originalNumber = "+18005551111";
|
||||
final String secondNumber = "+18005552222";
|
||||
|
||||
final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes());
|
||||
final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>());
|
||||
final UUID originalUuid = account.getUuid();
|
||||
|
||||
final Account existingAccount = accountsManager.create(secondNumber, "password", null, new AccountAttributes());
|
||||
final Account existingAccount = accountsManager.create(secondNumber, "password", null, new AccountAttributes(), new ArrayList<>());
|
||||
final UUID existingAccountUuid = existingAccount.getUuid();
|
||||
|
||||
accountsManager.update(existingAccount, a -> a.addDevice(new Device(Device.MASTER_ID, "test", "token", "salt", null, null, null, true, 1, null, 0, 0, null, 0, new DeviceCapabilities())));
|
||||
|
||||
accountsManager.changeNumber(account, secondNumber);
|
||||
|
||||
assertTrue(accountsManager.get(originalNumber).isEmpty());
|
||||
assertTrue(accountsManager.getByE164(originalNumber).isEmpty());
|
||||
|
||||
assertTrue(accountsManager.get(secondNumber).isPresent());
|
||||
assertEquals(Optional.of(originalUuid), accountsManager.get(secondNumber).map(Account::getUuid));
|
||||
assertTrue(accountsManager.getByE164(secondNumber).isPresent());
|
||||
assertEquals(Optional.of(originalUuid), accountsManager.getByE164(secondNumber).map(Account::getUuid));
|
||||
|
||||
assertEquals(secondNumber, accountsManager.get(originalUuid).map(Account::getNumber).orElseThrow());
|
||||
assertEquals(secondNumber, accountsManager.getByAccountIdentifier(originalUuid).map(Account::getNumber).orElseThrow());
|
||||
|
||||
verify(clientPresenceManager).displacePresence(existingAccountUuid, Device.MASTER_ID);
|
||||
|
||||
@@ -238,24 +281,28 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
final String originalNumber = "+18005551111";
|
||||
final String secondNumber = "+18005552222";
|
||||
|
||||
final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes());
|
||||
final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>());
|
||||
final UUID originalUuid = account.getUuid();
|
||||
final UUID originalPni = account.getPhoneNumberIdentifier();
|
||||
|
||||
final Account existingAccount = accountsManager.create(secondNumber, "password", null, new AccountAttributes());
|
||||
final Account existingAccount = accountsManager.create(secondNumber, "password", null, new AccountAttributes(), new ArrayList<>());
|
||||
final UUID existingAccountUuid = existingAccount.getUuid();
|
||||
|
||||
accountsManager.changeNumber(account, secondNumber);
|
||||
final Account changedNumberAccount = accountsManager.changeNumber(account, secondNumber);
|
||||
final UUID secondPni = changedNumberAccount.getPhoneNumberIdentifier();
|
||||
|
||||
final Account reRegisteredAccount = accountsManager.create(originalNumber, "password", null, new AccountAttributes());
|
||||
final Account reRegisteredAccount = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>());
|
||||
|
||||
assertEquals(existingAccountUuid, reRegisteredAccount.getUuid());
|
||||
assertEquals(originalPni, reRegisteredAccount.getPhoneNumberIdentifier());
|
||||
|
||||
assertEquals(Optional.empty(), deletedAccounts.findUuid(originalNumber));
|
||||
assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber));
|
||||
|
||||
accountsManager.changeNumber(reRegisteredAccount, secondNumber);
|
||||
final Account changedNumberReRegisteredAccount = accountsManager.changeNumber(reRegisteredAccount, secondNumber);
|
||||
|
||||
assertEquals(Optional.of(originalUuid), deletedAccounts.findUuid(originalNumber));
|
||||
assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber));
|
||||
assertEquals(secondPni, changedNumberReRegisteredAccount.getPhoneNumberIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
+36
-7
@@ -15,10 +15,13 @@ import static org.mockito.Mockito.atLeast;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
|
||||
import java.io.IOException;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
@@ -33,6 +36,7 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
@@ -53,6 +57,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
|
||||
|
||||
private static final String ACCOUNTS_TABLE_NAME = "accounts_test";
|
||||
private static final String NUMBERS_TABLE_NAME = "numbers_test";
|
||||
private static final String PNI_TABLE_NAME = "pni_test";
|
||||
|
||||
private static final int SCAN_PAGE_SIZE = 1;
|
||||
|
||||
@@ -94,10 +99,28 @@ class AccountsManagerConcurrentModificationIntegrationTest {
|
||||
dynamoDbExtension.getDynamoDbClient().createTable(createNumbersTableRequest);
|
||||
}
|
||||
|
||||
{
|
||||
CreateTableRequest createPhoneNumberIdentifierTableRequest = CreateTableRequest.builder()
|
||||
.tableName(PNI_TABLE_NAME)
|
||||
.keySchema(KeySchemaElement.builder()
|
||||
.attributeName(Accounts.ATTR_PNI_UUID)
|
||||
.keyType(KeyType.HASH)
|
||||
.build())
|
||||
.attributeDefinitions(AttributeDefinition.builder()
|
||||
.attributeName(Accounts.ATTR_PNI_UUID)
|
||||
.attributeType(ScalarAttributeType.B)
|
||||
.build())
|
||||
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
|
||||
.build();
|
||||
|
||||
dynamoDbExtension.getDynamoDbClient().createTable(createPhoneNumberIdentifierTableRequest);
|
||||
}
|
||||
|
||||
accounts = new Accounts(
|
||||
dynamoDbExtension.getDynamoDbClient(),
|
||||
dynamoDbExtension.getTableName(),
|
||||
NUMBERS_TABLE_NAME,
|
||||
PNI_TABLE_NAME,
|
||||
SCAN_PAGE_SIZE);
|
||||
|
||||
{
|
||||
@@ -112,19 +135,25 @@ class AccountsManagerConcurrentModificationIntegrationTest {
|
||||
return null;
|
||||
}).when(deletedAccountsManager).lockAndTake(anyString(), any());
|
||||
|
||||
final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class);
|
||||
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(anyString()))
|
||||
.thenAnswer((Answer<UUID>) invocation -> UUID.randomUUID());
|
||||
|
||||
accountsManager = new AccountsManager(
|
||||
accounts,
|
||||
phoneNumberIdentifiers,
|
||||
RedisClusterHelper.buildMockRedisCluster(commands),
|
||||
deletedAccountsManager,
|
||||
mock(DirectoryQueue.class),
|
||||
mock(KeysDynamoDb.class),
|
||||
mock(Keys.class),
|
||||
mock(MessagesManager.class),
|
||||
mock(UsernamesManager.class),
|
||||
mock(ProfilesManager.class),
|
||||
mock(StoredVerificationCodeManager.class),
|
||||
mock(SecureStorageClient.class),
|
||||
mock(SecureBackupClient.class),
|
||||
mock(ClientPresenceManager.class)
|
||||
mock(ClientPresenceManager.class),
|
||||
mock(Clock.class)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -135,7 +164,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
|
||||
final UUID uuid;
|
||||
{
|
||||
final Account account = accountsManager.update(
|
||||
accountsManager.create("+14155551212", "password", null, new AccountAttributes()),
|
||||
accountsManager.create("+14155551212", "password", null, new AccountAttributes(), new ArrayList<>()),
|
||||
a -> {
|
||||
a.setUnidentifiedAccessKey(new byte[16]);
|
||||
|
||||
@@ -182,8 +211,8 @@ class AccountsManagerConcurrentModificationIntegrationTest {
|
||||
modifyDevice(uuid, Device.MASTER_ID, device -> device.setName("deviceName"))
|
||||
).join();
|
||||
|
||||
final Account managerAccount = accountsManager.get(uuid).orElseThrow();
|
||||
final Account dynamoAccount = accounts.get(uuid).orElseThrow();
|
||||
final Account managerAccount = accountsManager.getByAccountIdentifier(uuid).orElseThrow();
|
||||
final Account dynamoAccount = accounts.getByAccountIdentifier(uuid).orElseThrow();
|
||||
|
||||
final Account redisAccount = getLastAccountFromRedisMock(commands);
|
||||
|
||||
@@ -222,7 +251,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
|
||||
private CompletableFuture<?> modifyAccount(final UUID uuid, final Consumer<Account> accountMutation) {
|
||||
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
final Account account = accountsManager.get(uuid).orElseThrow();
|
||||
final Account account = accountsManager.getByAccountIdentifier(uuid).orElseThrow();
|
||||
accountsManager.update(account, accountMutation);
|
||||
}, mutationExecutor);
|
||||
}
|
||||
@@ -230,7 +259,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
|
||||
private CompletableFuture<?> modifyDevice(final UUID uuid, final long deviceId, final Consumer<Device> deviceMutation) {
|
||||
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
final Account account = accountsManager.get(uuid).orElseThrow();
|
||||
final Account account = accountsManager.getByAccountIdentifier(uuid).orElseThrow();
|
||||
accountsManager.updateDevice(account, deviceId, deviceMutation);
|
||||
}, mutationExecutor);
|
||||
}
|
||||
|
||||
@@ -12,12 +12,14 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.uuid.UUIDComparator;
|
||||
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -25,11 +27,6 @@ import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.assertj.core.api.AssertionsForClassTypes;
|
||||
import org.jdbi.v3.core.transaction.TransactionException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
@@ -38,15 +35,22 @@ import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.CancellationReason;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
|
||||
import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
|
||||
import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement;
|
||||
import software.amazon.awssdk.services.dynamodb.model.KeyType;
|
||||
import software.amazon.awssdk.services.dynamodb.model.Put;
|
||||
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactionConflictException;
|
||||
@@ -55,7 +59,8 @@ import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
|
||||
class AccountsTest {
|
||||
|
||||
private static final String ACCOUNTS_TABLE_NAME = "accounts_test";
|
||||
private static final String NUMBERS_TABLE_NAME = "numbers_test";
|
||||
private static final String NUMBER_CONSTRAINT_TABLE_NAME = "numbers_test";
|
||||
private static final String PNI_CONSTRAINT_TABLE_NAME = "pni_test";
|
||||
|
||||
private static final int SCAN_PAGE_SIZE = 1;
|
||||
|
||||
@@ -74,7 +79,7 @@ class AccountsTest {
|
||||
@BeforeEach
|
||||
void setupAccountsDao() {
|
||||
CreateTableRequest createNumbersTableRequest = CreateTableRequest.builder()
|
||||
.tableName(NUMBERS_TABLE_NAME)
|
||||
.tableName(NUMBER_CONSTRAINT_TABLE_NAME)
|
||||
.keySchema(KeySchemaElement.builder()
|
||||
.attributeName(Accounts.ATTR_ACCOUNT_E164)
|
||||
.keyType(KeyType.HASH)
|
||||
@@ -88,27 +93,48 @@ class AccountsTest {
|
||||
|
||||
dynamoDbExtension.getDynamoDbClient().createTable(createNumbersTableRequest);
|
||||
|
||||
CreateTableRequest createPhoneNumberIdentifierTableRequest = CreateTableRequest.builder()
|
||||
.tableName(PNI_CONSTRAINT_TABLE_NAME)
|
||||
.keySchema(KeySchemaElement.builder()
|
||||
.attributeName(Accounts.ATTR_PNI_UUID)
|
||||
.keyType(KeyType.HASH)
|
||||
.build())
|
||||
.attributeDefinitions(AttributeDefinition.builder()
|
||||
.attributeName(Accounts.ATTR_PNI_UUID)
|
||||
.attributeType(ScalarAttributeType.B)
|
||||
.build())
|
||||
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
|
||||
.build();
|
||||
|
||||
dynamoDbExtension.getDynamoDbClient().createTable(createPhoneNumberIdentifierTableRequest);
|
||||
|
||||
this.accounts = new Accounts(
|
||||
dynamoDbExtension.getDynamoDbClient(),
|
||||
dynamoDbExtension.getTableName(),
|
||||
NUMBERS_TABLE_NAME,
|
||||
NUMBER_CONSTRAINT_TABLE_NAME,
|
||||
PNI_CONSTRAINT_TABLE_NAME,
|
||||
SCAN_PAGE_SIZE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStore() {
|
||||
Device device = generateDevice (1 );
|
||||
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device));
|
||||
Device device = generateDevice(1);
|
||||
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
|
||||
|
||||
boolean freshUser = accounts.create(account);
|
||||
|
||||
assertThat(freshUser).isTrue();
|
||||
verifyStoredState("+14151112222", account.getUuid(), account, true);
|
||||
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
|
||||
|
||||
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
|
||||
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid());
|
||||
|
||||
freshUser = accounts.create(account);
|
||||
assertThat(freshUser).isTrue();
|
||||
verifyStoredState("+14151112222", account.getUuid(), account, true);
|
||||
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
|
||||
|
||||
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
|
||||
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -117,11 +143,14 @@ class AccountsTest {
|
||||
devices.add(generateDevice(1));
|
||||
devices.add(generateDevice(2));
|
||||
|
||||
Account account = generateAccount("+14151112222", UUID.randomUUID(), devices);
|
||||
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), devices);
|
||||
|
||||
accounts.create(account);
|
||||
|
||||
verifyStoredState("+14151112222", account.getUuid(), account, true);
|
||||
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
|
||||
|
||||
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
|
||||
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -131,46 +160,123 @@ class AccountsTest {
|
||||
devicesFirst.add(generateDevice(2));
|
||||
|
||||
UUID uuidFirst = UUID.randomUUID();
|
||||
Account accountFirst = generateAccount("+14151112222", uuidFirst, devicesFirst);
|
||||
UUID pniFirst = UUID.randomUUID();
|
||||
Account accountFirst = generateAccount("+14151112222", uuidFirst, pniFirst, devicesFirst);
|
||||
|
||||
Set<Device> devicesSecond = new HashSet<>();
|
||||
devicesSecond.add(generateDevice(1));
|
||||
devicesSecond.add(generateDevice(2));
|
||||
|
||||
UUID uuidSecond = UUID.randomUUID();
|
||||
Account accountSecond = generateAccount("+14152221111", uuidSecond, devicesSecond);
|
||||
UUID pniSecond = UUID.randomUUID();
|
||||
Account accountSecond = generateAccount("+14152221111", uuidSecond, pniSecond, devicesSecond);
|
||||
|
||||
accounts.create(accountFirst);
|
||||
accounts.create(accountSecond);
|
||||
|
||||
Optional<Account> retrievedFirst = accounts.get("+14151112222");
|
||||
Optional<Account> retrievedSecond = accounts.get("+14152221111");
|
||||
Optional<Account> retrievedFirst = accounts.getByE164("+14151112222");
|
||||
Optional<Account> retrievedSecond = accounts.getByE164("+14152221111");
|
||||
|
||||
assertThat(retrievedFirst.isPresent()).isTrue();
|
||||
assertThat(retrievedSecond.isPresent()).isTrue();
|
||||
|
||||
verifyStoredState("+14151112222", uuidFirst, retrievedFirst.get(), accountFirst);
|
||||
verifyStoredState("+14152221111", uuidSecond, retrievedSecond.get(), accountSecond);
|
||||
verifyStoredState("+14151112222", uuidFirst, pniFirst, retrievedFirst.get(), accountFirst);
|
||||
verifyStoredState("+14152221111", uuidSecond, pniSecond, retrievedSecond.get(), accountSecond);
|
||||
|
||||
retrievedFirst = accounts.get(uuidFirst);
|
||||
retrievedSecond = accounts.get(uuidSecond);
|
||||
retrievedFirst = accounts.getByAccountIdentifier(uuidFirst);
|
||||
retrievedSecond = accounts.getByAccountIdentifier(uuidSecond);
|
||||
|
||||
assertThat(retrievedFirst.isPresent()).isTrue();
|
||||
assertThat(retrievedSecond.isPresent()).isTrue();
|
||||
|
||||
verifyStoredState("+14151112222", uuidFirst, retrievedFirst.get(), accountFirst);
|
||||
verifyStoredState("+14152221111", uuidSecond, retrievedSecond.get(), accountSecond);
|
||||
verifyStoredState("+14151112222", uuidFirst, pniFirst, retrievedFirst.get(), accountFirst);
|
||||
verifyStoredState("+14152221111", uuidSecond, pniSecond, retrievedSecond.get(), accountSecond);
|
||||
|
||||
retrievedFirst = accounts.getByPhoneNumberIdentifier(pniFirst);
|
||||
retrievedSecond = accounts.getByPhoneNumberIdentifier(pniSecond);
|
||||
|
||||
assertThat(retrievedFirst.isPresent()).isTrue();
|
||||
assertThat(retrievedSecond.isPresent()).isTrue();
|
||||
|
||||
verifyStoredState("+14151112222", uuidFirst, pniFirst, retrievedFirst.get(), accountFirst);
|
||||
verifyStoredState("+14152221111", uuidSecond, pniSecond, retrievedSecond.get(), accountSecond);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveNoPni() throws JsonProcessingException {
|
||||
final Set<Device> devices = new HashSet<>();
|
||||
devices.add(generateDevice(1));
|
||||
devices.add(generateDevice(2));
|
||||
|
||||
final UUID uuid = UUID.randomUUID();
|
||||
|
||||
final Account account = generateAccount("+14151112222", uuid, null, devices);
|
||||
|
||||
// Accounts#create enforces that newly-created accounts have a PNI, so we need to make a bit of an end-run around it
|
||||
// to simulate an existing account with no PNI.
|
||||
{
|
||||
final TransactWriteItem phoneNumberConstraintPut = TransactWriteItem.builder()
|
||||
.put(
|
||||
Put.builder()
|
||||
.tableName(NUMBER_CONSTRAINT_TABLE_NAME)
|
||||
.item(Map.of(
|
||||
Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
|
||||
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.conditionExpression(
|
||||
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
|
||||
.expressionAttributeNames(
|
||||
Map.of("#uuid", Accounts.KEY_ACCOUNT_UUID,
|
||||
"#number", Accounts.ATTR_ACCOUNT_E164))
|
||||
.expressionAttributeValues(
|
||||
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
final TransactWriteItem accountPut = TransactWriteItem.builder()
|
||||
.put(Put.builder()
|
||||
.tableName(ACCOUNTS_TABLE_NAME)
|
||||
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
|
||||
.expressionAttributeNames(Map.of("#number", Accounts.ATTR_ACCOUNT_E164))
|
||||
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber())))
|
||||
.item(Map.of(
|
||||
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid),
|
||||
Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
|
||||
Accounts.ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
Accounts.ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
|
||||
Accounts.ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
dynamoDbExtension.getDynamoDbClient().transactWriteItems(TransactWriteItemsRequest.builder()
|
||||
.transactItems(phoneNumberConstraintPut, accountPut)
|
||||
.build());
|
||||
}
|
||||
|
||||
Optional<Account> retrieved = accounts.getByE164("+14151112222");
|
||||
|
||||
assertThat(retrieved.isPresent()).isTrue();
|
||||
verifyStoredState("+14151112222", uuid, null, retrieved.get(), account);
|
||||
|
||||
retrieved = accounts.getByAccountIdentifier(uuid);
|
||||
|
||||
assertThat(retrieved.isPresent()).isTrue();
|
||||
verifyStoredState("+14151112222", uuid, null, retrieved.get(), account);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOverwrite() {
|
||||
Device device = generateDevice (1 );
|
||||
UUID firstUuid = UUID.randomUUID();
|
||||
Account account = generateAccount("+14151112222", firstUuid, Collections.singleton(device));
|
||||
Device device = generateDevice(1);
|
||||
UUID firstUuid = UUID.randomUUID();
|
||||
UUID firstPni = UUID.randomUUID();
|
||||
Account account = generateAccount("+14151112222", firstUuid, firstPni, Collections.singleton(device));
|
||||
|
||||
accounts.create(account);
|
||||
|
||||
verifyStoredState("+14151112222", account.getUuid(), account, true);
|
||||
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
|
||||
|
||||
assertPhoneNumberConstraintExists("+14151112222", firstUuid);
|
||||
assertPhoneNumberIdentifierConstraintExists(firstPni, firstUuid);
|
||||
|
||||
account.setProfileName("name");
|
||||
|
||||
@@ -179,14 +285,17 @@ class AccountsTest {
|
||||
UUID secondUuid = UUID.randomUUID();
|
||||
|
||||
device = generateDevice(1);
|
||||
account = generateAccount("+14151112222", secondUuid, Collections.singleton(device));
|
||||
account = generateAccount("+14151112222", secondUuid, UUID.randomUUID(), Collections.singleton(device));
|
||||
|
||||
final boolean freshUser = accounts.create(account);
|
||||
assertThat(freshUser).isFalse();
|
||||
verifyStoredState("+14151112222", firstUuid, account, true);
|
||||
verifyStoredState("+14151112222", firstUuid, firstPni, account, true);
|
||||
|
||||
assertPhoneNumberConstraintExists("+14151112222", firstUuid);
|
||||
assertPhoneNumberIdentifierConstraintExists(firstPni, firstUuid);
|
||||
|
||||
device = generateDevice(1);
|
||||
Account invalidAccount = generateAccount("+14151113333", firstUuid, Collections.singleton(device));
|
||||
Account invalidAccount = generateAccount("+14151113333", firstUuid, UUID.randomUUID(), Collections.singleton(device));
|
||||
|
||||
assertThatThrownBy(() -> accounts.create(invalidAccount));
|
||||
}
|
||||
@@ -194,26 +303,32 @@ class AccountsTest {
|
||||
@Test
|
||||
void testUpdate() {
|
||||
Device device = generateDevice (1 );
|
||||
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device));
|
||||
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
|
||||
|
||||
accounts.create(account);
|
||||
|
||||
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
|
||||
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid());
|
||||
|
||||
device.setName("foobar");
|
||||
|
||||
accounts.update(account);
|
||||
|
||||
Optional<Account> retrieved = accounts.get("+14151112222");
|
||||
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
|
||||
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid());
|
||||
|
||||
Optional<Account> retrieved = accounts.getByE164("+14151112222");
|
||||
|
||||
assertThat(retrieved.isPresent()).isTrue();
|
||||
verifyStoredState("+14151112222", account.getUuid(), retrieved.get(), account);
|
||||
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), retrieved.get(), account);
|
||||
|
||||
retrieved = accounts.get(account.getUuid());
|
||||
retrieved = accounts.getByAccountIdentifier(account.getUuid());
|
||||
|
||||
assertThat(retrieved.isPresent()).isTrue();
|
||||
verifyStoredState("+14151112222", account.getUuid(), account, true);
|
||||
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
|
||||
|
||||
device = generateDevice(1);
|
||||
Account unknownAccount = generateAccount("+14151113333", UUID.randomUUID(), Collections.singleton(device));
|
||||
Account unknownAccount = generateAccount("+14151113333", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
|
||||
|
||||
assertThatThrownBy(() -> accounts.update(unknownAccount)).isInstanceOfAny(ConditionalCheckFailedException.class);
|
||||
|
||||
@@ -223,7 +338,7 @@ class AccountsTest {
|
||||
|
||||
assertThat(account.getVersion()).isEqualTo(2);
|
||||
|
||||
verifyStoredState("+14151112222", account.getUuid(), account, true);
|
||||
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
|
||||
|
||||
account.setVersion(1);
|
||||
|
||||
@@ -234,7 +349,7 @@ class AccountsTest {
|
||||
|
||||
accounts.update(account);
|
||||
|
||||
verifyStoredState("+14151112222", account.getUuid(), account, true);
|
||||
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -242,13 +357,12 @@ class AccountsTest {
|
||||
|
||||
final DynamoDbClient dynamoDbClient = mock(DynamoDbClient.class);
|
||||
accounts = new Accounts(dynamoDbClient,
|
||||
dynamoDbExtension.getTableName(), NUMBERS_TABLE_NAME, SCAN_PAGE_SIZE);
|
||||
dynamoDbExtension.getTableName(), NUMBER_CONSTRAINT_TABLE_NAME, PNI_CONSTRAINT_TABLE_NAME, SCAN_PAGE_SIZE);
|
||||
|
||||
when(dynamoDbClient.updateItem(any(UpdateItemRequest.class)))
|
||||
.thenThrow(TransactionConflictException.class);
|
||||
|
||||
Device device = generateDevice(1);
|
||||
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device));
|
||||
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID());
|
||||
|
||||
assertThatThrownBy(() -> accounts.update(account)).isInstanceOfAny(ContestedOptimisticLockException.class);
|
||||
}
|
||||
@@ -258,7 +372,7 @@ class AccountsTest {
|
||||
List<Account> users = new ArrayList<>();
|
||||
|
||||
for (int i = 1; i <= 100; i++) {
|
||||
Account account = generateAccount("+1" + String.format("%03d", i), UUID.randomUUID());
|
||||
Account account = generateAccount("+1" + String.format("%03d", i), UUID.randomUUID(), UUID.randomUUID());
|
||||
users.add(account);
|
||||
accounts.create(account);
|
||||
}
|
||||
@@ -276,7 +390,7 @@ class AccountsTest {
|
||||
.findAny()
|
||||
.orElseThrow();
|
||||
|
||||
verifyStoredState(expectedAccount.getNumber(), expectedAccount.getUuid(), retrievedAccount, expectedAccount);
|
||||
verifyStoredState(expectedAccount.getNumber(), expectedAccount.getUuid(), expectedAccount.getPhoneNumberIdentifier(), retrievedAccount, expectedAccount);
|
||||
|
||||
users.remove(expectedAccount);
|
||||
}
|
||||
@@ -293,7 +407,7 @@ class AccountsTest {
|
||||
.findAny()
|
||||
.orElseThrow();
|
||||
|
||||
verifyStoredState(expectedAccount.getNumber(), expectedAccount.getUuid(), retrievedAccount, expectedAccount);
|
||||
verifyStoredState(expectedAccount.getNumber(), expectedAccount.getUuid(), expectedAccount.getPhoneNumberIdentifier(), retrievedAccount, expectedAccount);
|
||||
|
||||
users.remove(expectedAccount);
|
||||
}
|
||||
@@ -306,48 +420,59 @@ class AccountsTest {
|
||||
void testDelete() {
|
||||
final Device deletedDevice = generateDevice(1);
|
||||
final Account deletedAccount = generateAccount("+14151112222", UUID.randomUUID(),
|
||||
Collections.singleton(deletedDevice));
|
||||
UUID.randomUUID(), Collections.singleton(deletedDevice));
|
||||
final Device retainedDevice = generateDevice(1);
|
||||
final Account retainedAccount = generateAccount("+14151112345", UUID.randomUUID(),
|
||||
Collections.singleton(retainedDevice));
|
||||
UUID.randomUUID(), Collections.singleton(retainedDevice));
|
||||
|
||||
accounts.create(deletedAccount);
|
||||
accounts.create(retainedAccount);
|
||||
|
||||
assertThat(accounts.get(deletedAccount.getUuid())).isPresent();
|
||||
assertThat(accounts.get(retainedAccount.getUuid())).isPresent();
|
||||
assertPhoneNumberConstraintExists("+14151112222", deletedAccount.getUuid());
|
||||
assertPhoneNumberIdentifierConstraintExists(deletedAccount.getPhoneNumberIdentifier(), deletedAccount.getUuid());
|
||||
assertPhoneNumberConstraintExists("+14151112345", retainedAccount.getUuid());
|
||||
assertPhoneNumberIdentifierConstraintExists(retainedAccount.getPhoneNumberIdentifier(), retainedAccount.getUuid());
|
||||
|
||||
assertThat(accounts.getByAccountIdentifier(deletedAccount.getUuid())).isPresent();
|
||||
assertThat(accounts.getByAccountIdentifier(retainedAccount.getUuid())).isPresent();
|
||||
|
||||
accounts.delete(deletedAccount.getUuid());
|
||||
|
||||
assertThat(accounts.get(deletedAccount.getUuid())).isNotPresent();
|
||||
assertThat(accounts.getByAccountIdentifier(deletedAccount.getUuid())).isNotPresent();
|
||||
|
||||
verifyStoredState(retainedAccount.getNumber(), retainedAccount.getUuid(),
|
||||
accounts.get(retainedAccount.getUuid()).get(), retainedAccount);
|
||||
assertPhoneNumberConstraintDoesNotExist(deletedAccount.getNumber());
|
||||
assertPhoneNumberIdentifierConstraintDoesNotExist(deletedAccount.getPhoneNumberIdentifier());
|
||||
|
||||
verifyStoredState(retainedAccount.getNumber(), retainedAccount.getUuid(), retainedAccount.getPhoneNumberIdentifier(),
|
||||
accounts.getByAccountIdentifier(retainedAccount.getUuid()).get(), retainedAccount);
|
||||
|
||||
{
|
||||
final Account recreatedAccount = generateAccount(deletedAccount.getNumber(), UUID.randomUUID(),
|
||||
Collections.singleton(generateDevice(1)));
|
||||
UUID.randomUUID(), Collections.singleton(generateDevice(1)));
|
||||
|
||||
final boolean freshUser = accounts.create(recreatedAccount);
|
||||
|
||||
assertThat(freshUser).isTrue();
|
||||
assertThat(accounts.get(recreatedAccount.getUuid())).isPresent();
|
||||
verifyStoredState(recreatedAccount.getNumber(), recreatedAccount.getUuid(),
|
||||
accounts.get(recreatedAccount.getUuid()).get(), recreatedAccount);
|
||||
assertThat(accounts.getByAccountIdentifier(recreatedAccount.getUuid())).isPresent();
|
||||
verifyStoredState(recreatedAccount.getNumber(), recreatedAccount.getUuid(), recreatedAccount.getPhoneNumberIdentifier(),
|
||||
accounts.getByAccountIdentifier(recreatedAccount.getUuid()).get(), recreatedAccount);
|
||||
|
||||
assertPhoneNumberConstraintExists(recreatedAccount.getNumber(), recreatedAccount.getUuid());
|
||||
assertPhoneNumberIdentifierConstraintExists(recreatedAccount.getPhoneNumberIdentifier(), recreatedAccount.getUuid());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMissing() {
|
||||
Device device = generateDevice (1 );
|
||||
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device));
|
||||
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
|
||||
|
||||
accounts.create(account);
|
||||
|
||||
Optional<Account> retrieved = accounts.get("+11111111");
|
||||
Optional<Account> retrieved = accounts.getByE164("+11111111");
|
||||
assertThat(retrieved.isPresent()).isFalse();
|
||||
|
||||
retrieved = accounts.get(UUID.randomUUID());
|
||||
retrieved = accounts.getByAccountIdentifier(UUID.randomUUID());
|
||||
assertThat(retrieved.isPresent()).isFalse();
|
||||
}
|
||||
|
||||
@@ -369,8 +494,9 @@ class AccountsTest {
|
||||
when(client.updateItem(any(UpdateItemRequest.class)))
|
||||
.thenThrow(RuntimeException.class);
|
||||
|
||||
Accounts accounts = new Accounts(client, ACCOUNTS_TABLE_NAME, NUMBERS_TABLE_NAME, SCAN_PAGE_SIZE);
|
||||
Account account = generateAccount("+14151112222", UUID.randomUUID());
|
||||
Accounts accounts = new Accounts(client, ACCOUNTS_TABLE_NAME, NUMBER_CONSTRAINT_TABLE_NAME,
|
||||
PNI_CONSTRAINT_TABLE_NAME, SCAN_PAGE_SIZE);
|
||||
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID());
|
||||
|
||||
try {
|
||||
accounts.update(account);
|
||||
@@ -406,17 +532,16 @@ class AccountsTest {
|
||||
@Test
|
||||
void testCanonicallyDiscoverableSet() {
|
||||
Device device = generateDevice(1);
|
||||
UUID uuid = UUID.randomUUID();
|
||||
Account account = generateAccount("+14151112222", uuid, Collections.singleton(device));
|
||||
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
|
||||
account.setDiscoverableByPhoneNumber(false);
|
||||
accounts.create(account);
|
||||
verifyStoredState("+14151112222", account.getUuid(), account, false);
|
||||
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, false);
|
||||
account.setDiscoverableByPhoneNumber(true);
|
||||
accounts.update(account);
|
||||
verifyStoredState("+14151112222", account.getUuid(), account, true);
|
||||
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, true);
|
||||
account.setDiscoverableByPhoneNumber(false);
|
||||
accounts.update(account);
|
||||
verifyStoredState("+14151112222", account.getUuid(), account, false);
|
||||
verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), account, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -424,27 +549,44 @@ class AccountsTest {
|
||||
final String originalNumber = "+14151112222";
|
||||
final String targetNumber = "+14151113333";
|
||||
|
||||
final UUID originalPni = UUID.randomUUID();
|
||||
final UUID targetPni = UUID.randomUUID();
|
||||
|
||||
final Device device = generateDevice(1);
|
||||
final Account account = generateAccount(originalNumber, UUID.randomUUID(), Collections.singleton(device));
|
||||
final Account account = generateAccount(originalNumber, UUID.randomUUID(), originalPni, Collections.singleton(device));
|
||||
|
||||
accounts.create(account);
|
||||
|
||||
assertThat(accounts.getByPhoneNumberIdentifier(originalPni)).isPresent();
|
||||
|
||||
assertPhoneNumberConstraintExists(originalNumber, account.getUuid());
|
||||
assertPhoneNumberIdentifierConstraintExists(originalPni, account.getUuid());
|
||||
|
||||
{
|
||||
final Optional<Account> retrieved = accounts.get(originalNumber);
|
||||
final Optional<Account> retrieved = accounts.getByE164(originalNumber);
|
||||
assertThat(retrieved).isPresent();
|
||||
|
||||
verifyStoredState(originalNumber, account.getUuid(), retrieved.get(), account);
|
||||
verifyStoredState(originalNumber, account.getUuid(), account.getPhoneNumberIdentifier(), retrieved.get(), account);
|
||||
}
|
||||
|
||||
accounts.changeNumber(account, targetNumber);
|
||||
accounts.changeNumber(account, targetNumber, targetPni);
|
||||
|
||||
assertThat(accounts.get(originalNumber)).isEmpty();
|
||||
assertThat(accounts.getByE164(originalNumber)).isEmpty();
|
||||
assertThat(accounts.getByAccountIdentifier(originalPni)).isEmpty();
|
||||
|
||||
assertPhoneNumberConstraintDoesNotExist(originalNumber);
|
||||
assertPhoneNumberIdentifierConstraintDoesNotExist(originalPni);
|
||||
assertPhoneNumberConstraintExists(targetNumber, account.getUuid());
|
||||
assertPhoneNumberIdentifierConstraintExists(targetPni, account.getUuid());
|
||||
|
||||
{
|
||||
final Optional<Account> retrieved = accounts.get(targetNumber);
|
||||
final Optional<Account> retrieved = accounts.getByE164(targetNumber);
|
||||
assertThat(retrieved).isPresent();
|
||||
|
||||
verifyStoredState(targetNumber, account.getUuid(), retrieved.get(), account);
|
||||
verifyStoredState(targetNumber, account.getUuid(), account.getPhoneNumberIdentifier(), retrieved.get(), account);
|
||||
|
||||
assertThat(retrieved.get().getPhoneNumberIdentifier()).isEqualTo(targetPni);
|
||||
assertThat(accounts.getByPhoneNumberIdentifier(targetPni)).isPresent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,16 +595,55 @@ class AccountsTest {
|
||||
final String originalNumber = "+14151112222";
|
||||
final String targetNumber = "+14151113333";
|
||||
|
||||
final UUID originalPni = UUID.randomUUID();
|
||||
final UUID targetPni = UUID.randomUUID();
|
||||
|
||||
final Device existingDevice = generateDevice(1);
|
||||
final Account existingAccount = generateAccount(targetNumber, UUID.randomUUID(), Collections.singleton(existingDevice));
|
||||
final Account existingAccount = generateAccount(targetNumber, UUID.randomUUID(), targetPni, Collections.singleton(existingDevice));
|
||||
|
||||
final Device device = generateDevice(1);
|
||||
final Account account = generateAccount(originalNumber, UUID.randomUUID(), Collections.singleton(device));
|
||||
final Account account = generateAccount(originalNumber, UUID.randomUUID(), originalPni, Collections.singleton(device));
|
||||
|
||||
accounts.create(account);
|
||||
accounts.create(existingAccount);
|
||||
|
||||
assertThrows(TransactionCanceledException.class, () -> accounts.changeNumber(account, targetNumber));
|
||||
assertThrows(TransactionCanceledException.class, () -> accounts.changeNumber(account, targetNumber, targetPni));
|
||||
|
||||
assertPhoneNumberConstraintExists(originalNumber, account.getUuid());
|
||||
assertPhoneNumberIdentifierConstraintExists(originalPni, account.getUuid());
|
||||
assertPhoneNumberConstraintExists(targetNumber, existingAccount.getUuid());
|
||||
assertPhoneNumberIdentifierConstraintExists(targetPni, existingAccount.getUuid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChangeNumberPhoneNumberIdentifierConflict() {
|
||||
final String originalNumber = "+14151112222";
|
||||
final String targetNumber = "+14151113333";
|
||||
|
||||
final Device device = generateDevice(1);
|
||||
final Account account = generateAccount(originalNumber, UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
|
||||
|
||||
accounts.create(account);
|
||||
|
||||
final UUID existingAccountIdentifier = UUID.randomUUID();
|
||||
final UUID existingPhoneNumberIdentifier = UUID.randomUUID();
|
||||
|
||||
// Artificially inject a conflicting PNI entry
|
||||
dynamoDbExtension.getDynamoDbClient().putItem(PutItemRequest.builder()
|
||||
.tableName(PNI_CONSTRAINT_TABLE_NAME)
|
||||
.item(Map.of(
|
||||
Accounts.ATTR_PNI_UUID, AttributeValues.fromUUID(existingPhoneNumberIdentifier),
|
||||
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(existingAccountIdentifier)))
|
||||
.conditionExpression(
|
||||
"attribute_not_exists(#pni) OR (attribute_exists(#pni) AND #uuid = :uuid)")
|
||||
.expressionAttributeNames(
|
||||
Map.of("#uuid", Accounts.KEY_ACCOUNT_UUID,
|
||||
"#pni", Accounts.ATTR_PNI_UUID))
|
||||
.expressionAttributeValues(
|
||||
Map.of(":uuid", AttributeValues.fromUUID(existingAccountIdentifier)))
|
||||
.build());
|
||||
|
||||
assertThrows(TransactionCanceledException.class, () -> accounts.changeNumber(account, targetNumber, existingPhoneNumberIdentifier));
|
||||
}
|
||||
|
||||
private Device generateDevice(long id) {
|
||||
@@ -473,20 +654,62 @@ class AccountsTest {
|
||||
random.nextBoolean(), random.nextBoolean(), random.nextBoolean()));
|
||||
}
|
||||
|
||||
private Account generateAccount(String number, UUID uuid) {
|
||||
private Account generateAccount(String number, UUID uuid, final UUID pni) {
|
||||
Device device = generateDevice(1);
|
||||
return generateAccount(number, uuid, Collections.singleton(device));
|
||||
return generateAccount(number, uuid, pni, Collections.singleton(device));
|
||||
}
|
||||
|
||||
private Account generateAccount(String number, UUID uuid, Set<Device> devices) {
|
||||
private Account generateAccount(String number, UUID uuid, final UUID pni, Set<Device> devices) {
|
||||
byte[] unidentifiedAccessKey = new byte[16];
|
||||
Random random = new Random(System.currentTimeMillis());
|
||||
Arrays.fill(unidentifiedAccessKey, (byte)random.nextInt(255));
|
||||
|
||||
return new Account(number, uuid, devices, unidentifiedAccessKey);
|
||||
return new Account(number, uuid, pni, devices, unidentifiedAccessKey);
|
||||
}
|
||||
|
||||
private void verifyStoredState(String number, UUID uuid, Account expecting, boolean canonicallyDiscoverable) {
|
||||
private void assertPhoneNumberConstraintExists(final String number, final UUID uuid) {
|
||||
final GetItemResponse numberConstraintResponse = dynamoDbExtension.getDynamoDbClient().getItem(
|
||||
GetItemRequest.builder()
|
||||
.tableName(NUMBER_CONSTRAINT_TABLE_NAME)
|
||||
.key(Map.of(Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
|
||||
.build());
|
||||
|
||||
assertThat(numberConstraintResponse.hasItem()).isTrue();
|
||||
assertThat(AttributeValues.getUUID(numberConstraintResponse.item(), Accounts.KEY_ACCOUNT_UUID, null)).isEqualTo(uuid);
|
||||
}
|
||||
|
||||
private void assertPhoneNumberConstraintDoesNotExist(final String number) {
|
||||
final GetItemResponse numberConstraintResponse = dynamoDbExtension.getDynamoDbClient().getItem(
|
||||
GetItemRequest.builder()
|
||||
.tableName(NUMBER_CONSTRAINT_TABLE_NAME)
|
||||
.key(Map.of(Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
|
||||
.build());
|
||||
|
||||
assertThat(numberConstraintResponse.hasItem()).isFalse();
|
||||
}
|
||||
|
||||
private void assertPhoneNumberIdentifierConstraintExists(final UUID phoneNumberIdentifier, final UUID uuid) {
|
||||
final GetItemResponse pniConstraintResponse = dynamoDbExtension.getDynamoDbClient().getItem(
|
||||
GetItemRequest.builder()
|
||||
.tableName(PNI_CONSTRAINT_TABLE_NAME)
|
||||
.key(Map.of(Accounts.ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
|
||||
.build());
|
||||
|
||||
assertThat(pniConstraintResponse.hasItem()).isTrue();
|
||||
assertThat(AttributeValues.getUUID(pniConstraintResponse.item(), Accounts.KEY_ACCOUNT_UUID, null)).isEqualTo(uuid);
|
||||
}
|
||||
|
||||
private void assertPhoneNumberIdentifierConstraintDoesNotExist(final UUID phoneNumberIdentifier) {
|
||||
final GetItemResponse pniConstraintResponse = dynamoDbExtension.getDynamoDbClient().getItem(
|
||||
GetItemRequest.builder()
|
||||
.tableName(PNI_CONSTRAINT_TABLE_NAME)
|
||||
.key(Map.of(Accounts.ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
|
||||
.build());
|
||||
|
||||
assertThat(pniConstraintResponse.hasItem()).isFalse();
|
||||
}
|
||||
|
||||
private void verifyStoredState(String number, UUID uuid, UUID pni, Account expecting, boolean canonicallyDiscoverable) {
|
||||
final DynamoDbClient db = dynamoDbExtension.getDynamoDbClient();
|
||||
|
||||
final GetItemResponse get = db.getItem(GetItemRequest.builder()
|
||||
@@ -506,14 +729,15 @@ class AccountsTest {
|
||||
!canonicallyDiscoverable)).isEqualTo(canonicallyDiscoverable);
|
||||
|
||||
Account result = Accounts.fromItem(get.item());
|
||||
verifyStoredState(number, uuid, result, expecting);
|
||||
verifyStoredState(number, uuid, pni, result, expecting);
|
||||
} else {
|
||||
throw new AssertionError("No data");
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyStoredState(String number, UUID uuid, Account result, Account expecting) {
|
||||
private void verifyStoredState(String number, UUID uuid, UUID pni, Account result, Account expecting) {
|
||||
assertThat(result.getNumber()).isEqualTo(number);
|
||||
assertThat(result.getPhoneNumberIdentifier()).isEqualTo(pni);
|
||||
assertThat(result.getLastSeen()).isEqualTo(expecting.getLastSeen());
|
||||
assertThat(result.getUuid()).isEqualTo(uuid);
|
||||
assertThat(result.getVersion()).isEqualTo(expecting.getVersion());
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ public class ContactDiscoveryWriterTest {
|
||||
when(acct.getUuid()).thenReturn(uuid);
|
||||
when(acct.isCanonicallyDiscoverable()).thenReturn(canonicallyDiscoverable);
|
||||
when(acct.shouldBeVisibleInDirectory()).thenReturn(shouldBeVisible);
|
||||
when(mgr.get(uuid)).thenReturn(Optional.of(acct));
|
||||
when(mgr.getByAccountIdentifier(uuid)).thenReturn(Optional.of(acct));
|
||||
ContactDiscoveryWriter writer = new ContactDiscoveryWriter(mgr);
|
||||
writer.onCrawlChunk(Optional.empty(), List.of(acct));
|
||||
verify(mgr, times(updateCalled ? 1 : 0)).update(acct, ContactDiscoveryWriter.NOOP_UPDATER);
|
||||
|
||||
+4
-4
@@ -22,15 +22,15 @@ public class KeysDynamoDbRule extends LocalDynamoDbRule {
|
||||
getDynamoDbClient().createTable(CreateTableRequest.builder()
|
||||
.tableName(TABLE_NAME)
|
||||
.keySchema(
|
||||
KeySchemaElement.builder().attributeName(KeysDynamoDb.KEY_ACCOUNT_UUID).keyType(KeyType.HASH).build(),
|
||||
KeySchemaElement.builder().attributeName(KeysDynamoDb.KEY_DEVICE_ID_KEY_ID).keyType(KeyType.RANGE)
|
||||
KeySchemaElement.builder().attributeName(Keys.KEY_ACCOUNT_UUID).keyType(KeyType.HASH).build(),
|
||||
KeySchemaElement.builder().attributeName(Keys.KEY_DEVICE_ID_KEY_ID).keyType(KeyType.RANGE)
|
||||
.build())
|
||||
.attributeDefinitions(AttributeDefinition.builder()
|
||||
.attributeName(KeysDynamoDb.KEY_ACCOUNT_UUID)
|
||||
.attributeName(Keys.KEY_ACCOUNT_UUID)
|
||||
.attributeType(ScalarAttributeType.B)
|
||||
.build(),
|
||||
AttributeDefinition.builder()
|
||||
.attributeName(KeysDynamoDb.KEY_DEVICE_ID_KEY_ID)
|
||||
.attributeName(Keys.KEY_DEVICE_ID_KEY_ID)
|
||||
.attributeType(ScalarAttributeType.B)
|
||||
.build())
|
||||
.provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(20L).writeCapacityUnits(20L).build())
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class KeysDynamoDbTest {
|
||||
|
||||
private Account account;
|
||||
private KeysDynamoDb keysDynamoDb;
|
||||
|
||||
@ClassRule
|
||||
public static KeysDynamoDbRule dynamoDbRule = new KeysDynamoDbRule();
|
||||
|
||||
private static final String ACCOUNT_NUMBER = "+18005551234";
|
||||
private static final long DEVICE_ID = 1L;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
keysDynamoDb = new KeysDynamoDb(dynamoDbRule.getDynamoDbClient(), KeysDynamoDbRule.TABLE_NAME);
|
||||
|
||||
account = mock(Account.class);
|
||||
when(account.getNumber()).thenReturn(ACCOUNT_NUMBER);
|
||||
when(account.getUuid()).thenReturn(UUID.randomUUID());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStore() {
|
||||
assertEquals("Initial pre-key count for an account should be zero",
|
||||
0, keysDynamoDb.getCount(account, DEVICE_ID));
|
||||
|
||||
keysDynamoDb.store(account, DEVICE_ID, List.of(new PreKey(1, "public-key")));
|
||||
assertEquals(1, keysDynamoDb.getCount(account, DEVICE_ID));
|
||||
|
||||
keysDynamoDb.store(account, DEVICE_ID, List.of(new PreKey(1, "public-key")));
|
||||
assertEquals("Repeatedly storing same key should have no effect",
|
||||
1, keysDynamoDb.getCount(account, DEVICE_ID));
|
||||
|
||||
keysDynamoDb.store(account, DEVICE_ID, List.of(new PreKey(2, "different-public-key")));
|
||||
assertEquals("Inserting a new key should overwrite all prior keys for the given account/device",
|
||||
1, keysDynamoDb.getCount(account, DEVICE_ID));
|
||||
|
||||
keysDynamoDb.store(account, DEVICE_ID, List.of(new PreKey(3, "third-public-key"), new PreKey(4, "fourth-public-key")));
|
||||
assertEquals("Inserting multiple new keys should overwrite all prior keys for the given account/device",
|
||||
2, keysDynamoDb.getCount(account, DEVICE_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTakeAccount() {
|
||||
final Device firstDevice = mock(Device.class);
|
||||
final Device secondDevice = mock(Device.class);
|
||||
|
||||
when(firstDevice.getId()).thenReturn(DEVICE_ID);
|
||||
when(secondDevice.getId()).thenReturn(DEVICE_ID + 1);
|
||||
when(account.getDevices()).thenReturn(Set.of(firstDevice, secondDevice));
|
||||
|
||||
assertEquals(Collections.emptyMap(), keysDynamoDb.take(account));
|
||||
|
||||
final PreKey firstDevicePreKey = new PreKey(1, "public-key");
|
||||
final PreKey secondDevicePreKey = new PreKey(2, "second-key");
|
||||
|
||||
keysDynamoDb.store(account, DEVICE_ID, List.of(firstDevicePreKey));
|
||||
keysDynamoDb.store(account, DEVICE_ID + 1, List.of(secondDevicePreKey));
|
||||
|
||||
final Map<Long, PreKey> expectedKeys = Map.of(DEVICE_ID, firstDevicePreKey,
|
||||
DEVICE_ID + 1, secondDevicePreKey);
|
||||
|
||||
assertEquals(expectedKeys, keysDynamoDb.take(account));
|
||||
assertEquals(0, keysDynamoDb.getCount(account, DEVICE_ID));
|
||||
assertEquals(0, keysDynamoDb.getCount(account, DEVICE_ID + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTakeAccountAndDeviceId() {
|
||||
assertEquals(Optional.empty(), keysDynamoDb.take(account, DEVICE_ID));
|
||||
|
||||
final PreKey preKey = new PreKey(1, "public-key");
|
||||
|
||||
keysDynamoDb.store(account, DEVICE_ID, List.of(preKey, new PreKey(2, "different-pre-key")));
|
||||
assertEquals(Optional.of(preKey), keysDynamoDb.take(account, DEVICE_ID));
|
||||
assertEquals(1, keysDynamoDb.getCount(account, DEVICE_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCount() {
|
||||
assertEquals(0, keysDynamoDb.getCount(account, DEVICE_ID));
|
||||
|
||||
keysDynamoDb.store(account, DEVICE_ID, List.of(new PreKey(1, "public-key")));
|
||||
assertEquals(1, keysDynamoDb.getCount(account, DEVICE_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteByAccount() {
|
||||
keysDynamoDb.store(account, DEVICE_ID, List.of(new PreKey(1, "public-key"), new PreKey(2, "different-public-key")));
|
||||
keysDynamoDb.store(account, DEVICE_ID + 1, List.of(new PreKey(3, "public-key-for-different-device")));
|
||||
|
||||
assertEquals(2, keysDynamoDb.getCount(account, DEVICE_ID));
|
||||
assertEquals(1, keysDynamoDb.getCount(account, DEVICE_ID + 1));
|
||||
|
||||
keysDynamoDb.delete(account.getUuid());
|
||||
|
||||
assertEquals(0, keysDynamoDb.getCount(account, DEVICE_ID));
|
||||
assertEquals(0, keysDynamoDb.getCount(account, DEVICE_ID + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteByAccountAndDevice() {
|
||||
keysDynamoDb.store(account, DEVICE_ID, List.of(new PreKey(1, "public-key"), new PreKey(2, "different-public-key")));
|
||||
keysDynamoDb.store(account, DEVICE_ID + 1, List.of(new PreKey(3, "public-key-for-different-device")));
|
||||
|
||||
assertEquals(2, keysDynamoDb.getCount(account, DEVICE_ID));
|
||||
assertEquals(1, keysDynamoDb.getCount(account, DEVICE_ID + 1));
|
||||
|
||||
keysDynamoDb.delete(account.getUuid(), DEVICE_ID);
|
||||
|
||||
assertEquals(0, keysDynamoDb.getCount(account, DEVICE_ID));
|
||||
assertEquals(1, keysDynamoDb.getCount(account, DEVICE_ID + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSortKeyPrefix() {
|
||||
AttributeValue got = KeysDynamoDb.getSortKeyPrefix(123);
|
||||
assertArrayEquals(new byte[]{0, 0, 0, 0, 0, 0, 0, 123}, got.b().asByteArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||
|
||||
public class KeysTest {
|
||||
|
||||
private Keys keys;
|
||||
|
||||
@ClassRule
|
||||
public static KeysDynamoDbRule dynamoDbRule = new KeysDynamoDbRule();
|
||||
|
||||
private static final UUID ACCOUNT_UUID = UUID.randomUUID();
|
||||
private static final long DEVICE_ID = 1L;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
keys = new Keys(dynamoDbRule.getDynamoDbClient(), KeysDynamoDbRule.TABLE_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStore() {
|
||||
assertEquals("Initial pre-key count for an account should be zero",
|
||||
0, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
|
||||
|
||||
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(new PreKey(1, "public-key")));
|
||||
assertEquals(1, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
|
||||
|
||||
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(new PreKey(1, "public-key")));
|
||||
assertEquals("Repeatedly storing same key should have no effect",
|
||||
1, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
|
||||
|
||||
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(new PreKey(2, "different-public-key")));
|
||||
assertEquals("Inserting a new key should overwrite all prior keys for the given account/device",
|
||||
1, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
|
||||
|
||||
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(new PreKey(3, "third-public-key"), new PreKey(4, "fourth-public-key")));
|
||||
assertEquals("Inserting multiple new keys should overwrite all prior keys for the given account/device",
|
||||
2, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTakeAccountAndDeviceId() {
|
||||
assertEquals(Optional.empty(), keys.take(ACCOUNT_UUID, DEVICE_ID));
|
||||
|
||||
final PreKey preKey = new PreKey(1, "public-key");
|
||||
|
||||
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(preKey, new PreKey(2, "different-pre-key")));
|
||||
assertEquals(Optional.of(preKey), keys.take(ACCOUNT_UUID, DEVICE_ID));
|
||||
assertEquals(1, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCount() {
|
||||
assertEquals(0, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
|
||||
|
||||
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(new PreKey(1, "public-key")));
|
||||
assertEquals(1, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteByAccount() {
|
||||
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(new PreKey(1, "public-key"), new PreKey(2, "different-public-key")));
|
||||
keys.store(ACCOUNT_UUID, DEVICE_ID + 1, List.of(new PreKey(3, "public-key-for-different-device")));
|
||||
|
||||
assertEquals(2, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
|
||||
assertEquals(1, keys.getCount(ACCOUNT_UUID, DEVICE_ID + 1));
|
||||
|
||||
keys.delete(ACCOUNT_UUID);
|
||||
|
||||
assertEquals(0, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
|
||||
assertEquals(0, keys.getCount(ACCOUNT_UUID, DEVICE_ID + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteByAccountAndDevice() {
|
||||
keys.store(ACCOUNT_UUID, DEVICE_ID, List.of(new PreKey(1, "public-key"), new PreKey(2, "different-public-key")));
|
||||
keys.store(ACCOUNT_UUID, DEVICE_ID + 1, List.of(new PreKey(3, "public-key-for-different-device")));
|
||||
|
||||
assertEquals(2, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
|
||||
assertEquals(1, keys.getCount(ACCOUNT_UUID, DEVICE_ID + 1));
|
||||
|
||||
keys.delete(ACCOUNT_UUID, DEVICE_ID);
|
||||
|
||||
assertEquals(0, keys.getCount(ACCOUNT_UUID, DEVICE_ID));
|
||||
assertEquals(1, keys.getCount(ACCOUNT_UUID, DEVICE_ID + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSortKeyPrefix() {
|
||||
AttributeValue got = Keys.getSortKeyPrefix(123);
|
||||
assertArrayEquals(new byte[]{0, 0, 0, 0, 0, 0, 0, 123}, got.b().asByteArray());
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -82,7 +82,7 @@ class MessagePersisterIntegrationTest {
|
||||
|
||||
when(account.getNumber()).thenReturn("+18005551234");
|
||||
when(account.getUuid()).thenReturn(accountUuid);
|
||||
when(accountsManager.get(accountUuid)).thenReturn(Optional.of(account));
|
||||
when(accountsManager.getByAccountIdentifier(accountUuid)).thenReturn(Optional.of(account));
|
||||
when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration());
|
||||
|
||||
messagesCache.start();
|
||||
|
||||
+3
-3
@@ -63,7 +63,7 @@ public class MessagePersisterTest extends AbstractRedisClusterTest {
|
||||
|
||||
final Account account = mock(Account.class);
|
||||
|
||||
when(accountsManager.get(DESTINATION_ACCOUNT_UUID)).thenReturn(Optional.of(account));
|
||||
when(accountsManager.getByAccountIdentifier(DESTINATION_ACCOUNT_UUID)).thenReturn(Optional.of(account));
|
||||
when(account.getNumber()).thenReturn(DESTINATION_ACCOUNT_NUMBER);
|
||||
when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration());
|
||||
|
||||
@@ -98,7 +98,7 @@ public class MessagePersisterTest extends AbstractRedisClusterTest {
|
||||
public void testPersistNextQueuesNoQueues() {
|
||||
messagePersister.persistNextQueues(Instant.now());
|
||||
|
||||
verify(accountsManager, never()).get(any(UUID.class));
|
||||
verify(accountsManager, never()).getByAccountIdentifier(any(UUID.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -147,7 +147,7 @@ public class MessagePersisterTest extends AbstractRedisClusterTest {
|
||||
|
||||
final Account account = mock(Account.class);
|
||||
|
||||
when(accountsManager.get(accountUuid)).thenReturn(Optional.of(account));
|
||||
when(accountsManager.getByAccountIdentifier(accountUuid)).thenReturn(Optional.of(account));
|
||||
when(account.getNumber()).thenReturn(accountNumber);
|
||||
|
||||
insertMessages(accountUuid, deviceId, messagesPerQueue, now);
|
||||
|
||||
+6
-5
@@ -104,7 +104,7 @@ public class MessagesCacheTest extends AbstractRedisClusterTest {
|
||||
DESTINATION_DEVICE_ID, messageGuid);
|
||||
|
||||
assertTrue(maybeRemovedMessage.isPresent());
|
||||
assertEquals(MessagesCache.constructEntityFromEnvelope(0, message), maybeRemovedMessage.get());
|
||||
assertEquals(MessagesCache.constructEntityFromEnvelope(message), maybeRemovedMessage.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -136,7 +136,7 @@ public class MessagesCacheTest extends AbstractRedisClusterTest {
|
||||
messagesToRemove.stream().map(message -> UUID.fromString(message.getServerGuid()))
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
assertEquals(messagesToRemove.stream().map(message -> MessagesCache.constructEntityFromEnvelope(0, message))
|
||||
assertEquals(messagesToRemove.stream().map(MessagesCache::constructEntityFromEnvelope)
|
||||
.collect(Collectors.toList()),
|
||||
removedMessages);
|
||||
|
||||
@@ -167,7 +167,7 @@ public class MessagesCacheTest extends AbstractRedisClusterTest {
|
||||
final MessageProtos.Envelope message = generateRandomMessage(messageGuid, sealedSender);
|
||||
final long messageId = messagesCache.insert(messageGuid, DESTINATION_UUID, DESTINATION_DEVICE_ID, message);
|
||||
|
||||
expectedMessages.add(MessagesCache.constructEntityFromEnvelope(messageId, message));
|
||||
expectedMessages.add(MessagesCache.constructEntityFromEnvelope(message));
|
||||
}
|
||||
|
||||
assertEquals(expectedMessages, messagesCache.get(DESTINATION_UUID, DESTINATION_DEVICE_ID, messageCount));
|
||||
@@ -224,7 +224,8 @@ public class MessagesCacheTest extends AbstractRedisClusterTest {
|
||||
.setServerTimestamp(timestamp)
|
||||
.setContent(ByteString.copyFromUtf8(RandomStringUtils.randomAlphanumeric(256)))
|
||||
.setType(MessageProtos.Envelope.Type.CIPHERTEXT)
|
||||
.setServerGuid(messageGuid.toString());
|
||||
.setServerGuid(messageGuid.toString())
|
||||
.setDestinationUuid(UUID.randomUUID().toString());
|
||||
|
||||
if (!sealedSender) {
|
||||
envelopeBuilder.setSourceDevice(random.nextInt(256))
|
||||
@@ -270,7 +271,7 @@ public class MessagesCacheTest extends AbstractRedisClusterTest {
|
||||
|
||||
messagesCache.insert(messageGuid, DESTINATION_UUID, DESTINATION_DEVICE_ID,
|
||||
generateRandomMessage(messageGuid, sealedSender));
|
||||
final int slot = SlotHash.getSlot(DESTINATION_UUID.toString() + "::" + DESTINATION_DEVICE_ID);
|
||||
final int slot = SlotHash.getSlot(DESTINATION_UUID + "::" + DESTINATION_DEVICE_ID);
|
||||
|
||||
assertTrue(messagesCache.getQueuesToPersist(slot + 1, Instant.now().plusSeconds(60), 100).isEmpty());
|
||||
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
|
||||
|
||||
class PhoneNumberIdentifiersTest {
|
||||
|
||||
private static final String PNI_TABLE_NAME = "pni_test";
|
||||
|
||||
@RegisterExtension
|
||||
static DynamoDbExtension DYNAMO_DB_EXTENSION = DynamoDbExtension.builder()
|
||||
.tableName(PNI_TABLE_NAME)
|
||||
.hashKey(PhoneNumberIdentifiers.KEY_E164)
|
||||
.attributeDefinition(AttributeDefinition.builder()
|
||||
.attributeName(PhoneNumberIdentifiers.KEY_E164)
|
||||
.attributeType(ScalarAttributeType.S)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
private PhoneNumberIdentifiers phoneNumberIdentifiers;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
phoneNumberIdentifiers = new PhoneNumberIdentifiers(DYNAMO_DB_EXTENSION.getDynamoDbClient(), PNI_TABLE_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPhoneNumberIdentifier() {
|
||||
final String number = "+18005551234";
|
||||
final String differentNumber = "+18005556789";
|
||||
|
||||
final UUID firstPni = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
|
||||
final UUID secondPni = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
|
||||
|
||||
assertEquals(firstPni, secondPni);
|
||||
assertNotEquals(firstPni, phoneNumberIdentifiers.getPhoneNumberIdentifier(differentNumber));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatePhoneNumberIdentifierIfNotExists() {
|
||||
final String number = "+18005551234";
|
||||
|
||||
assertEquals(phoneNumberIdentifiers.generatePhoneNumberIdentifierIfNotExists(number),
|
||||
phoneNumberIdentifiers.generatePhoneNumberIdentifierIfNotExists(number));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -34,7 +34,7 @@ class RefreshingAccountAndDeviceSupplierTest {
|
||||
when(initialDevice.getId()).thenReturn(deviceId);
|
||||
when(initialAccount.getDevice(deviceId)).thenReturn(Optional.of(initialDevice));
|
||||
|
||||
when(accountsManager.get(any(UUID.class))).thenAnswer(answer -> {
|
||||
when(accountsManager.getByAccountIdentifier(any(UUID.class))).thenAnswer(answer -> {
|
||||
final Account account = mock(Account.class);
|
||||
final Device device = mock(Device.class);
|
||||
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
@@ -33,7 +33,7 @@ class ReportMessageManagerTest {
|
||||
|
||||
assertDoesNotThrow(() -> reportMessageManager.store(null, messageGuid));
|
||||
|
||||
verifyZeroInteractions(reportMessageDynamoDb);
|
||||
verifyNoInteractions(reportMessageDynamoDb);
|
||||
|
||||
reportMessageManager.store(number, messageGuid);
|
||||
|
||||
|
||||
+3
-3
@@ -5,17 +5,17 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.tests.auth;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
public class ExternalServiceCredentialsGeneratorTest {
|
||||
|
||||
@Test
|
||||
public void testGenerateDerivedUsername() {
|
||||
ExternalServiceCredentialGenerator generator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], true);
|
||||
ExternalServiceCredentialGenerator generator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32]);
|
||||
ExternalServiceCredentials credentials = generator.generateFor("+14152222222");
|
||||
|
||||
assertThat(credentials.getUsername()).isNotEqualTo("+14152222222");
|
||||
|
||||
+80
-15
@@ -31,6 +31,7 @@ import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -223,19 +224,20 @@ class AccountControllerTest {
|
||||
when(pendingAccountsManager.getCodeForNumber(SENDER_HAS_STORAGE)).thenReturn(Optional.of(new StoredVerificationCode("666666", System.currentTimeMillis(), null, null)));
|
||||
when(pendingAccountsManager.getCodeForNumber(SENDER_TRANSFER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), null, null)));
|
||||
|
||||
when(accountsManager.get(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount));
|
||||
when(accountsManager.get(eq(SENDER_REG_LOCK))).thenReturn(Optional.of(senderRegLockAccount));
|
||||
when(accountsManager.get(eq(SENDER_OVER_PIN))).thenReturn(Optional.of(senderPinAccount));
|
||||
when(accountsManager.get(eq(SENDER))).thenReturn(Optional.empty());
|
||||
when(accountsManager.get(eq(SENDER_OLD))).thenReturn(Optional.empty());
|
||||
when(accountsManager.get(eq(SENDER_PREAUTH))).thenReturn(Optional.empty());
|
||||
when(accountsManager.get(eq(SENDER_HAS_STORAGE))).thenReturn(Optional.of(senderHasStorage));
|
||||
when(accountsManager.get(eq(SENDER_TRANSFER))).thenReturn(Optional.of(senderTransfer));
|
||||
when(accountsManager.getByE164(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount));
|
||||
when(accountsManager.getByE164(eq(SENDER_REG_LOCK))).thenReturn(Optional.of(senderRegLockAccount));
|
||||
when(accountsManager.getByE164(eq(SENDER_OVER_PIN))).thenReturn(Optional.of(senderPinAccount));
|
||||
when(accountsManager.getByE164(eq(SENDER))).thenReturn(Optional.empty());
|
||||
when(accountsManager.getByE164(eq(SENDER_OLD))).thenReturn(Optional.empty());
|
||||
when(accountsManager.getByE164(eq(SENDER_PREAUTH))).thenReturn(Optional.empty());
|
||||
when(accountsManager.getByE164(eq(SENDER_HAS_STORAGE))).thenReturn(Optional.of(senderHasStorage));
|
||||
when(accountsManager.getByE164(eq(SENDER_TRANSFER))).thenReturn(Optional.of(senderTransfer));
|
||||
|
||||
when(accountsManager.create(any(), any(), any(), any())).thenAnswer((Answer<Account>) invocation -> {
|
||||
when(accountsManager.create(any(), any(), any(), any(), any())).thenAnswer((Answer<Account>) invocation -> {
|
||||
final Account account = mock(Account.class);
|
||||
when(account.getUuid()).thenReturn(UUID.randomUUID());
|
||||
when(account.getNumber()).thenReturn(invocation.getArgument(0, String.class));
|
||||
when(account.getBadges()).thenReturn(invocation.getArgument(4, List.class));
|
||||
|
||||
return account;
|
||||
});
|
||||
@@ -1017,7 +1019,7 @@ class AccountControllerTest {
|
||||
.header("Authorization", AuthHelper.getProvisioningAuthHeader(SENDER, "bar"))
|
||||
.put(Entity.entity(new AccountAttributes(), MediaType.APPLICATION_JSON_TYPE), AccountCreationResult.class);
|
||||
|
||||
verify(accountsManager).create(eq(SENDER), eq("bar"), any(), any());
|
||||
verify(accountsManager).create(eq(SENDER), eq("bar"), any(), any(), anyList());
|
||||
|
||||
if (enrolledInVerifyExperiment) {
|
||||
verify(smsSender).reportVerificationSucceeded("VerificationSid");
|
||||
@@ -1095,7 +1097,8 @@ class AccountControllerTest {
|
||||
verify(pinLimiter).validate(eq(SENDER_REG_LOCK));
|
||||
|
||||
verify(accountsManager).create(eq(SENDER_REG_LOCK), eq("bar"), any(), argThat(
|
||||
attributes -> Hex.toStringCondensed(registration_lock_key).equals(attributes.getRegistrationLock())));
|
||||
attributes -> Hex.toStringCondensed(registration_lock_key).equals(attributes.getRegistrationLock())),
|
||||
argThat(List::isEmpty));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1335,7 +1338,7 @@ class AccountControllerTest {
|
||||
when(existingAccount.getUuid()).thenReturn(UUID.randomUUID());
|
||||
when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock);
|
||||
|
||||
when(accountsManager.get(number)).thenReturn(Optional.of(existingAccount));
|
||||
when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount));
|
||||
|
||||
final Response response =
|
||||
resources.getJerseyTest()
|
||||
@@ -1365,7 +1368,7 @@ class AccountControllerTest {
|
||||
when(existingAccount.getUuid()).thenReturn(UUID.randomUUID());
|
||||
when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock);
|
||||
|
||||
when(accountsManager.get(number)).thenReturn(Optional.of(existingAccount));
|
||||
when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount));
|
||||
|
||||
final Response response =
|
||||
resources.getJerseyTest()
|
||||
@@ -1397,7 +1400,7 @@ class AccountControllerTest {
|
||||
when(existingAccount.getUuid()).thenReturn(UUID.randomUUID());
|
||||
when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock);
|
||||
|
||||
when(accountsManager.get(number)).thenReturn(Optional.of(existingAccount));
|
||||
when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount));
|
||||
|
||||
final Response response =
|
||||
resources.getJerseyTest()
|
||||
@@ -1429,7 +1432,7 @@ class AccountControllerTest {
|
||||
when(existingAccount.getUuid()).thenReturn(UUID.randomUUID());
|
||||
when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock);
|
||||
|
||||
when(accountsManager.get(number)).thenReturn(Optional.of(existingAccount));
|
||||
when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount));
|
||||
|
||||
final Response response =
|
||||
resources.getJerseyTest()
|
||||
@@ -1670,6 +1673,21 @@ class AccountControllerTest {
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAccountAttributesBadUnidentifiedKeyLength() {
|
||||
final AccountAttributes attributes = new AccountAttributes(false, 2222, null, null, false, null);
|
||||
attributes.setUnidentifiedAccessKey(new byte[7]);
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/attributes/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.put(Entity.json(attributes));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteAccount() throws InterruptedException {
|
||||
Response response =
|
||||
@@ -1731,4 +1749,51 @@ class AccountControllerTest {
|
||||
Arguments.of("captcha enforced", true, Set.of("1"), 402)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAccountExists() {
|
||||
final Account account = mock(Account.class);
|
||||
|
||||
final UUID accountIdentifier = UUID.randomUUID();
|
||||
final UUID phoneNumberIdentifier = UUID.randomUUID();
|
||||
|
||||
when(accountsManager.getByAccountIdentifier(any())).thenReturn(Optional.empty());
|
||||
when(accountsManager.getByAccountIdentifier(accountIdentifier)).thenReturn(Optional.of(account));
|
||||
when(accountsManager.getByPhoneNumberIdentifier(any())).thenReturn(Optional.empty());
|
||||
when(accountsManager.getByPhoneNumberIdentifier(phoneNumberIdentifier)).thenReturn(Optional.of(account));
|
||||
|
||||
when(rateLimiters.getCheckAccountExistenceLimiter()).thenReturn(mock(RateLimiter.class));
|
||||
|
||||
assertThat(resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/account/%s", accountIdentifier))
|
||||
.request()
|
||||
.header("X-Forwarded-For", "127.0.0.1")
|
||||
.head()
|
||||
.getStatus()).isEqualTo(200);
|
||||
|
||||
assertThat(resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/account/%s", phoneNumberIdentifier))
|
||||
.request()
|
||||
.header("X-Forwarded-For", "127.0.0.1")
|
||||
.head()
|
||||
.getStatus()).isEqualTo(200);
|
||||
|
||||
assertThat(resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/account/%s", UUID.randomUUID()))
|
||||
.request()
|
||||
.header("X-Forwarded-For", "127.0.0.1")
|
||||
.head()
|
||||
.getStatus()).isEqualTo(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAccountExistsAuthenticated() {
|
||||
assertThat(resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/account/%s", UUID.randomUUID()))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.header("X-Forwarded-For", "127.0.0.1")
|
||||
.head()
|
||||
.getStatus()).isEqualTo(400);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user