Compare commits

...

135 Commits
v0.9 ... v0.52

Author SHA1 Message Date
Moxie Marlinspike
8f6aff3a7e Bump version to 0.52
// FREEBIE
2015-06-24 13:46:08 -07:00
Moxie Marlinspike
fb411b20cc Make adding and removing master device operations.
// FREEBIE
2015-06-22 11:01:08 -07:00
Moxie Marlinspike
52ce7d6935 Enhance device management API.
1. Put a limit on the number of registered devices per account.

2. Support removing devices.

3. Support device names and created dates.

4. Support enumerating devices.

// FREEBIE
2015-06-19 21:41:22 -07:00
Moxie Marlinspike
75ee398633 Remove server-side tracking of "supports SMS." Nobody does!
// FREEBIE
2015-06-17 16:45:23 -07:00
Moxie Marlinspike
53bdd946d6 Update TextSecure envelope protobuf.
Support envelope 'content' field.

// FREEBIE
2015-06-17 16:29:07 -07:00
Moxie Marlinspike
79f36664ef Bump version to 0.50
// FREEBIE
2015-06-06 21:04:53 -07:00
Moxie Marlinspike
83078a48ab Support for expiration on APN messages.
// FREEBIE
2015-06-06 21:04:08 -07:00
Moxie Marlinspike
6f67a812dc Make APN fallback 30 seconds.
// FREEBIE
2015-05-27 16:25:31 -07:00
Moxie Marlinspike
6ad705b40e Fall back straight to APN.
// FREEBIE
2015-05-27 16:19:56 -07:00
Moxie Marlinspike
4cb43415a1 Track APN fallback deliverability metrics.
// FREEBIE
2015-05-15 17:01:23 -07:00
Moxie Marlinspike
bbb09b558c Support for APN fallback retries.
// FREEBIE
2015-05-15 16:04:27 -07:00
Moxie Marlinspike
6363be81e0 Support for configured test devices with hardcoded verification.
Closes #40

// FREEBIE
2015-05-13 15:35:59 -07:00
Moxie Marlinspike
c6810d7460 Bump version to 0.49
// FREEBIE
2015-05-13 14:31:38 -07:00
Moxie Marlinspike
4c1e7e7c2f Rate limit messages on source+destination rather than just src.
// FREEBIE
2015-04-24 16:25:59 -07:00
Moxie Marlinspike
931081752a Updated sample config
// FREEBIE

Closes #38
2015-04-21 19:45:31 -07:00
Moxie Marlinspike
424e98e67e Bump version to 0.48
// FREEBIE
2015-04-21 19:44:20 -07:00
Moxie Marlinspike
7cfa93f5f8 Tone down websocket logging for bad federated responses.
// FREEBIE
2015-04-21 19:44:02 -07:00
Moxie Marlinspike
fd8e8d1475 Catch WebApplicationException inside WebsocketConnection.
// FREEBIE
2015-04-16 11:33:16 -07:00
Moxie Marlinspike
7ed5eb22ec Additional WebsocketConnection test.
// FREEBIE
2015-04-16 10:45:24 -07:00
Moxie Marlinspike
fa1c275904 Bump version to 0.46
// FREEBIE
2015-04-15 16:44:22 -07:00
Moxie Marlinspike
558c72bbb7 Make pending messages indexable by sender and timestamp.
Rather than just timestamp.

// FREEBIE
2015-04-15 16:43:44 -07:00
Moxie Marlinspike
37976455bc Bump version to 0.45
// FREEBIE
2015-04-15 16:20:25 -07:00
Moxie Marlinspike
db6ee8f687 Make stored messages REST accessible.
Add REST endpoints for retrieving and acknowledging pending
messges, beyond the WebSocket.

// FREEBIE
2015-04-15 16:19:07 -07:00
Moxie Marlinspike
53e7ffa311 Bump version to 0.44 2015-03-30 10:31:35 -07:00
Moxie Marlinspike
e0f7ff325a Add voip push support in communication with push server.
// FREEBIE
2015-03-25 15:59:52 -07:00
Moxie Marlinspike
1fcd1e33c5 Log keepalives for unsubscribed channels.
// FREEBIE
2015-03-24 14:13:32 -07:00
Moxie Marlinspike
843b16c1f0 Correctly serialize provisioning addresses.
// FREEBIE
2015-03-24 12:10:59 -07:00
Moxie Marlinspike
a58f3f0fe3 Check subscription status on websocket keepalive.
// FREEBIE
2015-03-24 10:48:14 -07:00
Moxie Marlinspike
e69e395b25 Support for receiving "canonical id" update events from pushserver.
// FREEBIE
2015-03-24 10:47:45 -07:00
Moxie Marlinspike
456164fc24 Support registering a 'voip' APN ID.
// FREEBIE
2015-03-24 10:47:20 -07:00
Moxie Marlinspike
9b7f61a09d Bump version to 0.43 2015-03-20 07:57:00 -07:00
Moxie Marlinspike
2de9adb7ae Make dispatch subscription/unsubscription synchronized. 2015-03-19 14:37:10 -07:00
Moxie Marlinspike
c7e0cc1158 Use a custom redis pubsub implementation rather than Jedis.
// FREEBIE
2015-03-17 13:30:51 -07:00
Moxie Marlinspike
e79861c30a Add connection duration stats.
// FREEBIE
2015-03-15 10:21:24 -07:00
Moxie Marlinspike
407f596b61 Bump version to 0.41
// FREEBIE
2015-03-13 14:38:39 -07:00
Moxie Marlinspike
41d30fc8dc Resubscribe listeners when subscription link breaks.
// FREEBIE
2015-03-13 10:56:48 -07:00
Moxie Marlinspike
2e429c5b35 Loop voice verification prompt 3 times.
Fixes #18
Closes #32

// FREEBIE
2015-03-13 10:09:40 -07:00
Moxie Marlinspike
28afe3470b Upgrade jedis to 2.6.2
// FREEBIE
2015-03-13 10:06:44 -07:00
Moxie Marlinspike
f623b24196 Fix string formatting.
// FREEBIE
2015-03-12 16:29:49 -07:00
Moxie Marlinspike
2016c17894 Bump version to 0.40 2015-03-10 18:29:14 -07:00
Moxie Marlinspike
2d28077010 Make idle timeout 90s
// FREEBIE
2015-03-10 17:27:43 -07:00
Moxie Marlinspike
7011f3c3c7 Fix 500 on validation error 2015-03-10 16:00:20 -07:00
Moxie Marlinspike
1403dbd5dd Handle pubsub callbacks from a cached thread pool.
...implement some belt and suspenders dead letter handling.

...implement some belt and suspenders redis pubsub queue handling.

// FREEBIE
2015-03-10 12:45:05 -07:00
Moxie Marlinspike
6ef3845a34 Don't consider empty relays present on receipt delivery. 2015-03-09 15:41:50 -07:00
Moxie Marlinspike
de2f0914f0 Reenable websocket notifications for Android.
// FREEBIE
2015-03-09 09:39:09 -07:00
Moxie Marlinspike
080ae0985f Support URL-safe Base64 encoding for directory tokens.
// FREEBIE
2015-03-09 09:34:02 -07:00
Moxie Marlinspike
887f49760f Bump version to 0.35
// FREEBIE
2015-03-07 09:25:24 -08:00
Moxie Marlinspike
be77f2291b Temporarily disable GCM websocket notifications.
Workaround for a client bug.

// FREEBIE
2015-03-07 09:24:55 -08:00
Moxie Marlinspike
b585b849a1 Bump version to 0.34 2015-03-05 11:59:41 -08:00
Moxie Marlinspike
0c94e3d994 Don't print the full stack trace for twilio exceptions. 2015-03-05 08:37:27 -08:00
Moxie Marlinspike
4a93658d0f Don't consider an empty string to be a possible relay.
// FREEBIE
2015-03-05 08:36:56 -08:00
Moxie Marlinspike
6da19c6254 Set registration id when newly provisioned device registers. 2015-03-05 08:36:30 -08:00
Moxie Marlinspike
289058be81 Bump version to 0.33 2015-02-23 21:26:27 -08:00
Moxie Marlinspike
864675ecde Return stored messages in order.
// FREEBIE
2015-02-23 12:14:41 -08:00
Moxie Marlinspike
c79d7e3e30 Close existing websocket connection for a device on new connect.
// FREEBIE
2015-02-23 12:11:07 -08:00
Moxie Marlinspike
549cc6f492 Bump version to 0.32
// FREEBIE
2015-02-23 11:27:25 -08:00
Moxie Marlinspike
aa84ab66af Support for GCM stored messages.
// FREEBIE
2015-02-04 14:19:50 -08:00
Moxie Marlinspike
1fef812c67 Bump version to 0.31
// FREEBIE
2015-02-02 09:37:50 -08:00
Moxie Marlinspike
9170f74887 Vacuum messages now too.
// FREEBIE
2015-02-02 08:59:32 -08:00
Moxie Marlinspike
c9bd700d31 Bump version to 0.30
// FREEBIE
2015-02-02 08:55:11 -08:00
Moxie Marlinspike
0928e4c035 Fix leaky bucket serialization.
// FREEBIE
2015-01-30 11:48:49 -08:00
Moxie Marlinspike
75aec0a8d4 Switch to Redis for all caching.
// FREEBIE
2015-01-29 15:37:28 -08:00
Moxie Marlinspike
1f5ee36a6b Switch to postgresql-backed message DB.
// FREEBIE
2015-01-29 13:25:33 -08:00
Moxie Marlinspike
45a0b74b89 Device provisioning fixes.
// FREEBIE
2015-01-21 15:15:40 -08:00
Moxie Marlinspike
f7132bdbbc Rearrange provisioning flow. Add needsMessageSync response.
// FREEBIE
2015-01-21 13:56:58 -08:00
Moxie Marlinspike
d2dbff173a Adjust encoding 2015-01-19 19:03:06 -08:00
Moxie Marlinspike
79f83babb3 Support for ephemeral provisioning communication channels.
// FREEBIE
2015-01-18 18:09:25 -08:00
Moxie Marlinspike
715181f830 Remove duplicate dependency.
// FREEBIE

Fixes #26
2015-01-03 20:26:11 -08:00
Moxie Marlinspike
5c1c80dad3 Bump version to 0.29
// FREEBIE
2015-01-03 19:43:46 -08:00
Moxie Marlinspike
32c0712715 Chunk local directory update queries.
// FREEBIE
2015-01-03 19:43:19 -08:00
Moxie Marlinspike
fa4e492d1c Get rid of GSON dependency.
// FREEBIE
2015-01-03 18:28:51 -08:00
Moxie Marlinspike
4711fa2a9a Bump version to 0.27
// FREEBIE
2015-01-03 17:34:44 -08:00
Moxie Marlinspike
08291502eb Expire in-memory queues after 30 days of inactivity.
// FREEBIE
2015-01-03 17:24:35 -08:00
Moxie Marlinspike
1f0acd0622 Don't warn on connection timeout exceptions.
// FREEBIE
2015-01-03 16:51:59 -08:00
Moxie Marlinspike
e88b732715 Add PaperTrail support.
// FREEBIE
2015-01-03 16:51:28 -08:00
Moxie Marlinspike
dafda85c36 Move JSON reporter to Dropwizard ReporterFactory structure. 2015-01-02 23:53:40 -08:00
Moxie Marlinspike
8441fa9687 Fix bugs associated with PubSub encoding.
// FREEBIE
2014-12-12 12:35:05 -08:00
Moxie Marlinspike
77800dfb01 Update websocket-resources.
// FREEBIE
2014-12-08 09:07:45 -08:00
Moxie Marlinspike
41d15b738b Refactor direct connect delivery pipeline and message store.
1) Make message store contents more memory efficient.

2) Make notification pipeline simpler and more memory efficient.

3) Don't b64 encode websocket message bodies.

// FREEBIE
2014-12-06 20:00:39 -08:00
Moxie Marlinspike
aa2a5ff929 Bump version to 0.26
// FREEBIE
2014-12-03 13:36:25 -08:00
Moxie Marlinspike
56d3c1e73f Turn down log levels.
// FREEBIE
2014-12-03 11:44:40 -08:00
Moxie Marlinspike
f401f9a674 Schedule at 1min instead of 10min.
// FREEBIE
2014-12-03 11:35:38 -08:00
Moxie Marlinspike
30933d792b Timestamp comparison should be the other way.
// FREEBIE
2014-12-03 11:33:34 -08:00
Moxie Marlinspike
905717977e Turn down logging on metrics reporter.
// FREEBIE
2014-12-03 11:09:37 -08:00
Moxie Marlinspike
b802994809 Do a timestamp comparison on unregister events.
// FREEBIE
2014-12-03 11:09:01 -08:00
Moxie Marlinspike
ac96f906b3 Bump version to 0.25
// FREEBIE
2014-12-02 15:37:40 -08:00
Moxie Marlinspike
cc395e914f Fix APN push payload.
// FREEBIE
2014-12-01 14:01:53 -08:00
Moxie Marlinspike
f8063f8faf Add feedback handler.
// FREEBIE
2014-12-01 13:27:06 -08:00
Moxie Marlinspike
958ada9110 Bump dropwizard version.
// FREEBIE
2014-12-01 12:10:14 -08:00
Moxie Marlinspike
3452ea29b8 Use push microservice instead of doing push directly.
// FREEBIE
2014-12-01 11:23:29 -08:00
Moxie Marlinspike
675b6f4b5e Update APN payload.
// FREEBIE
2014-11-27 18:20:23 -08:00
Moxie Marlinspike
4fab67b0f5 Switch to production APN endpoint.
// FREEBIE
2014-11-27 16:25:02 -08:00
Moxie Marlinspike
8a2131416d Bump version to 0.24
// FREEBIE
2014-11-27 16:24:27 -08:00
Moxie Marlinspike
2525304215 Account for websocket-resources changes.
// FREEBIE
2014-11-15 09:48:09 -08:00
Moxie Marlinspike
fdb35d4f77 Switch to WebSocket-Resources
// FREEBIE
2014-11-14 17:59:50 -08:00
Moxie Marlinspike
222c7ea641 Support for signature token based account verification. 2014-11-13 14:56:24 -08:00
Moxie Marlinspike
8f2722263f Bump version to 0.23 2014-11-04 19:33:07 -08:00
Moxie Marlinspike
fd662e3401 Add vacuum command.
// FREEBIE
2014-11-04 19:32:35 -08:00
Moxie Marlinspike
bc65461ecb Bump version to 0.22 2014-10-01 15:03:25 -07:00
Moxie Marlinspike
30017371df Reconnect even when Smack thinks it doesn't need to. 2014-10-01 14:07:12 -07:00
Moxie Marlinspike
b944b86bf8 Bump version to 0.21
// FREEBIE
2014-07-30 11:45:45 -07:00
Moxie Marlinspike
6ba8352fa6 Update sample config to include GCM senderId
// FREEBIE
2014-07-30 11:38:23 -07:00
Moxie Marlinspike
aadf76692e Bump version to 0.20
// FREEBIE
2014-07-30 11:36:54 -07:00
Moxie Marlinspike
c9a1386a55 Fix for PubSub channel.
1) Create channels based on numbers rather than DB row ids.

2) Ensure that stored messages are cleared at reregistration
   time.
2014-07-26 20:41:25 -07:00
Moxie Marlinspike
4eb88a3e02 Server side support for delivery receipts. 2014-07-25 15:48:34 -07:00
Moxie Marlinspike
160c0bfe14 Switch from Java serialization to JSON for memcache storage. 2014-07-23 18:02:35 -07:00
Moxie Marlinspike
4cd098af1d Switch to GCM CCS and add support for APN feedback processing. 2014-07-23 18:00:49 -07:00
Moxie Marlinspike
362abd618f Bump version to 0.19
// FREEBIE
2014-07-21 01:20:57 -07:00
Moxie Marlinspike
69de9f6684 Fix stored message retrieval.
// FREEBIE
2014-07-21 01:20:14 -07:00
Moxie Marlinspike
2aa379bf21 Bumping version to 0.18
// FREEBIE
2014-07-17 11:05:38 -07:00
Moxie Marlinspike
820a2f1a63 Break FederationController into V1 and V2 2014-07-16 17:24:01 -07:00
Moxie Marlinspike
6fac7614f5 Allow device to query their currently stored signed prekey. 2014-07-16 14:44:00 -07:00
Moxie Marlinspike
b724ea8d3b Renamed 'device key' to 'signed prekey'. 2014-07-11 10:37:19 -07:00
Moxie Marlinspike
06f80c320d Introduce V2 API for PreKey updates and requests.
1) A /v2/keys controller.

2) Separate wire protocol PreKey POJOs from database PreKey
   objects.

3) Separate wire protocol PreKey submission and response POJOs.

4) Introduce a new update/response JSON format for /v2/keys.
2014-07-10 18:06:45 -07:00
Moxie Marlinspike
d9de015eab Bump version to 0.17 2014-07-10 17:45:11 -07:00
Moxie Marlinspike
dd36c861ba Pipeline directory update redis flow for a 10x speedup. 2014-07-10 17:31:39 -07:00
Moxie Marlinspike
b34e46af93 Bump version to 0.16 2014-06-30 12:18:39 -07:00
Moxie Marlinspike
405802c492 Get JSON metrics response code. 2014-06-30 12:18:16 -07:00
Moxie Marlinspike
e15f3c9d2b By default, dont try to gunzip 2014-06-29 19:48:47 -07:00
Moxie Marlinspike
885af064c9 Support unrecognized properties. 2014-06-29 18:16:43 -07:00
Moxie Marlinspike
40529dc41f Fix JSON reporter. 2014-06-27 19:49:21 -07:00
Moxie Marlinspike
2452f6ef8a Fix dependency conflicts. 2014-06-27 19:48:49 -07:00
Moxie Marlinspike
4c543e6f06 Update websocket close codes to comply with RFC 2014-06-26 16:08:29 -07:00
Moxie Marlinspike
bc5fd5d441 Update sample config 2014-06-26 16:08:29 -07:00
Moxie Marlinspike
7a33cef27e Updated iOS message delivery.
1) Use WebSockets for delivery if a client is connected.

2) If a client isn't connected, write to a redis queue and send
   an APN push.
2014-06-26 16:08:29 -07:00
Moxie Marlinspike
b433b9c879 Upgrade to dropwizard 0.7. 2014-06-26 16:08:29 -07:00
Moxie Marlinspike
5d169c523f Bump version to 0.13 2014-06-25 21:52:07 -07:00
Moxie Marlinspike
98d277368f Final migration step, remove identity_key column from keys table. 2014-06-25 21:51:22 -07:00
Moxie Marlinspike
3bd58bf25e Bumping version to 0.12 2014-06-25 21:27:00 -07:00
Moxie Marlinspike
ba05e577ae Treat account object as authoritative source for identity keys.
Step 3 in migration.
2014-06-25 21:26:25 -07:00
Moxie Marlinspike
4206f6af45 Bumping version to 0.11 2014-06-25 18:55:54 -07:00
Moxie Marlinspike
0c5da1cc47 Schema migration for identity keys. 2014-06-25 18:55:26 -07:00
Moxie Marlinspike
d9bd1c679e Bump version to 0.10 2014-06-25 11:36:12 -07:00
Moxie Marlinspike
437eb8de37 Write identity key into 'account' object.
This is the beginning of a migration to storing one identity
key per account, instead of the braindead duplication we're
doing now.  Part one of a two-part deployment in the schema
migration process.
2014-06-25 11:34:54 -07:00
Moxie Marlinspike
f14c181840 Add host system metrics. 2014-04-12 14:14:18 -07:00
178 changed files with 9860 additions and 3029 deletions

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@ run.sh
local.yml
config/production.yml
config/federated.yml
config/staging.yml
.opsmanage

View File

@@ -1,31 +1,16 @@
twilio:
accountId:
twilio: # Twilio SMS gateway configuration
accountId:
accountToken:
number:
localDomain: # The domain Twilio can call back to.
international: # Boolean specifying Twilio for international delivery
# Optional. If specified, Nexmo will be used for non-US SMS and
# voice verification if twilio.international is false. Otherwise,
# Nexmo, if specified, Nexmo will only be used as a fallback
# for failed Twilio deliveries.
nexmo:
apiKey:
apiSecret:
number:
push: # GCM/APN push server configuration
host:
port:
username:
password:
gcm:
apiKey:
# Optional. Only if iOS clients are supported.
apn:
# In PEM format.
certificate:
# In PEM format.
key:
s3:
s3: # AWS S3 configuration
accessKey:
accessSecret:
@@ -34,13 +19,37 @@ s3:
# correct permissions.
attachmentsBucket:
memcache:
servers:
user:
password:
directory: # Redis server configuration for TS directory
url:
redis:
url:
cache: # Redis server configuration for general purpose caching
url:
websocket:
enabled: true
messageStore: # Postgres database configuration for message store
driverClass: org.postgresql.Driver
user:
password:
url:
database: # Postgres database configuration for account store
# the name of your JDBC driver
driverClass: org.postgresql.Driver
# the username
user:
# the password
password:
# the JDBC URL
url: jdbc:postgresql://somehost:somport/somedb
# any properties specific to your JDBC driver:
properties:
charSet: UTF-8
federation:
name:
@@ -51,48 +60,3 @@ federation:
authenticationToken: foo
certificate: in pem format
# Optional address of graphite server to report metrics
graphite:
host:
port:
http:
shutdownGracePeriod: 0s
database:
# the name of your JDBC driver
driverClass: org.postgresql.Driver
# the username
user:
# the password
password:
# the JDBC URL
url: jdbc:postgresql://somehost:somport/somedb
# any properties specific to your JDBC driver:
properties:
charSet: UTF-8
# the maximum amount of time to wait on an empty pool before throwing an exception
maxWaitForConnection: 1s
# the SQL query to run when validating a connection's liveness
validationQuery: "/* MyService Health Check */ SELECT 1"
# the minimum number of connections to keep open
minSize: 8
# the maximum number of connections to keep open
maxSize: 32
# whether or not idle connections should be validated
checkConnectionWhileIdle: false
# how long a connection must be held before it can be validated
checkConnectionHealthWhenIdleFor: 10s
# the maximum lifetime of an idle connection
closeConnectionIfIdleFor: 1 minute

155
pom.xml
View File

@@ -9,39 +9,83 @@
<groupId>org.whispersystems.textsecure</groupId>
<artifactId>TextSecureServer</artifactId>
<version>0.9</version>
<version>0.52</version>
<properties>
<dropwizard.version>0.7.1</dropwizard.version>
<jackson.api.version>2.3.3</jackson.api.version>
<commons-codec.version>1.6</commons-codec.version>
</properties>
<dependencies>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-jdbi</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-auth</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-client</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-migrations</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-testing</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-metrics-graphite</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>com.dcsquare</groupId>
<artifactId>dropwizard-papertrail</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-json</artifactId>
<version>1.18.1</version>
</dependency>
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-graphite</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-server</artifactId>
<version>9.0.7.v20131107</version>
</dependency>
<dependency>
<groupId>bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>140</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.metrics</groupId>
<artifactId>metrics-graphite</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>com.google.android.gcm</groupId>
<artifactId>gcm-server</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>net.spy</groupId>
<artifactId>spymemcached</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>com.notnoop.apns</groupId>
<artifactId>apns</artifactId>
@@ -56,45 +100,20 @@
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>2.4.1</version>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.2.1</version>
<version>2.6.2</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-jdbi</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-auth</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-client</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-migrations</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-testing</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.twilio.sdk</groupId>
<artifactId>twilio-java-sdk</artifactId>
<version>3.4.1</version>
<version>3.4.5</version>
</dependency>
<dependency>
@@ -102,26 +121,34 @@
<artifactId>postgresql</artifactId>
<version>9.1-901.jdbc4</version>
</dependency>
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-json</artifactId>
<version>1.17.1</version>
<groupId>org.igniterealtime.smack</groupId>
<artifactId>smack-tcp</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.whispersystems</groupId>
<artifactId>websocket-resources</artifactId>
<version>0.2.3</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-websocket</artifactId>
<version>8.1.14.v20131031</version>
</dependency>
<dependency>
<groupId>org.coursera</groupId>
<artifactId>metrics-datadog</artifactId>
<version>0.1.5</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.api.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>

View File

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

View File

@@ -1,5 +1,5 @@
/**
* Copyright (C) 2013 Open WhisperSystems
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -16,15 +16,18 @@
*/
package textsecure;
option java_package = "org.whispersystems.textsecuregcm.entities";
option java_outer_classname = "MessageProtos";
option java_package = "org.whispersystems.textsecuregcm.storage";
option java_outer_classname = "PubSubProtos";
message OutgoingMessageSignal {
optional uint32 type = 1;
optional string source = 2;
optional uint32 sourceDevice = 7;
optional string relay = 3;
// repeated string destinations = 4;
optional uint64 timestamp = 5;
optional bytes message = 6;
}
message PubSubMessage {
enum Type {
UNKNOWN = 0;
QUERY_DB = 1;
DELIVER = 2;
KEEPALIVE = 3;
CLOSE = 4;
}
optional Type type = 1;
optional bytes content = 2;
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (C) 2013 Open WhisperSystems
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -14,33 +14,17 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.configuration;
package textsecure;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
option java_package = "org.whispersystems.textsecuregcm.storage";
option java_outer_classname = "StoredMessageProtos";
public class MemcacheConfiguration {
@NotEmpty
@JsonProperty
private String servers;
@JsonProperty
private String user;
@JsonProperty
private String password;
public String getServers() {
return servers;
message StoredMessage {
enum Type {
UNKNOWN = 0;
MESSAGE = 1;
}
public String getUser() {
return user;
}
public String getPassword() {
return password;
}
optional Type type = 1;
optional bytes content = 2;
}

42
protobuf/TextSecure.proto Normal file
View File

@@ -0,0 +1,42 @@
/**
* Copyright (C) 2013 - 2015 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package textsecure;
option java_package = "org.whispersystems.textsecuregcm.entities";
option java_outer_classname = "MessageProtos";
message Envelope {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
RECEIPT = 5;
}
optional Type type = 1;
optional string source = 2;
optional uint32 sourceDevice = 7;
optional string relay = 3;
optional uint64 timestamp = 5;
optional bytes legacyMessage = 6; // Contains an encrypted DataMessage
optional bytes content = 8; // Contains an encrypted Content
}
message ProvisioningUuid {
optional string uuid = 1;
}

View File

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

View File

@@ -0,0 +1,172 @@
package org.whispersystems.dispatch;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dispatch.io.RedisPubSubConnectionFactory;
import org.whispersystems.dispatch.redis.PubSubConnection;
import org.whispersystems.dispatch.redis.PubSubReply;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class DispatchManager extends Thread {
private final Logger logger = LoggerFactory.getLogger(DispatchManager.class);
private final Executor executor = Executors.newCachedThreadPool();
private final Map<String, DispatchChannel> subscriptions = new ConcurrentHashMap<>();
private final Optional<DispatchChannel> deadLetterChannel;
private final RedisPubSubConnectionFactory redisPubSubConnectionFactory;
private PubSubConnection pubSubConnection;
private volatile boolean running;
public DispatchManager(RedisPubSubConnectionFactory redisPubSubConnectionFactory,
Optional<DispatchChannel> deadLetterChannel)
{
this.redisPubSubConnectionFactory = redisPubSubConnectionFactory;
this.deadLetterChannel = deadLetterChannel;
}
@Override
public void start() {
this.pubSubConnection = redisPubSubConnectionFactory.connect();
this.running = true;
super.start();
}
public void shutdown() {
this.running = false;
this.pubSubConnection.close();
}
public synchronized void subscribe(String name, DispatchChannel dispatchChannel) {
Optional<DispatchChannel> previous = Optional.fromNullable(subscriptions.get(name));
subscriptions.put(name, dispatchChannel);
try {
pubSubConnection.subscribe(name);
} catch (IOException e) {
logger.warn("Subscription error", e);
}
if (previous.isPresent()) {
dispatchUnsubscription(name, previous.get());
}
}
public synchronized void unsubscribe(String name, DispatchChannel channel) {
Optional<DispatchChannel> subscription = Optional.fromNullable(subscriptions.get(name));
if (subscription.isPresent() && subscription.get() == channel) {
subscriptions.remove(name);
try {
pubSubConnection.unsubscribe(name);
} catch (IOException e) {
logger.warn("Unsubscribe error", e);
}
dispatchUnsubscription(name, subscription.get());
}
}
public boolean hasSubscription(String name) {
return subscriptions.containsKey(name);
}
@Override
public void run() {
while (running) {
try {
PubSubReply reply = pubSubConnection.read();
switch (reply.getType()) {
case UNSUBSCRIBE: break;
case SUBSCRIBE: dispatchSubscribe(reply); break;
case MESSAGE: dispatchMessage(reply); break;
default: throw new AssertionError("Unknown pubsub reply type! " + reply.getType());
}
} catch (IOException e) {
logger.warn("***** PubSub Connection Error *****", e);
if (running) {
this.pubSubConnection.close();
this.pubSubConnection = redisPubSubConnectionFactory.connect();
resubscribeAll();
}
}
}
logger.warn("DispatchManager Shutting Down...");
}
private void dispatchSubscribe(final PubSubReply reply) {
Optional<DispatchChannel> subscription = Optional.fromNullable(subscriptions.get(reply.getChannel()));
if (subscription.isPresent()) {
dispatchSubscription(reply.getChannel(), subscription.get());
} else {
logger.warn("Received subscribe event for non-existing channel: " + reply.getChannel());
}
}
private void dispatchMessage(PubSubReply reply) {
Optional<DispatchChannel> subscription = Optional.fromNullable(subscriptions.get(reply.getChannel()));
if (subscription.isPresent()) {
dispatchMessage(reply.getChannel(), subscription.get(), reply.getContent().get());
} else if (deadLetterChannel.isPresent()) {
dispatchMessage(reply.getChannel(), deadLetterChannel.get(), reply.getContent().get());
} else {
logger.warn("Received message for non-existing channel, with no dead letter handler: " + reply.getChannel());
}
}
private void resubscribeAll() {
new Thread() {
@Override
public void run() {
synchronized (DispatchManager.this) {
try {
for (String name : subscriptions.keySet()) {
pubSubConnection.subscribe(name);
}
} catch (IOException e) {
logger.warn("***** RESUBSCRIPTION ERROR *****", e);
}
}
}
}.start();
}
private void dispatchMessage(final String name, final DispatchChannel channel, final byte[] message) {
executor.execute(new Runnable() {
@Override
public void run() {
channel.onDispatchMessage(name, message);
}
});
}
private void dispatchSubscription(final String name, final DispatchChannel channel) {
executor.execute(new Runnable() {
@Override
public void run() {
channel.onDispatchSubscribed(name);
}
});
}
private void dispatchUnsubscription(final String name, final DispatchChannel channel) {
executor.execute(new Runnable() {
@Override
public void run() {
channel.onDispatchUnsubscribed(name);
}
});
}
}

View File

@@ -0,0 +1,64 @@
package org.whispersystems.dispatch.io;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class RedisInputStream {
private static final byte CR = 0x0D;
private static final byte LF = 0x0A;
private final InputStream inputStream;
public RedisInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
public String readLine() throws IOException {
ByteArrayOutputStream boas = new ByteArrayOutputStream();
boolean foundCr = false;
while (true) {
int character = inputStream.read();
if (character == -1) {
throw new IOException("Stream closed!");
}
boas.write(character);
if (foundCr && character == LF) break;
else if (character == CR) foundCr = true;
else if (foundCr) foundCr = false;
}
byte[] data = boas.toByteArray();
return new String(data, 0, data.length-2);
}
public byte[] readFully(int size) throws IOException {
byte[] result = new byte[size];
int offset = 0;
int remaining = result.length;
while (remaining > 0) {
int read = inputStream.read(result, offset, remaining);
if (read < 0) {
throw new IOException("Stream closed!");
}
offset += read;
remaining -= read;
}
return result;
}
public void close() throws IOException {
inputStream.close();
}
}

View File

@@ -0,0 +1,9 @@
package org.whispersystems.dispatch.io;
import org.whispersystems.dispatch.redis.PubSubConnection;
public interface RedisPubSubConnectionFactory {
public PubSubConnection connect();
}

View File

@@ -0,0 +1,119 @@
package org.whispersystems.dispatch.redis;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dispatch.io.RedisInputStream;
import org.whispersystems.dispatch.redis.protocol.ArrayReplyHeader;
import org.whispersystems.dispatch.redis.protocol.IntReply;
import org.whispersystems.dispatch.redis.protocol.StringReplyHeader;
import org.whispersystems.dispatch.util.Util;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
public class PubSubConnection {
private final Logger logger = LoggerFactory.getLogger(PubSubConnection.class);
private static final byte[] UNSUBSCRIBE_TYPE = {'u', 'n', 's', 'u', 'b', 's', 'c', 'r', 'i', 'b', 'e' };
private static final byte[] SUBSCRIBE_TYPE = {'s', 'u', 'b', 's', 'c', 'r', 'i', 'b', 'e' };
private static final byte[] MESSAGE_TYPE = {'m', 'e', 's', 's', 'a', 'g', 'e' };
private static final byte[] SUBSCRIBE_COMMAND = {'S', 'U', 'B', 'S', 'C', 'R', 'I', 'B', 'E', ' ' };
private static final byte[] UNSUBSCRIBE_COMMAND = {'U', 'N', 'S', 'U', 'B', 'S', 'C', 'R', 'I', 'B', 'E', ' '};
private static final byte[] CRLF = {'\r', '\n' };
private final OutputStream outputStream;
private final RedisInputStream inputStream;
private final Socket socket;
private final AtomicBoolean closed;
public PubSubConnection(Socket socket) throws IOException {
this.socket = socket;
this.outputStream = socket.getOutputStream();
this.inputStream = new RedisInputStream(new BufferedInputStream(socket.getInputStream()));
this.closed = new AtomicBoolean(false);
}
public void subscribe(String channelName) throws IOException {
if (closed.get()) throw new IOException("Connection closed!");
byte[] command = Util.combine(SUBSCRIBE_COMMAND, channelName.getBytes(), CRLF);
outputStream.write(command);
}
public void unsubscribe(String channelName) throws IOException {
if (closed.get()) throw new IOException("Connection closed!");
byte[] command = Util.combine(UNSUBSCRIBE_COMMAND, channelName.getBytes(), CRLF);
outputStream.write(command);
}
public PubSubReply read() throws IOException {
if (closed.get()) throw new IOException("Connection closed!");
ArrayReplyHeader replyHeader = new ArrayReplyHeader(inputStream.readLine());
if (replyHeader.getElementCount() != 3) {
throw new IOException("Received array reply header with strange count: " + replyHeader.getElementCount());
}
StringReplyHeader replyTypeHeader = new StringReplyHeader(inputStream.readLine());
byte[] replyType = inputStream.readFully(replyTypeHeader.getStringLength());
inputStream.readLine();
if (Arrays.equals(SUBSCRIBE_TYPE, replyType)) return readSubscribeReply();
else if (Arrays.equals(UNSUBSCRIBE_TYPE, replyType)) return readUnsubscribeReply();
else if (Arrays.equals(MESSAGE_TYPE, replyType)) return readMessageReply();
else throw new IOException("Unknown reply type: " + new String(replyType));
}
public void close() {
try {
this.closed.set(true);
this.inputStream.close();
this.outputStream.close();
this.socket.close();
} catch (IOException e) {
logger.warn("Exception while closing", e);
}
}
private PubSubReply readMessageReply() throws IOException {
StringReplyHeader channelNameHeader = new StringReplyHeader(inputStream.readLine());
byte[] channelName = inputStream.readFully(channelNameHeader.getStringLength());
inputStream.readLine();
StringReplyHeader messageHeader = new StringReplyHeader(inputStream.readLine());
byte[] message = inputStream.readFully(messageHeader.getStringLength());
inputStream.readLine();
return new PubSubReply(PubSubReply.Type.MESSAGE, new String(channelName), Optional.of(message));
}
private PubSubReply readUnsubscribeReply() throws IOException {
String channelName = readSubscriptionReply();
return new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, channelName, Optional.<byte[]>absent());
}
private PubSubReply readSubscribeReply() throws IOException {
String channelName = readSubscriptionReply();
return new PubSubReply(PubSubReply.Type.SUBSCRIBE, channelName, Optional.<byte[]>absent());
}
private String readSubscriptionReply() throws IOException {
StringReplyHeader channelNameHeader = new StringReplyHeader(inputStream.readLine());
byte[] channelName = inputStream.readFully(channelNameHeader.getStringLength());
inputStream.readLine();
IntReply subscriptionCount = new IntReply(inputStream.readLine());
return new String(channelName);
}
}

View File

@@ -0,0 +1,35 @@
package org.whispersystems.dispatch.redis;
import com.google.common.base.Optional;
public class PubSubReply {
public enum Type {
MESSAGE,
SUBSCRIBE,
UNSUBSCRIBE
}
private final Type type;
private final String channel;
private final Optional<byte[]> content;
public PubSubReply(Type type, String channel, Optional<byte[]> content) {
this.type = type;
this.channel = channel;
this.content = content;
}
public Type getType() {
return type;
}
public String getChannel() {
return channel;
}
public Optional<byte[]> getContent() {
return content;
}
}

View File

@@ -0,0 +1,24 @@
package org.whispersystems.dispatch.redis.protocol;
import java.io.IOException;
public class ArrayReplyHeader {
private final int elementCount;
public ArrayReplyHeader(String header) throws IOException {
if (header == null || header.length() < 2 || header.charAt(0) != '*') {
throw new IOException("Invalid array reply header: " + header);
}
try {
this.elementCount = Integer.parseInt(header.substring(1));
} catch (NumberFormatException e) {
throw new IOException(e);
}
}
public int getElementCount() {
return elementCount;
}
}

View File

@@ -0,0 +1,24 @@
package org.whispersystems.dispatch.redis.protocol;
import java.io.IOException;
public class IntReply {
private final int value;
public IntReply(String reply) throws IOException {
if (reply == null || reply.length() < 2 || reply.charAt(0) != ':') {
throw new IOException("Invalid int reply: " + reply);
}
try {
this.value = Integer.parseInt(reply.substring(1));
} catch (NumberFormatException e) {
throw new IOException(e);
}
}
public int getValue() {
return value;
}
}

View File

@@ -0,0 +1,24 @@
package org.whispersystems.dispatch.redis.protocol;
import java.io.IOException;
public class StringReplyHeader {
private final int stringLength;
public StringReplyHeader(String header) throws IOException {
if (header == null || header.length() < 2 || header.charAt(0) != '$') {
throw new IOException("Invalid string reply header: " + header);
}
try {
this.stringLength = Integer.parseInt(header.substring(1));
} catch (NumberFormatException e) {
throw new IOException(e);
}
}
public int getStringLength() {
return stringLength;
}
}

View File

@@ -0,0 +1,36 @@
package org.whispersystems.dispatch.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class Util {
public static byte[] combine(byte[]... elements) {
try {
int sum = 0;
for (byte[] element : elements) {
sum += element.length;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream(sum);
for (byte[] element : elements) {
baos.write(element);
}
return baos.toByteArray();
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -17,24 +17,30 @@
package org.whispersystems.textsecuregcm;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.yammer.dropwizard.config.Configuration;
import com.yammer.dropwizard.db.DatabaseConfiguration;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration;
import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration;
import org.whispersystems.textsecuregcm.configuration.MetricsConfiguration;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import io.dropwizard.Configuration;
import io.dropwizard.client.JerseyClientConfiguration;
import io.dropwizard.db.DataSourceFactory;
public class WhisperServerConfiguration extends Configuration {
@NotNull
@@ -46,8 +52,9 @@ public class WhisperServerConfiguration extends Configuration {
private NexmoConfiguration nexmo;
@NotNull
@Valid
@JsonProperty
private GcmConfiguration gcm;
private PushConfiguration push;
@NotNull
@Valid
@@ -57,15 +64,22 @@ public class WhisperServerConfiguration extends Configuration {
@NotNull
@Valid
@JsonProperty
private MemcacheConfiguration memcache;
private RedisConfiguration cache;
@NotNull
@Valid
@JsonProperty
private RedisConfiguration redis;
private RedisConfiguration directory;
@Valid
@NotNull
@JsonProperty
private ApnConfiguration apn = new ApnConfiguration();
private DataSourceFactory messageStore;
@Valid
@NotNull
@JsonProperty
private List<TestDeviceConfiguration> testDevices = new LinkedList<>();
@Valid
@JsonProperty
@@ -74,7 +88,7 @@ public class WhisperServerConfiguration extends Configuration {
@Valid
@NotNull
@JsonProperty
private DatabaseConfiguration database = new DatabaseConfiguration();
private DataSourceFactory database = new DataSourceFactory();
@Valid
@NotNull
@@ -87,11 +101,16 @@ public class WhisperServerConfiguration extends Configuration {
@Valid
@JsonProperty
private MetricsConfiguration metrics = new MetricsConfiguration();
private WebsocketConfiguration websocket = new WebsocketConfiguration();
@JsonProperty
private RedPhoneConfiguration redphone = new RedPhoneConfiguration();
@Valid
@NotNull
@JsonProperty
private WebsocketConfiguration websocket = new WebsocketConfiguration();
private JerseyClientConfiguration httpClient = new JerseyClientConfiguration();
public WebsocketConfiguration getWebsocketConfiguration() {
return websocket;
@@ -105,27 +124,31 @@ public class WhisperServerConfiguration extends Configuration {
return nexmo;
}
public GcmConfiguration getGcmConfiguration() {
return gcm;
public PushConfiguration getPushConfiguration() {
return push;
}
public ApnConfiguration getApnConfiguration() {
return apn;
public JerseyClientConfiguration getJerseyClientConfiguration() {
return httpClient;
}
public S3Configuration getS3Configuration() {
return s3;
}
public MemcacheConfiguration getMemcacheConfiguration() {
return memcache;
public RedisConfiguration getCacheConfiguration() {
return cache;
}
public RedisConfiguration getRedisConfiguration() {
return redis;
public RedisConfiguration getDirectoryConfiguration() {
return directory;
}
public DatabaseConfiguration getDatabaseConfiguration() {
public DataSourceFactory getMessageStoreConfiguration() {
return messageStore;
}
public DataSourceFactory getDataSourceFactory() {
return database;
}
@@ -141,7 +164,18 @@ public class WhisperServerConfiguration extends Configuration {
return graphite;
}
public MetricsConfiguration getMetricsConfiguration() {
return metrics;
public RedPhoneConfiguration getRedphoneConfiguration() {
return redphone;
}
public Map<String, Integer> getTestDevices() {
Map<String, Integer> results = new HashMap<>();
for (TestDeviceConfiguration testDeviceConfiguration : testDevices) {
results.put(testDeviceConfiguration.getNumber(),
testDeviceConfiguration.getCode());
}
return results;
}
}

View File

@@ -16,22 +16,16 @@
*/
package org.whispersystems.textsecuregcm;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.graphite.GraphiteReporter;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.google.common.base.Optional;
import com.yammer.dropwizard.Service;
import com.yammer.dropwizard.config.Bootstrap;
import com.yammer.dropwizard.config.Environment;
import com.yammer.dropwizard.config.HttpConfiguration;
import com.yammer.dropwizard.db.DatabaseConfiguration;
import com.yammer.dropwizard.jdbi.DBIFactory;
import com.yammer.dropwizard.migrations.MigrationsBundle;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Clock;
import com.yammer.metrics.core.MetricPredicate;
import com.yammer.metrics.reporting.DatadogReporter;
import com.yammer.metrics.reporting.GraphiteReporter;
import net.spy.memcached.MemcachedClient;
import com.sun.jersey.api.client.Client;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.skife.jdbi.v2.DBI;
import org.whispersystems.dispatch.DispatchChannel;
import org.whispersystems.dispatch.DispatchManager;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
import org.whispersystems.textsecuregcm.auth.MultiBasicAuthProvider;
@@ -40,21 +34,35 @@ import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.AttachmentController;
import org.whispersystems.textsecuregcm.controllers.DeviceController;
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
import org.whispersystems.textsecuregcm.controllers.FederationController;
import org.whispersystems.textsecuregcm.controllers.KeysController;
import org.whispersystems.textsecuregcm.controllers.FederationControllerV1;
import org.whispersystems.textsecuregcm.controllers.FederationControllerV2;
import org.whispersystems.textsecuregcm.controllers.KeepAliveController;
import org.whispersystems.textsecuregcm.controllers.KeysControllerV1;
import org.whispersystems.textsecuregcm.controllers.KeysControllerV2;
import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.controllers.WebsocketControllerFactory;
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
import org.whispersystems.textsecuregcm.controllers.ReceiptController;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle;
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.metrics.JsonMetricsReporter;
import org.whispersystems.textsecuregcm.providers.MemcacheHealthCheck;
import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory;
import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge;
import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge;
import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
import org.whispersystems.textsecuregcm.providers.TimeProvider;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
import org.whispersystems.textsecuregcm.push.FeedbackHandler;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.PushServiceClient;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.push.WebsocketSender;
import org.whispersystems.textsecuregcm.sms.NexmoSmsSender;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
@@ -63,23 +71,42 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.Messages;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.storage.PendingDevices;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.CORSHeaderFilter;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.UrlSigner;
import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler;
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
import org.whispersystems.textsecuregcm.workers.VacuumCommand;
import org.whispersystems.websocket.WebSocketResourceProviderFactory;
import org.whispersystems.websocket.setup.WebSocketEnvironment;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletRegistration;
import java.security.Security;
import java.util.EnumSet;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.Application;
import io.dropwizard.client.JerseyClientBuilder;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.jdbi.DBIFactory;
import io.dropwizard.metrics.graphite.GraphiteReporterFactory;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import redis.clients.jedis.JedisPool;
public class WhisperServerService extends Service<WhisperServerConfiguration> {
public class WhisperServerService extends Application<WhisperServerConfiguration> {
static {
Security.addProvider(new BouncyCastleProvider());
@@ -87,94 +114,155 @@ public class WhisperServerService extends Service<WhisperServerConfiguration> {
@Override
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
bootstrap.setName("whisper-server");
bootstrap.addCommand(new DirectoryCommand());
bootstrap.addBundle(new MigrationsBundle<WhisperServerConfiguration>() {
bootstrap.addCommand(new VacuumCommand());
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") {
@Override
public DatabaseConfiguration getDatabaseConfiguration(WhisperServerConfiguration configuration) {
return configuration.getDatabaseConfiguration();
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
return configuration.getDataSourceFactory();
}
});
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("messagedb", "messagedb.xml") {
@Override
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
return configuration.getMessageStoreConfiguration();
}
});
}
@Override
public String getName() {
return "whisper-server";
}
@Override
public void run(WhisperServerConfiguration config, Environment environment)
throws Exception
{
config.getHttpConfiguration().setConnectorType(HttpConfiguration.ConnectorType.NONBLOCKING);
SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics());
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
DBIFactory dbiFactory = new DBIFactory();
DBI jdbi = dbiFactory.build(environment, config.getDatabaseConfiguration(), "postgresql");
DBI database = dbiFactory.build(environment, config.getDataSourceFactory(), "accountdb");
DBI messagedb = dbiFactory.build(environment, config.getMessageStoreConfiguration(), "messagedb");
Accounts accounts = jdbi.onDemand(Accounts.class);
PendingAccounts pendingAccounts = jdbi.onDemand(PendingAccounts.class);
PendingDevices pendingDevices = jdbi.onDemand(PendingDevices.class);
Keys keys = jdbi.onDemand(Keys.class);
StoredMessages storedMessages = jdbi.onDemand(StoredMessages.class );
Accounts accounts = database.onDemand(Accounts.class);
PendingAccounts pendingAccounts = database.onDemand(PendingAccounts.class);
PendingDevices pendingDevices = database.onDemand(PendingDevices.class);
Keys keys = database.onDemand(Keys.class);
Messages messages = messagedb.onDemand(Messages.class);
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool();
RedisClientFactory cacheClientFactory = new RedisClientFactory(config.getCacheConfiguration().getUrl());
JedisPool cacheClient = cacheClientFactory.getRedisClientPool();
JedisPool directoryClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
Client httpClient = new JerseyClientBuilder(environment).using(config.getJerseyClientConfiguration())
.build(getName());
DirectoryManager directory = new DirectoryManager(redisClient);
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient);
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager(pendingDevices, memcachedClient);
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
PubSubManager pubSubManager = new PubSubManager(redisClient);
StoredMessageManager storedMessageManager = new StoredMessageManager(storedMessages, pubSubManager);
DirectoryManager directory = new DirectoryManager(directoryClient);
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, cacheClient);
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, cacheClient);
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
MessagesManager messagesManager = new MessagesManager(messages);
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(messagesManager);
DispatchManager dispatchManager = new DispatchManager(cacheClientFactory, Optional.<DispatchChannel>of(deadLetterHandler));
PubSubManager pubSubManager = new PubSubManager(cacheClient, dispatchManager);
PushServiceClient pushServiceClient = new PushServiceClient(httpClient, config.getPushConfiguration());
WebsocketSender websocketSender = new WebsocketSender(messagesManager, pubSubManager);
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheClient);
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushServiceClient);
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
PushSender pushSender = new PushSender(apnFallbackManager, pushServiceClient, websocketSender);
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager);
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
PushSender pushSender = new PushSender(config.getGcmConfiguration(),
config.getApnConfiguration(),
storedMessageManager,
accountsManager);
environment.lifecycle().manage(apnFallbackManager);
environment.lifecycle().manage(pubSubManager);
environment.lifecycle().manage(feedbackHandler);
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
KeysController keysController = new KeysController(rateLimiters, keys, accountsManager, federatedClientManager);
MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager);
KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager);
KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager);
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, federatedClientManager);
environment.addProvider(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
FederatedPeer.class,
deviceAuthenticator,
Device.class, "WhisperServer"));
environment.jersey().register(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
FederatedPeer.class,
deviceAuthenticator,
Device.class, "WhisperServer"));
environment.addResource(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender));
environment.addResource(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters));
environment.addResource(new DirectoryController(rateLimiters, directory));
environment.addResource(new FederationController(accountsManager, attachmentController, keysController, messageController));
environment.addResource(attachmentController);
environment.addResource(keysController);
environment.addResource(messageController);
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, messagesManager, new TimeProvider(), authorizationKey, config.getTestDevices()));
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters));
environment.jersey().register(new DirectoryController(rateLimiters, directory));
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController, keysControllerV1));
environment.jersey().register(new FederationControllerV2(accountsManager, attachmentController, messageController, keysControllerV2));
environment.jersey().register(new ReceiptController(receiptSender));
environment.jersey().register(new ProvisioningController(rateLimiters, pushSender));
environment.jersey().register(attachmentController);
environment.jersey().register(keysControllerV1);
environment.jersey().register(keysControllerV2);
environment.jersey().register(messageController);
if (config.getWebsocketConfiguration().isEnabled()) {
environment.addServlet(new WebsocketControllerFactory(deviceAuthenticator, storedMessageManager, pubSubManager),
"/v1/websocket/");
environment.addFilter(new CORSHeaderFilter(), "/*");
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config, 90000);
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator));
webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(accountsManager, pushSender, receiptSender, messagesManager, pubSubManager, apnFallbackManager));
webSocketEnvironment.jersey().register(new KeepAliveController(pubSubManager));
WebSocketEnvironment provisioningEnvironment = new WebSocketEnvironment(environment, config);
provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(pubSubManager));
provisioningEnvironment.jersey().register(new KeepAliveController(pubSubManager));
WebSocketResourceProviderFactory webSocketServlet = new WebSocketResourceProviderFactory(webSocketEnvironment );
WebSocketResourceProviderFactory provisioningServlet = new WebSocketResourceProviderFactory(provisioningEnvironment);
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", webSocketServlet );
ServletRegistration.Dynamic provisioning = environment.servlets().addServlet("Provisioning", provisioningServlet);
websocket.addMapping("/v1/websocket/");
websocket.setAsyncSupported(true);
provisioning.addMapping("/v1/websocket/provisioning/");
provisioning.setAsyncSupported(true);
webSocketServlet.start();
provisioningServlet.start();
FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class);
filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
filter.setInitParameter("allowedOrigins", "*");
filter.setInitParameter("allowedHeaders", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin");
filter.setInitParameter("allowedMethods", "GET,PUT,POST,DELETE,OPTIONS");
filter.setInitParameter("preflightMaxAge", "5184000");
filter.setInitParameter("allowCredentials", "true");
}
environment.addHealthCheck(new RedisHealthCheck(redisClient));
environment.addHealthCheck(new MemcacheHealthCheck(memcachedClient));
environment.healthChecks().register("directory", new RedisHealthCheck(directoryClient));
environment.healthChecks().register("cache", new RedisHealthCheck(cacheClient));
environment.addProvider(new IOExceptionMapper());
environment.addProvider(new RateLimitExceededExceptionMapper());
environment.jersey().register(new IOExceptionMapper());
environment.jersey().register(new RateLimitExceededExceptionMapper());
environment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
environment.jersey().register(new DeviceLimitExceededExceptionMapper());
environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge());
environment.metrics().register(name(FreeMemoryGauge.class, "free_memory"), new FreeMemoryGauge());
environment.metrics().register(name(NetworkSentGauge.class, "bytes_sent"), new NetworkSentGauge());
environment.metrics().register(name(NetworkReceivedGauge.class, "bytes_received"), new NetworkReceivedGauge());
if (config.getGraphiteConfiguration().isEnabled()) {
GraphiteReporter.enable(15, TimeUnit.SECONDS,
config.getGraphiteConfiguration().getHost(),
config.getGraphiteConfiguration().getPort());
}
GraphiteReporterFactory graphiteReporterFactory = new GraphiteReporterFactory();
graphiteReporterFactory.setHost(config.getGraphiteConfiguration().getHost());
graphiteReporterFactory.setPort(config.getGraphiteConfiguration().getPort());
if (config.getMetricsConfiguration().isEnabled()) {
new JsonMetricsReporter("textsecure", Metrics.defaultRegistry(),
config.getMetricsConfiguration().getToken(),
config.getMetricsConfiguration().getHost())
.start(60, TimeUnit.SECONDS);
GraphiteReporter graphiteReporter = (GraphiteReporter) graphiteReporterFactory.build(environment.metrics());
graphiteReporter.start(15, TimeUnit.SECONDS);
}
}
@@ -189,5 +277,4 @@ public class WhisperServerService extends Service<WhisperServerConfiguration> {
public static void main(String[] args) throws Exception {
new WhisperServerService().run(args);
}
}

View File

@@ -16,29 +16,27 @@
*/
package org.whispersystems.textsecuregcm.auth;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.AuthenticationException;
import com.yammer.dropwizard.auth.Authenticator;
import com.yammer.dropwizard.auth.basic.BasicCredentials;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Constants;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
public class AccountAuthenticator implements Authenticator<BasicCredentials, Account> {
private final Meter authenticationFailedMeter = Metrics.newMeter(AccountAuthenticator.class,
"authentication", "failed",
TimeUnit.MINUTES);
private final Meter authenticationSucceededMeter = Metrics.newMeter(AccountAuthenticator.class,
"authentication", "succeeded",
TimeUnit.MINUTES);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(), "authentication", "failed" ));
private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(), "authentication", "succeeded"));
private final Logger logger = LoggerFactory.getLogger(AccountAuthenticator.class);

View File

@@ -25,13 +25,13 @@ import java.io.IOException;
public class AuthorizationHeader {
private final String number;
private final long accountId;
private final long accountId;
private final String password;
private AuthorizationHeader(String number, long accountId, String password) {
this.number = number;
this.number = number;
this.accountId = accountId;
this.password = password;
this.password = password;
}
public static AuthorizationHeader fromUserAndPassword(String user, String password) throws InvalidAuthorizationHeaderException {

View File

@@ -0,0 +1,76 @@
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 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;
public class AuthorizationToken {
private final Logger logger = LoggerFactory.getLogger(AuthorizationToken.class);
private final String token;
private final byte[] key;
public AuthorizationToken(String token, byte[] key) {
this.token = token;
this.key = key;
}
public boolean isValid(String number, long currentTimeMillis) {
String[] parts = token.split(":");
if (parts.length != 3) {
return false;
}
if (!number.equals(parts[0])) {
return false;
}
if (!isValidTime(parts[1], currentTimeMillis)) {
return false;
}
return isValidSignature(parts[0] + ":" + parts[1], parts[2]);
}
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) {
try {
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(key, "HmacSHA256"));
byte[] ourSuffix = Util.truncate(hmac.doFinal(prefix.getBytes()), 10);
byte[] theirSuffix = Hex.decodeHex(suffix.toCharArray());
return MessageDigest.isEqual(ourSuffix, theirSuffix);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
} catch (DecoderException e) {
logger.warn("Authorizationtoken", e);
return false;
}
}
}

View File

@@ -16,30 +16,35 @@
*/
package org.whispersystems.textsecuregcm.auth;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.AuthenticationException;
import com.yammer.dropwizard.auth.Authenticator;
import com.yammer.dropwizard.auth.basic.BasicCredentials;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.util.Constants;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
public class FederatedPeerAuthenticator implements Authenticator<BasicCredentials, FederatedPeer> {
private final Meter authenticationFailedMeter = Metrics.newMeter(FederatedPeerAuthenticator.class,
"authentication", "failed",
TimeUnit.MINUTES);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter authenticationSucceededMeter = Metrics.newMeter(FederatedPeerAuthenticator.class,
"authentication", "succeeded",
TimeUnit.MINUTES);
private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(),
"authentication",
"failed"));
private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(),
"authentication",
"succeeded"));
private final Logger logger = LoggerFactory.getLogger(FederatedPeerAuthenticator.class);

View File

@@ -21,17 +21,14 @@ import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.dropwizard.auth.Authenticator;
import com.yammer.dropwizard.auth.basic.BasicAuthProvider;
import com.yammer.dropwizard.auth.basic.BasicCredentials;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.dropwizard.auth.Auth;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicAuthProvider;
import io.dropwizard.auth.basic.BasicCredentials;
public class MultiBasicAuthProvider<T1,T2> implements InjectableProvider<Auth, Parameter> {
private final Logger logger = LoggerFactory.getLogger(MultiBasicAuthProvider.class);
private final BasicAuthProvider<T1> provider1;
private final BasicAuthProvider<T2> provider2;
@@ -44,8 +41,8 @@ public class MultiBasicAuthProvider<T1,T2> implements InjectableProvider<Auth, P
Class<?> clazz2,
String realm)
{
this.provider1 = new BasicAuthProvider<T1>(authenticator1, realm);
this.provider2 = new BasicAuthProvider<T2>(authenticator2, realm);
this.provider1 = new BasicAuthProvider<>(authenticator1, realm);
this.provider2 = new BasicAuthProvider<>(authenticator2, realm);
this.clazz1 = clazz1;
this.clazz2 = clazz2;
}

View File

@@ -1,20 +0,0 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DataDogConfiguration {
@JsonProperty
private String apiKey;
@JsonProperty
private boolean enabled = false;
public String getApiKey() {
return apiKey;
}
public boolean isEnabled() {
return enabled && apiKey != null;
}
}

View File

@@ -18,12 +18,8 @@ package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import java.util.LinkedList;
import java.util.List;
public class FederationConfiguration {
@@ -34,31 +30,7 @@ public class FederationConfiguration {
@JsonProperty
private String name;
@JsonProperty
private String herokuPeers;
public List<FederatedPeer> getPeers() {
if (peers != null) {
return peers;
}
if (herokuPeers != null) {
List<FederatedPeer> peers = new LinkedList<>();
JsonElement root = new JsonParser().parse(herokuPeers);
JsonArray peerElements = root.getAsJsonArray();
for (JsonElement peer : peerElements) {
String name = peer.getAsJsonObject().get("name").getAsString();
String url = peer.getAsJsonObject().get("url").getAsString();
String authenticationToken = peer.getAsJsonObject().get("authenticationToken").getAsString();
String certificate = peer.getAsJsonObject().get("certificate").getAsString();
peers.add(new FederatedPeer(name, url, authenticationToken, certificate));
}
return peers;
}
return peers;
}

View File

@@ -19,8 +19,14 @@ package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class GcmConfiguration {
@NotNull
@JsonProperty
private long senderId;
@NotEmpty
@JsonProperty
private String apiKey;
@@ -28,4 +34,8 @@ public class GcmConfiguration {
public String getApiKey() {
return apiKey;
}
public long getSenderId() {
return senderId;
}
}

View File

@@ -0,0 +1,14 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
public class MessageStoreConfiguration {
@JsonProperty
@NotEmpty
private String url;
public String getUrl() {
return url;
}
}

View File

@@ -1,27 +0,0 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
public class MetricsConfiguration {
@JsonProperty
private String token;
@JsonProperty
private String host;
@JsonProperty
private boolean enabled = false;
public String getHost() {
return host;
}
public String getToken() {
return token;
}
public boolean isEnabled() {
return enabled && token != null && host != null;
}
}

View File

@@ -0,0 +1,40 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Min;
public class PushConfiguration {
@JsonProperty
@NotEmpty
private String host;
@JsonProperty
@Min(1)
private int port;
@JsonProperty
@NotEmpty
private String username;
@JsonProperty
@NotEmpty
private String password;
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}

View File

@@ -0,0 +1,20 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Optional;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
public class RedPhoneConfiguration {
@JsonProperty
private String authKey;
public Optional<byte[]> getAuthorizationKey() throws DecoderException {
if (authKey == null || authKey.trim().length() == 0) {
return Optional.absent();
}
return Optional.of(Hex.decodeHex(authKey.toCharArray()));
}
}

View File

@@ -0,0 +1,25 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class TestDeviceConfiguration {
@JsonProperty
@NotEmpty
private String number;
@JsonProperty
@NotNull
private int code;
public String getNumber() {
return number;
}
public int getCode() {
return code;
}
}

View File

@@ -16,24 +16,26 @@
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.AuthorizationToken;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.providers.TimeProvider;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
@@ -54,6 +56,9 @@ import javax.ws.rs.core.Response;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Map;
import io.dropwizard.auth.Auth;
@Path("/v1/accounts")
public class AccountController {
@@ -64,16 +69,28 @@ public class AccountController {
private final AccountsManager accounts;
private final RateLimiters rateLimiters;
private final SmsSender smsSender;
private final MessagesManager messagesManager;
private final TimeProvider timeProvider;
private final Optional<byte[]> authorizationKey;
private final Map<String, Integer> testDevices;
public AccountController(PendingAccountsManager pendingAccounts,
AccountsManager accounts,
RateLimiters rateLimiters,
SmsSender smsSenderFactory)
SmsSender smsSenderFactory,
MessagesManager messagesManager,
TimeProvider timeProvider,
Optional<byte[]> authorizationKey,
Map<String, Integer> testDevices)
{
this.pendingAccounts = pendingAccounts;
this.accounts = accounts;
this.rateLimiters = rateLimiters;
this.smsSender = smsSenderFactory;
this.pendingAccounts = pendingAccounts;
this.accounts = accounts;
this.rateLimiters = rateLimiters;
this.smsSender = smsSenderFactory;
this.messagesManager = messagesManager;
this.timeProvider = timeProvider;
this.authorizationKey = authorizationKey;
this.testDevices = testDevices;
}
@Timed
@@ -99,10 +116,12 @@ public class AccountController {
throw new WebApplicationException(Response.status(422).build());
}
VerificationCode verificationCode = generateVerificationCode();
VerificationCode verificationCode = generateVerificationCode(number);
pendingAccounts.store(number, verificationCode.getVerificationCode());
if (transport.equals("sms")) {
if (testDevices.containsKey(number)) {
// noop
} else if (transport.equals("sms")) {
smsSender.deliverSmsVerification(number, verificationCode.getVerificationCodeDisplay());
} else if (transport.equals("voice")) {
smsSender.deliverVoxVerification(number, verificationCode.getVerificationCodeSpeech());
@@ -139,39 +158,60 @@ public class AccountController {
throw new WebApplicationException(Response.status(417).build());
}
Device device = new Device();
device.setId(Device.MASTER_ID);
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setSignalingKey(accountAttributes.getSignalingKey());
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
Account account = new Account();
account.setNumber(number);
account.setSupportsSms(accountAttributes.getSupportsSms());
account.addDevice(device);
accounts.create(account);
pendingAccounts.remove(number);
logger.debug("Stored device...");
createAccount(number, password, accountAttributes);
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad Authorization Header", e);
throw new WebApplicationException(Response.status(401).build());
}
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Path("/token/{verification_token}")
public void verifyToken(@PathParam("verification_token") String verificationToken,
@HeaderParam("Authorization") String authorizationHeader,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
{
try {
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
String number = header.getNumber();
String password = header.getPassword();
rateLimiters.getVerifyLimiter().validate(number);
if (!authorizationKey.isPresent()) {
logger.debug("Attempt to authorize with key but not configured...");
throw new WebApplicationException(Response.status(403).build());
}
AuthorizationToken token = new AuthorizationToken(verificationToken, authorizationKey.get());
if (!token.isValid(number, timeProvider.getCurrentTimeMillis())) {
throw new WebApplicationException(Response.status(403).build());
}
createAccount(number, password, accountAttributes);
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad authorization header", e);
throw new WebApplicationException(Response.status(401).build());
}
}
@Timed
@PUT
@Path("/gcm/")
@Consumes(MediaType.APPLICATION_JSON)
public void setGcmRegistrationId(@Auth Account account, @Valid GcmRegistrationId registrationId) {
public void setGcmRegistrationId(@Auth Account account, @Valid GcmRegistrationId registrationId) {
Device device = account.getAuthenticatedDevice().get();
device.setApnId(null);
device.setVoipApnId(null);
device.setGcmId(registrationId.getGcmRegistrationId());
if (registrationId.isWebSocketChannel()) device.setFetchesMessages(true);
else device.setFetchesMessages(false);
accounts.update(account);
}
@@ -181,6 +221,7 @@ public class AccountController {
public void deleteGcmRegistrationId(@Auth Account account) {
Device device = account.getAuthenticatedDevice().get();
device.setGcmId(null);
device.setFetchesMessages(false);
accounts.update(account);
}
@@ -191,7 +232,9 @@ public class AccountController {
public void setApnRegistrationId(@Auth Account account, @Valid ApnRegistrationId registrationId) {
Device device = account.getAuthenticatedDevice().get();
device.setApnId(registrationId.getApnRegistrationId());
device.setVoipApnId(registrationId.getVoipRegistrationId());
device.setGcmId(null);
device.setFetchesMessages(true);
accounts.update(account);
}
@@ -201,6 +244,25 @@ public class AccountController {
public void deleteApnRegistrationId(@Auth Account account) {
Device device = account.getAuthenticatedDevice().get();
device.setApnId(null);
device.setFetchesMessages(false);
accounts.update(account);
}
@Timed
@PUT
@Path("/wsc/")
public void setWebSocketChannelSupported(@Auth Account account) {
Device device = account.getAuthenticatedDevice().get();
device.setFetchesMessages(true);
accounts.update(account);
}
@Timed
@DELETE
@Path("/wsc/")
public void deleteWebSocketChannel(@Auth Account account) {
Device device = account.getAuthenticatedDevice().get();
device.setFetchesMessages(false);
accounts.update(account);
}
@@ -213,8 +275,34 @@ public class AccountController {
encodedVerificationText)).build();
}
@VisibleForTesting protected VerificationCode generateVerificationCode() {
private void createAccount(String number, String password, AccountAttributes accountAttributes) {
Device device = new Device();
device.setId(Device.MASTER_ID);
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setSignalingKey(accountAttributes.getSignalingKey());
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
device.setName(accountAttributes.getName());
device.setCreated(System.currentTimeMillis());
device.setLastSeen(Util.todayInMillis());
Account account = new Account();
account.setNumber(number);
account.addDevice(device);
accounts.create(account);
messagesManager.clear(number);
pendingAccounts.remove(number);
logger.debug("Stored device...");
}
@VisibleForTesting protected VerificationCode generateVerificationCode(String number) {
try {
if (testDevices.containsKey(number)) {
return new VerificationCode(testDevices.get(number));
}
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
int randomInt = 100000 + random.nextInt(900000);
return new VerificationCode(randomInt);

View File

@@ -17,9 +17,8 @@
package org.whispersystems.textsecuregcm.controllers;
import com.amazonaws.HttpMethod;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptor;
@@ -44,6 +43,8 @@ import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import io.dropwizard.auth.Auth;
@Path("/v1/attachments")
public class AttachmentController {

View File

@@ -16,26 +16,29 @@
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
import org.whispersystems.textsecuregcm.entities.DeviceInfoList;
import org.whispersystems.textsecuregcm.entities.DeviceResponse;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
@@ -45,14 +48,21 @@ import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import io.dropwizard.auth.Auth;
@Path("/v1/devices")
public class DeviceController {
private final Logger logger = LoggerFactory.getLogger(DeviceController.class);
private static final int MAX_DEVICES = 3;
private final PendingDevicesManager pendingDevices;
private final AccountsManager accounts;
private final RateLimiters rateLimiters;
@@ -68,13 +78,47 @@ public class DeviceController {
@Timed
@GET
@Path("/provisioning_code")
@Produces(MediaType.APPLICATION_JSON)
public DeviceInfoList getDevices(@Auth Account account) {
List<DeviceInfo> devices = new LinkedList<>();
for (Device device : account.getDevices()) {
devices.add(new DeviceInfo(device.getId(), device.getName(),
device.getLastSeen(), device.getCreated()));
}
return new DeviceInfoList(devices);
}
@Timed
@DELETE
@Path("/{device_id}")
public void removeDevice(@Auth Account account, @PathParam("device_id") long deviceId) {
if (account.getAuthenticatedDevice().get().getId() != Device.MASTER_ID) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
account.removeDevice(deviceId);
accounts.update(account);
}
@Timed
@GET
@Path("/provisioning/code")
@Produces(MediaType.APPLICATION_JSON)
public VerificationCode createDeviceToken(@Auth Account account)
throws RateLimitExceededException
throws RateLimitExceededException, DeviceLimitExceededException
{
rateLimiters.getAllocateDeviceLimiter().validate(account.getNumber());
if (account.getActiveDeviceCount() >= MAX_DEVICES) {
throw new DeviceLimitExceededException(account.getDevices().size(), MAX_DEVICES);
}
if (account.getAuthenticatedDevice().get().getId() != Device.MASTER_ID) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
VerificationCode verificationCode = generateVerificationCode();
pendingDevices.store(account.getNumber(), verificationCode.getVerificationCode());
@@ -89,7 +133,7 @@ public class DeviceController {
public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode,
@HeaderParam("Authorization") String authorizationHeader,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
throws RateLimitExceededException, DeviceLimitExceededException
{
try {
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
@@ -101,7 +145,7 @@ public class DeviceController {
Optional<String> storedVerificationCode = pendingDevices.getCodeForNumber(number);
if (!storedVerificationCode.isPresent() ||
!verificationCode.equals(storedVerificationCode.get()))
!MessageDigest.isEqual(verificationCode.getBytes(), storedVerificationCode.get().getBytes()))
{
throw new WebApplicationException(Response.status(403).build());
}
@@ -112,11 +156,19 @@ public class DeviceController {
throw new WebApplicationException(Response.status(403).build());
}
if (account.get().getActiveDeviceCount() >= MAX_DEVICES) {
throw new DeviceLimitExceededException(account.get().getDevices().size(), MAX_DEVICES);
}
Device device = new Device();
device.setName(accountAttributes.getName());
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setSignalingKey(accountAttributes.getSignalingKey());
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setId(account.get().getNextDeviceId());
device.setRegistrationId(accountAttributes.getRegistrationId());
device.setLastSeen(Util.todayInMillis());
device.setCreated(System.currentTimeMillis());
account.get().addDevice(device);
accounts.update(account.get());

View File

@@ -0,0 +1,21 @@
package org.whispersystems.textsecuregcm.controllers;
public class DeviceLimitExceededException extends Exception {
private final int currentDevices;
private final int maxDevices;
public DeviceLimitExceededException(int currentDevices, int maxDevices) {
this.currentDevices = currentDevices;
this.maxDevices = maxDevices;
}
public int getCurrentDevices() {
return currentDevices;
}
public int getMaxDevices() {
return maxDevices;
}
}

View File

@@ -16,11 +16,11 @@
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.annotation.Timed;
import com.yammer.metrics.core.Histogram;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact;
@@ -30,6 +30,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.Constants;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
@@ -45,11 +46,15 @@ import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.Auth;
@Path("/v1/directory")
public class DirectoryController {
private final Logger logger = LoggerFactory.getLogger(DirectoryController.class);
private final Histogram contactsHistogram = Metrics.newHistogram(DirectoryController.class, "contacts");
private final Logger logger = LoggerFactory.getLogger(DirectoryController.class);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Histogram contactsHistogram = metricRegistry.histogram(name(getClass(), "contacts"));
private final RateLimiters rateLimiters;
private final DirectoryManager directory;
@@ -69,7 +74,7 @@ public class DirectoryController {
rateLimiters.getContactsLimiter().validate(account.getNumber());
try {
Optional<ClientContact> contact = directory.get(Base64.decodeWithoutPadding(token));
Optional<ClientContact> contact = directory.get(decodeToken(token));
if (contact.isPresent()) return Response.ok().entity(contact.get()).build();
else return Response.status(404).build();
@@ -95,7 +100,7 @@ public class DirectoryController {
List<byte[]> tokens = new LinkedList<>();
for (String encodedContact : contacts.getContacts()) {
tokens.add(Base64.decodeWithoutPadding(encodedContact));
tokens.add(decodeToken(encodedContact));
}
List<ClientContact> intersection = directory.get(tokens);
@@ -105,4 +110,8 @@ public class DirectoryController {
throw new WebApplicationException(Response.status(400).build());
}
}
private byte[] decodeToken(String encoded) throws IOException {
return Base64.decodeWithoutPadding(encoded.replace('-', '+').replace('_', '/'));
}
}

View File

@@ -1,167 +1,19 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.controllers;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AccountCount;
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.federation.NonLimitedAccount;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.util.Util;
import javax.validation.Valid;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
@Path("/v1/federation")
public class FederationController {
private final Logger logger = LoggerFactory.getLogger(FederationController.class);
protected final AccountsManager accounts;
protected final AttachmentController attachmentController;
protected final MessageController messageController;
private static final int ACCOUNT_CHUNK_SIZE = 10000;
private final AccountsManager accounts;
private final AttachmentController attachmentController;
private final KeysController keysController;
private final MessageController messageController;
public FederationController(AccountsManager accounts,
public FederationController(AccountsManager accounts,
AttachmentController attachmentController,
KeysController keysController,
MessageController messageController)
MessageController messageController)
{
this.accounts = accounts;
this.attachmentController = attachmentController;
this.keysController = keysController;
this.messageController = messageController;
}
@Timed
@GET
@Path("/attachment/{attachmentId}")
@Produces(MediaType.APPLICATION_JSON)
public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer,
@PathParam("attachmentId") long attachmentId)
throws IOException
{
return attachmentController.redirectToAttachment(new NonLimitedAccount("Unknown", -1, peer.getName()),
attachmentId, Optional.<String>absent());
}
@Timed
@GET
@Path("/key/{number}")
@Produces(MediaType.APPLICATION_JSON)
public PreKey getKey(@Auth FederatedPeer peer,
@PathParam("number") String number)
throws IOException
{
try {
return keysController.get(new NonLimitedAccount("Unknown", -1, peer.getName()), number, Optional.<String>absent());
} catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e);
throw new IOException(e);
}
}
@Timed
@GET
@Path("/key/{number}/{device}")
@Produces(MediaType.APPLICATION_JSON)
public UnstructuredPreKeyList getKeys(@Auth FederatedPeer peer,
@PathParam("number") String number,
@PathParam("device") String device)
throws IOException
{
try {
return keysController.getDeviceKey(new NonLimitedAccount("Unknown", -1, peer.getName()),
number, device, Optional.<String>absent());
} catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e);
throw new IOException(e);
}
}
@Timed
@PUT
@Path("/messages/{source}/{sourceDeviceId}/{destination}")
public void sendMessages(@Auth FederatedPeer peer,
@PathParam("source") String source,
@PathParam("sourceDeviceId") long sourceDeviceId,
@PathParam("destination") String destination,
@Valid IncomingMessageList messages)
throws IOException
{
try {
messages.setRelay(null);
messageController.sendMessage(new NonLimitedAccount(source, sourceDeviceId, peer.getName()), destination, messages);
} catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e);
throw new IOException(e);
}
}
@Timed
@GET
@Path("/user_count")
@Produces(MediaType.APPLICATION_JSON)
public AccountCount getUserCount(@Auth FederatedPeer peer) {
return new AccountCount((int)accounts.getCount());
}
@Timed
@GET
@Path("/user_tokens/{offset}")
@Produces(MediaType.APPLICATION_JSON)
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
@PathParam("offset") int offset)
{
List<Account> accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE);
List<ClientContact> clientContacts = new LinkedList<>();
for (Account account : accountList) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
if (!account.isActive()) {
clientContact.setInactive(true);
}
clientContacts.add(clientContact);
}
return new ClientContacts(clientContacts);
}
}

View File

@@ -0,0 +1,164 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AccountCount;
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.federation.NonLimitedAccount;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.util.Util;
import javax.validation.Valid;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import io.dropwizard.auth.Auth;
@Path("/v1/federation")
public class FederationControllerV1 extends FederationController {
private final Logger logger = LoggerFactory.getLogger(FederationControllerV1.class);
private static final int ACCOUNT_CHUNK_SIZE = 10000;
private final KeysControllerV1 keysControllerV1;
public FederationControllerV1(AccountsManager accounts,
AttachmentController attachmentController,
MessageController messageController,
KeysControllerV1 keysControllerV1)
{
super(accounts, attachmentController, messageController);
this.keysControllerV1 = keysControllerV1;
}
@Timed
@GET
@Path("/attachment/{attachmentId}")
@Produces(MediaType.APPLICATION_JSON)
public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer,
@PathParam("attachmentId") long attachmentId)
throws IOException
{
return attachmentController.redirectToAttachment(new NonLimitedAccount("Unknown", -1, peer.getName()),
attachmentId, Optional.<String>absent());
}
@Timed
@GET
@Path("/key/{number}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyV1> getKey(@Auth FederatedPeer peer,
@PathParam("number") String number)
throws IOException
{
try {
return keysControllerV1.get(new NonLimitedAccount("Unknown", -1, peer.getName()),
number, Optional.<String>absent());
} catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e);
throw new IOException(e);
}
}
@Timed
@GET
@Path("/key/{number}/{device}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyResponseV1> getKeysV1(@Auth FederatedPeer peer,
@PathParam("number") String number,
@PathParam("device") String device)
throws IOException
{
try {
return keysControllerV1.getDeviceKey(new NonLimitedAccount("Unknown", -1, peer.getName()),
number, device, Optional.<String>absent());
} catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e);
throw new IOException(e);
}
}
@Timed
@PUT
@Path("/messages/{source}/{sourceDeviceId}/{destination}")
public void sendMessages(@Auth FederatedPeer peer,
@PathParam("source") String source,
@PathParam("sourceDeviceId") long sourceDeviceId,
@PathParam("destination") String destination,
@Valid IncomingMessageList messages)
throws IOException
{
try {
messages.setRelay(null);
messageController.sendMessage(new NonLimitedAccount(source, sourceDeviceId, peer.getName()), destination, messages);
} catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e);
throw new IOException(e);
}
}
@Timed
@GET
@Path("/user_count")
@Produces(MediaType.APPLICATION_JSON)
public AccountCount getUserCount(@Auth FederatedPeer peer) {
return new AccountCount((int)accounts.getCount());
}
@Timed
@GET
@Path("/user_tokens/{offset}")
@Produces(MediaType.APPLICATION_JSON)
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
@PathParam("offset") int offset)
{
List<Account> accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE);
List<ClientContact> clientContacts = new LinkedList<>();
for (Account account : accountList) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null);
if (!account.isActive()) {
clientContact.setInactive(true);
}
clientContacts.add(clientContact);
}
return new ClientContacts(clientContacts);
}
}

View File

@@ -0,0 +1,51 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.federation.NonLimitedAccount;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import io.dropwizard.auth.Auth;
@Path("/v2/federation")
public class FederationControllerV2 extends FederationController {
private final Logger logger = LoggerFactory.getLogger(FederationControllerV2.class);
private final KeysControllerV2 keysControllerV2;
public FederationControllerV2(AccountsManager accounts, AttachmentController attachmentController, MessageController messageController, KeysControllerV2 keysControllerV2) {
super(accounts, attachmentController, messageController);
this.keysControllerV2 = keysControllerV2;
}
@Timed
@GET
@Path("/key/{number}/{device}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyResponseV2> getKeysV2(@Auth FederatedPeer peer,
@PathParam("number") String number,
@PathParam("device") String device)
throws IOException
{
try {
return keysControllerV2.getDeviceKeys(new NonLimitedAccount("Unknown", -1, peer.getName()),
number, device, Optional.<String>absent());
} catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e);
throw new IOException(e);
}
}
}

View File

@@ -0,0 +1,7 @@
package org.whispersystems.textsecuregcm.controllers;
public class InvalidDestinationException extends Exception {
public InvalidDestinationException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,48 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import org.whispersystems.websocket.session.WebSocketSession;
import org.whispersystems.websocket.session.WebSocketSessionContext;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import io.dropwizard.auth.Auth;
@Path("/v1/keepalive")
public class KeepAliveController {
private final Logger logger = LoggerFactory.getLogger(KeepAliveController.class);
private final PubSubManager pubSubManager;
public KeepAliveController(PubSubManager pubSubManager) {
this.pubSubManager = pubSubManager;
}
@Timed
@GET
public Response getKeepAlive(@Auth(required = false) Account account,
@WebSocketSession WebSocketSessionContext context)
{
if (account != null) {
WebsocketAddress address = new WebsocketAddress(account.getNumber(),
account.getAuthenticatedDevice().get().getId());
if (!pubSubManager.hasLocalSubscription(address)) {
logger.warn("***** No local subscription found for: " + address);
context.getClient().close(1000, "OK");
}
}
return Response.ok().build();
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (C) 2013 Open WhisperSystems
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -16,46 +16,32 @@
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.PreKeyList;
import org.whispersystems.textsecuregcm.entities.PreKeyStatus;
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
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.KeyRecord;
import org.whispersystems.textsecuregcm.storage.Keys;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.LinkedList;
import java.util.List;
@Path("/v1/keys")
import io.dropwizard.auth.Auth;
public class KeysController {
private final Logger logger = LoggerFactory.getLogger(KeysController.class);
private final RateLimiters rateLimiters;
private final Keys keys;
private final AccountsManager accounts;
private final FederatedClientManager federatedClientManager;
protected final RateLimiters rateLimiters;
protected final Keys keys;
protected final AccountsManager accounts;
protected final FederatedClientManager federatedClientManager;
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
FederatedClientManager federatedClientManager)
@@ -66,112 +52,65 @@ public class KeysController {
this.federatedClientManager = federatedClientManager;
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void setKeys(@Auth Account account, @Valid PreKeyList preKeys) {
Device device = account.getAuthenticatedDevice().get();
keys.store(account.getNumber(), device.getId(), preKeys.getKeys(), preKeys.getLastResortKey());
}
@Timed
@GET
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public PreKeyStatus getStatus(@Auth Account account) {
public PreKeyCount getStatus(@Auth Account account) {
int count = keys.getCount(account.getNumber(), account.getAuthenticatedDevice().get().getId());
if (count > 0) {
count = count - 1;
}
return new PreKeyStatus(count);
return new PreKeyCount(count);
}
@Timed
@GET
@Path("/{number}/{device_id}")
@Produces(MediaType.APPLICATION_JSON)
public UnstructuredPreKeyList getDeviceKey(@Auth Account account,
@PathParam("number") String number,
@PathParam("device_id") String deviceId,
@QueryParam("relay") Optional<String> relay)
throws RateLimitExceededException
protected TargetKeys getLocalKeys(String number, String deviceIdSelector)
throws NoSuchUserException
{
try {
if (account.isRateLimited()) {
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number + "." + deviceId);
}
Optional<UnstructuredPreKeyList> results;
if (!relay.isPresent()) results = getLocalKeys(number, deviceId);
else results = federatedClientManager.getClient(relay.get()).getKeys(number, deviceId);
if (results.isPresent()) return results.get();
else throw new WebApplicationException(Response.status(404).build());
} catch (NoSuchPeerException e) {
throw new WebApplicationException(Response.status(404).build());
}
}
@Timed
@GET
@Path("/{number}")
@Produces(MediaType.APPLICATION_JSON)
public PreKey get(@Auth Account account,
@PathParam("number") String number,
@QueryParam("relay") Optional<String> relay)
throws RateLimitExceededException
{
UnstructuredPreKeyList results = getDeviceKey(account, number, String.valueOf(Device.MASTER_ID), relay);
return results.getKeys().get(0);
}
private Optional<UnstructuredPreKeyList> getLocalKeys(String number, String deviceIdSelector) {
Optional<Account> destination = accounts.get(number);
if (!destination.isPresent() || !destination.get().isActive()) {
return Optional.absent();
throw new NoSuchUserException("Target account is inactive");
}
try {
if (deviceIdSelector.equals("*")) {
Optional<UnstructuredPreKeyList> preKeys = keys.get(number);
return getActiveKeys(destination.get(), preKeys);
Optional<List<KeyRecord>> preKeys = keys.get(number);
return new TargetKeys(destination.get(), preKeys);
}
long deviceId = Long.parseLong(deviceIdSelector);
Optional<Device> targetDevice = destination.get().getDevice(deviceId);
if (!targetDevice.isPresent() || !targetDevice.get().isActive()) {
return Optional.absent();
throw new NoSuchUserException("Target device is inactive.");
}
Optional<UnstructuredPreKeyList> preKeys = keys.get(number, deviceId);
return getActiveKeys(destination.get(), preKeys);
Optional<List<KeyRecord>> preKeys = keys.get(number, deviceId);
return new TargetKeys(destination.get(), preKeys);
} catch (NumberFormatException e) {
throw new WebApplicationException(Response.status(422).build());
}
}
private Optional<UnstructuredPreKeyList> getActiveKeys(Account destination,
Optional<UnstructuredPreKeyList> preKeys)
{
if (!preKeys.isPresent()) return Optional.absent();
List<PreKey> filteredKeys = new LinkedList<>();
public static class TargetKeys {
private final Account destination;
private final Optional<List<KeyRecord>> keys;
for (PreKey preKey : preKeys.get().getKeys()) {
Optional<Device> device = destination.getDevice(preKey.getDeviceId());
if (device.isPresent() && device.get().isActive()) {
preKey.setRegistrationId(device.get().getRegistrationId());
filteredKeys.add(preKey);
}
public TargetKeys(Account destination, Optional<List<KeyRecord>> keys) {
this.destination = destination;
this.keys = keys;
}
if (filteredKeys.isEmpty()) return Optional.absent();
else return Optional.of(new UnstructuredPreKeyList(filteredKeys));
public Optional<List<KeyRecord>> getKeys() {
return keys;
}
public Account getDestination() {
return destination;
}
}
}

View File

@@ -0,0 +1,136 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
import org.whispersystems.textsecuregcm.entities.PreKeyStateV1;
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
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.KeyRecord;
import org.whispersystems.textsecuregcm.storage.Keys;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.LinkedList;
import java.util.List;
import io.dropwizard.auth.Auth;
@Path("/v1/keys")
public class KeysControllerV1 extends KeysController {
private final Logger logger = LoggerFactory.getLogger(KeysControllerV1.class);
public KeysControllerV1(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
FederatedClientManager federatedClientManager)
{
super(rateLimiters, keys, accounts, federatedClientManager);
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void setKeys(@Auth Account account, @Valid PreKeyStateV1 preKeys) {
Device device = account.getAuthenticatedDevice().get();
String identityKey = preKeys.getLastResortKey().getIdentityKey();
if (!identityKey.equals(account.getIdentityKey())) {
account.setIdentityKey(identityKey);
accounts.update(account);
}
keys.store(account.getNumber(), device.getId(), preKeys.getKeys(), preKeys.getLastResortKey());
}
@Timed
@GET
@Path("/{number}/{device_id}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyResponseV1> getDeviceKey(@Auth Account account,
@PathParam("number") String number,
@PathParam("device_id") String deviceId,
@QueryParam("relay") Optional<String> relay)
throws RateLimitExceededException
{
try {
if (account.isRateLimited()) {
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number + "." + deviceId);
}
if (relay.isPresent()) {
return federatedClientManager.getClient(relay.get()).getKeysV1(number, deviceId);
}
TargetKeys targetKeys = getLocalKeys(number, deviceId);
if (!targetKeys.getKeys().isPresent()) {
return Optional.absent();
}
List<PreKeyV1> preKeys = new LinkedList<>();
Account destination = targetKeys.getDestination();
for (KeyRecord record : targetKeys.getKeys().get()) {
Optional<Device> device = destination.getDevice(record.getDeviceId());
if (device.isPresent() && device.get().isActive()) {
preKeys.add(new PreKeyV1(record.getDeviceId(), record.getKeyId(),
record.getPublicKey(), destination.getIdentityKey(),
device.get().getRegistrationId()));
}
}
if (preKeys.isEmpty()) return Optional.absent();
else return Optional.of(new PreKeyResponseV1(preKeys));
} catch (NoSuchPeerException | NoSuchUserException e) {
throw new WebApplicationException(Response.status(404).build());
}
}
@Timed
@GET
@Path("/{number}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyV1> get(@Auth Account account,
@PathParam("number") String number,
@QueryParam("relay") Optional<String> relay)
throws RateLimitExceededException
{
Optional<PreKeyResponseV1> results = getDeviceKey(account, number, String.valueOf(Device.MASTER_ID), relay);
if (results.isPresent()) return Optional.of(results.get().getKeys().get(0));
else return Optional.absent();
}
}

View File

@@ -0,0 +1,156 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItemV2;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
import org.whispersystems.textsecuregcm.entities.PreKeyStateV2;
import org.whispersystems.textsecuregcm.entities.PreKeyV2;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
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.KeyRecord;
import org.whispersystems.textsecuregcm.storage.Keys;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.LinkedList;
import java.util.List;
import io.dropwizard.auth.Auth;
@Path("/v2/keys")
public class KeysControllerV2 extends KeysController {
public KeysControllerV2(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
FederatedClientManager federatedClientManager)
{
super(rateLimiters, keys, accounts, federatedClientManager);
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void setKeys(@Auth Account account, @Valid PreKeyStateV2 preKeys) {
Device device = account.getAuthenticatedDevice().get();
boolean updateAccount = false;
if (!preKeys.getSignedPreKey().equals(device.getSignedPreKey())) {
device.setSignedPreKey(preKeys.getSignedPreKey());
updateAccount = true;
}
if (!preKeys.getIdentityKey().equals(account.getIdentityKey())) {
account.setIdentityKey(preKeys.getIdentityKey());
updateAccount = true;
}
if (updateAccount) {
accounts.update(account);
}
keys.store(account.getNumber(), device.getId(), preKeys.getPreKeys(), preKeys.getLastResortKey());
}
@Timed
@GET
@Path("/{number}/{device_id}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyResponseV2> getDeviceKeys(@Auth Account account,
@PathParam("number") String number,
@PathParam("device_id") String deviceId,
@QueryParam("relay") Optional<String> relay)
throws RateLimitExceededException
{
try {
if (account.isRateLimited()) {
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number + "." + deviceId);
}
if (relay.isPresent()) {
return federatedClientManager.getClient(relay.get()).getKeysV2(number, deviceId);
}
TargetKeys targetKeys = getLocalKeys(number, deviceId);
Account destination = targetKeys.getDestination();
List<PreKeyResponseItemV2> devices = new LinkedList<>();
for (Device device : destination.getDevices()) {
if (device.isActive() && (deviceId.equals("*") || device.getId() == Long.parseLong(deviceId))) {
SignedPreKey signedPreKey = device.getSignedPreKey();
PreKeyV2 preKey = null;
if (targetKeys.getKeys().isPresent()) {
for (KeyRecord keyRecord : targetKeys.getKeys().get()) {
if (keyRecord.getDeviceId() == device.getId()) {
preKey = new PreKeyV2(keyRecord.getKeyId(), keyRecord.getPublicKey());
}
}
}
if (signedPreKey != null || preKey != null) {
devices.add(new PreKeyResponseItemV2(device.getId(), device.getRegistrationId(), signedPreKey, preKey));
}
}
}
if (devices.isEmpty()) return Optional.absent();
else return Optional.of(new PreKeyResponseV2(destination.getIdentityKey(), devices));
} catch (NoSuchPeerException | NoSuchUserException e) {
throw new WebApplicationException(Response.status(404).build());
}
}
@Timed
@PUT
@Path("/signed")
@Consumes(MediaType.APPLICATION_JSON)
public void setSignedKey(@Auth Account account, @Valid SignedPreKey signedPreKey) {
Device device = account.getAuthenticatedDevice().get();
device.setSignedPreKey(signedPreKey);
accounts.update(account);
}
@Timed
@GET
@Path("/signed")
@Produces(MediaType.APPLICATION_JSON)
public Optional<SignedPreKey> getSignedKey(@Auth Account account) {
Device device = account.getAuthenticatedDevice().get();
SignedPreKey signedPreKey = device.getSignedPreKey();
if (signedPreKey != null) return Optional.of(signedPreKey);
else return Optional.absent();
}
}

View File

@@ -16,17 +16,19 @@
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import com.google.protobuf.ByteString;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.entities.MessageResponse;
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
import org.whispersystems.textsecuregcm.entities.SendMessageResponse;
import org.whispersystems.textsecuregcm.entities.StaleDevices;
import org.whispersystems.textsecuregcm.federation.FederatedClient;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
@@ -34,14 +36,19 @@ import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.Util;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
@@ -56,6 +63,8 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import io.dropwizard.auth.Auth;
@Path("/v1/messages")
public class MessageController {
@@ -63,17 +72,23 @@ public class MessageController {
private final RateLimiters rateLimiters;
private final PushSender pushSender;
private final ReceiptSender receiptSender;
private final FederatedClientManager federatedClientManager;
private final AccountsManager accountsManager;
private final MessagesManager messagesManager;
public MessageController(RateLimiters rateLimiters,
PushSender pushSender,
ReceiptSender receiptSender,
AccountsManager accountsManager,
MessagesManager messagesManager,
FederatedClientManager federatedClientManager)
{
this.rateLimiters = rateLimiters;
this.pushSender = pushSender;
this.receiptSender = receiptSender;
this.accountsManager = accountsManager;
this.messagesManager = messagesManager;
this.federatedClientManager = federatedClientManager;
}
@@ -81,16 +96,21 @@ public class MessageController {
@Path("/{destination}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void sendMessage(@Auth Account source,
@PathParam("destination") String destinationName,
@Valid IncomingMessageList messages)
@Produces(MediaType.APPLICATION_JSON)
public SendMessageResponse sendMessage(@Auth Account source,
@PathParam("destination") String destinationName,
@Valid IncomingMessageList messages)
throws IOException, RateLimitExceededException
{
rateLimiters.getMessagesLimiter().validate(source.getNumber());
rateLimiters.getMessagesLimiter().validate(source.getNumber() + "__" + destinationName);
try {
if (messages.getRelay() == null) sendLocalMessage(source, destinationName, messages);
else sendRelayMessage(source, destinationName, messages);
boolean isSyncMessage = source.getNumber().equals(destinationName);
if (Util.isEmpty(messages.getRelay())) sendLocalMessage(source, destinationName, messages, isSyncMessage);
else sendRelayMessage(source, destinationName, messages, isSyncMessage);
return new SendMessageResponse(!isSyncMessage && source.getActiveDeviceCount() > 1);
} catch (NoSuchUserException e) {
throw new WebApplicationException(Response.status(404).build());
} catch (MismatchedDevicesException e) {
@@ -104,6 +124,8 @@ public class MessageController {
.type(MediaType.APPLICATION_JSON)
.entity(new StaleDevices(e.getStaleDevices()))
.build());
} catch (InvalidDestinationException e) {
throw new WebApplicationException(Response.status(400).build());
}
}
@@ -127,21 +149,57 @@ public class MessageController {
}
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
public OutgoingMessageEntityList getPendingMessages(@Auth Account account) {
return new OutgoingMessageEntityList(messagesManager.getMessagesForDevice(account.getNumber(),
account.getAuthenticatedDevice()
.get().getId()));
}
@Timed
@DELETE
@Path("/{source}/{timestamp}")
public void removePendingMessage(@Auth Account account,
@PathParam("source") String source,
@PathParam("timestamp") long timestamp)
throws IOException
{
try {
Optional<OutgoingMessageEntity> message = messagesManager.delete(account.getNumber(), source, timestamp);
if (message.isPresent() && message.get().getType() != Envelope.Type.RECEIPT_VALUE) {
receiptSender.sendReceipt(account,
message.get().getSource(),
message.get().getTimestamp(),
Optional.fromNullable(message.get().getRelay()));
}
} catch (NoSuchUserException | NotPushRegisteredException | TransientPushFailureException e) {
logger.warn("Sending delivery receipt", e);
}
}
private void sendLocalMessage(Account source,
String destinationName,
IncomingMessageList messages)
IncomingMessageList messages,
boolean isSyncMessage)
throws NoSuchUserException, MismatchedDevicesException, IOException, StaleDevicesException
{
Account destination = getDestinationAccount(destinationName);
Account destination;
validateCompleteDeviceList(destination, messages.getMessages());
if (!isSyncMessage) destination = getDestinationAccount(destinationName);
else destination = source;
validateCompleteDeviceList(destination, messages.getMessages(), isSyncMessage);
validateRegistrationIds(destination, messages.getMessages());
for (IncomingMessage incomingMessage : messages.getMessages()) {
Optional<Device> destinationDevice = destination.getDevice(incomingMessage.getDestinationDeviceId());
if (destinationDevice.isPresent()) {
sendLocalMessage(source, destination, destinationDevice.get(), incomingMessage);
sendLocalMessage(source, destination, destinationDevice.get(), messages.getTimestamp(), incomingMessage);
}
}
}
@@ -149,20 +207,26 @@ public class MessageController {
private void sendLocalMessage(Account source,
Account destinationAccount,
Device destinationDevice,
long timestamp,
IncomingMessage incomingMessage)
throws NoSuchUserException, IOException
{
try {
Optional<byte[]> messageBody = getMessageBody(incomingMessage);
OutgoingMessageSignal.Builder messageBuilder = OutgoingMessageSignal.newBuilder();
Optional<byte[]> messageBody = getMessageBody(incomingMessage);
Optional<byte[]> messageContent = getMessageContent(incomingMessage);
Envelope.Builder messageBuilder = Envelope.newBuilder();
messageBuilder.setType(incomingMessage.getType())
messageBuilder.setType(Envelope.Type.valueOf(incomingMessage.getType()))
.setSource(source.getNumber())
.setTimestamp(System.currentTimeMillis())
.setSourceDevice((int)source.getAuthenticatedDevice().get().getId());
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId());
if (messageBody.isPresent()) {
messageBuilder.setMessage(ByteString.copyFrom(messageBody.get()));
messageBuilder.setLegacyMessage(ByteString.copyFrom(messageBody.get()));
}
if (messageContent.isPresent()) {
messageBuilder.setContent(ByteString.copyFrom(messageContent.get()));
}
if (source.getRelay().isPresent()) {
@@ -181,9 +245,12 @@ public class MessageController {
private void sendRelayMessage(Account source,
String destinationName,
IncomingMessageList messages)
throws IOException, NoSuchUserException
IncomingMessageList messages,
boolean isSyncMessage)
throws IOException, NoSuchUserException, InvalidDestinationException
{
if (isSyncMessage) throw new InvalidDestinationException("Transcript messages can't be relayed!");
try {
FederatedClient client = federatedClientManager.getClient(messages.getRelay());
client.sendMessages(source.getNumber(), source.getAuthenticatedDevice().get().getId(),
@@ -226,7 +293,9 @@ public class MessageController {
}
}
private void validateCompleteDeviceList(Account account, List<IncomingMessage> messages)
private void validateCompleteDeviceList(Account account,
List<IncomingMessage> messages,
boolean isSyncMessage)
throws MismatchedDevicesException
{
Set<Long> messageDeviceIds = new HashSet<>();
@@ -240,7 +309,9 @@ public class MessageController {
}
for (Device device : account.getDevices()) {
if (device.isActive()) {
if (device.isActive() &&
!(isSyncMessage && device.getId() == account.getAuthenticatedDevice().get().getId()))
{
accountDeviceIds.add(device.getId());
if (!messageDeviceIds.contains(device.getId())) {
@@ -277,6 +348,8 @@ public class MessageController {
}
private Optional<byte[]> getMessageBody(IncomingMessage message) {
if (Util.isEmpty(message.getBody())) return Optional.absent();
try {
return Optional.of(Base64.decode(message.getBody()));
} catch (IOException ioe) {
@@ -284,4 +357,15 @@ public class MessageController {
return Optional.absent();
}
}
private Optional<byte[]> getMessageContent(IncomingMessage message) {
if (Util.isEmpty(message.getContent())) return Optional.absent();
try {
return Optional.of(Base64.decode(message.getContent()));
} catch (IOException ioe) {
logger.debug("Bad B64", ioe);
return Optional.absent();
}
}
}

View File

@@ -1,7 +1,6 @@
package org.whispersystems.textsecuregcm.controllers;
import java.util.List;
import java.util.Set;
public class MismatchedDevicesException extends Exception {

View File

@@ -16,8 +16,6 @@
*/
package org.whispersystems.textsecuregcm.controllers;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import java.util.LinkedList;
import java.util.List;
@@ -27,7 +25,7 @@ public class NoSuchUserException extends Exception {
public NoSuchUserException(String user) {
super(user);
missing = new LinkedList<String>();
missing = new LinkedList<>();
missing.add(user);
}

View File

@@ -0,0 +1,55 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import org.whispersystems.textsecuregcm.entities.ProvisioningMessage;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.WebsocketSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import io.dropwizard.auth.Auth;
@Path("/v1/provisioning")
public class ProvisioningController {
private final RateLimiters rateLimiters;
private final WebsocketSender websocketSender;
public ProvisioningController(RateLimiters rateLimiters, PushSender pushSender) {
this.rateLimiters = rateLimiters;
this.websocketSender = pushSender.getWebSocketSender();
}
@Timed
@Path("/{destination}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public void sendProvisioningMessage(@Auth Account source,
@PathParam("destination") String destinationName,
@Valid ProvisioningMessage message)
throws RateLimitExceededException, InvalidWebsocketAddressException, IOException
{
rateLimiters.getMessagesLimiter().validate(source.getNumber());
if (!websocketSender.sendProvisioningMessage(new ProvisioningAddress(destinationName, 0),
Base64.decode(message.getBody())))
{
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
}
}

View File

@@ -0,0 +1,47 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
import org.whispersystems.textsecuregcm.storage.Account;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.io.IOException;
import io.dropwizard.auth.Auth;
@Path("/v1/receipt")
public class ReceiptController {
private final ReceiptSender receiptSender;
public ReceiptController(ReceiptSender receiptSender) {
this.receiptSender = receiptSender;
}
@Timed
@PUT
@Path("/{destination}/{messageId}")
public void sendDeliveryReceipt(@Auth Account source,
@PathParam("destination") String destination,
@PathParam("messageId") long messageId,
@QueryParam("relay") Optional<String> relay)
throws IOException
{
try {
receiptSender.sendReceipt(source, destination, messageId, relay);
} catch (NoSuchUserException | NotPushRegisteredException e) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
} catch (TransientPushFailureException e) {
throw new IOException(e);
}
}
}

View File

@@ -1,155 +0,0 @@
package org.whispersystems.textsecuregcm.controllers;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.jetty.websocket.WebSocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AcknowledgeWebsocketMessage;
import org.whispersystems.textsecuregcm.entities.IncomingWebsocketMessage;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubListener;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import org.whispersystems.textsecuregcm.websocket.WebsocketMessage;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class WebsocketController implements WebSocket.OnTextMessage, PubSubListener {
private static final Logger logger = LoggerFactory.getLogger(WebsocketController.class);
private static final ObjectMapper mapper = new ObjectMapper();
private static final Map<Long, String> pendingMessages = new HashMap<>();
private final StoredMessageManager storedMessageManager;
private final PubSubManager pubSubManager;
private final Account account;
private final Device device;
private Connection connection;
private long pendingMessageSequence;
public WebsocketController(StoredMessageManager storedMessageManager,
PubSubManager pubSubManager,
Account account)
{
this.storedMessageManager = storedMessageManager;
this.pubSubManager = pubSubManager;
this.account = account;
this.device = account.getAuthenticatedDevice().get();
}
@Override
public void onOpen(Connection connection) {
this.connection = connection;
pubSubManager.subscribe(new WebsocketAddress(this.account.getId(), this.device.getId()), this);
handleQueryDatabase();
}
@Override
public void onClose(int i, String s) {
handleClose();
}
@Override
public void onMessage(String body) {
try {
IncomingWebsocketMessage incomingMessage = mapper.readValue(body, IncomingWebsocketMessage.class);
switch (incomingMessage.getType()) {
case IncomingWebsocketMessage.TYPE_ACKNOWLEDGE_MESSAGE: handleMessageAck(body); break;
case IncomingWebsocketMessage.TYPE_PING_MESSAGE: handlePing(); break;
default: handleClose(); break;
}
} catch (IOException e) {
logger.debug("Parse", e);
handleClose();
}
}
@Override
public void onPubSubMessage(PubSubMessage outgoingMessage) {
switch (outgoingMessage.getType()) {
case PubSubMessage.TYPE_DELIVER: handleDeliverOutgoingMessage(outgoingMessage.getContents()); break;
case PubSubMessage.TYPE_QUERY_DB: handleQueryDatabase(); break;
default:
logger.warn("Unknown pubsub message: " + outgoingMessage.getType());
}
}
private void handleDeliverOutgoingMessage(String message) {
try {
long messageSequence;
synchronized (pendingMessages) {
messageSequence = pendingMessageSequence++;
pendingMessages.put(messageSequence, message);
}
connection.sendMessage(mapper.writeValueAsString(new WebsocketMessage(messageSequence, message)));
} catch (IOException e) {
logger.debug("Response failed", e);
handleClose();
}
}
private void handleMessageAck(String message) {
try {
AcknowledgeWebsocketMessage ack = mapper.readValue(message, AcknowledgeWebsocketMessage.class);
synchronized (pendingMessages) {
pendingMessages.remove(ack.getId());
}
} catch (IOException e) {
logger.warn("Mapping", e);
}
}
private void handlePing() {
try {
IncomingWebsocketMessage pongMessage = new IncomingWebsocketMessage(IncomingWebsocketMessage.TYPE_PONG_MESSAGE);
connection.sendMessage(mapper.writeValueAsString(pongMessage));
} catch (IOException e) {
logger.warn("Pong failed", e);
handleClose();
}
}
private void handleClose() {
pubSubManager.unsubscribe(new WebsocketAddress(account.getId(), device.getId()), this);
connection.close();
List<String> remainingMessages = new LinkedList<>();
synchronized (pendingMessages) {
Long[] pendingKeys = pendingMessages.keySet().toArray(new Long[0]);
Arrays.sort(pendingKeys);
for (long pendingKey : pendingKeys) {
remainingMessages.add(pendingMessages.get(pendingKey));
}
pendingMessages.clear();
}
storedMessageManager.storeMessages(account, device, remainingMessages);
}
private void handleQueryDatabase() {
List<String> messages = storedMessageManager.getOutgoingMessages(account, device);
for (String message : messages) {
handleDeliverOutgoingMessage(message);
}
}
}

View File

@@ -1,98 +0,0 @@
package org.whispersystems.textsecuregcm.controllers;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.AuthenticationException;
import com.yammer.dropwizard.auth.basic.BasicCredentials;
import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
import javax.servlet.http.HttpServletRequest;
import java.util.LinkedHashMap;
import java.util.Map;
public class WebsocketControllerFactory extends WebSocketServlet {
private final Logger logger = LoggerFactory.getLogger(WebsocketControllerFactory.class);
private final StoredMessageManager storedMessageManager;
private final PubSubManager pubSubManager;
private final AccountAuthenticator accountAuthenticator;
private final LinkedHashMap<BasicCredentials, Optional<Account>> cache =
new LinkedHashMap<BasicCredentials, Optional<Account>>() {
@Override
protected boolean removeEldestEntry(Map.Entry<BasicCredentials, Optional<Account>> eldest) {
return size() > 10;
}
};
public WebsocketControllerFactory(AccountAuthenticator accountAuthenticator,
StoredMessageManager storedMessageManager,
PubSubManager pubSubManager)
{
this.accountAuthenticator = accountAuthenticator;
this.storedMessageManager = storedMessageManager;
this.pubSubManager = pubSubManager;
}
@Override
public WebSocket doWebSocketConnect(HttpServletRequest request, String s) {
try {
String username = request.getParameter("user");
String password = request.getParameter("password");
if (username == null || password == null) {
return null;
}
BasicCredentials credentials = new BasicCredentials(username, password);
Optional<Account> account = cache.remove(credentials);
if (account == null) {
account = accountAuthenticator.authenticate(new BasicCredentials(username, password));
}
if (!account.isPresent()) {
return null;
}
return new WebsocketController(storedMessageManager, pubSubManager, account.get());
} catch (AuthenticationException e) {
throw new AssertionError(e);
}
}
@Override
public boolean checkOrigin(HttpServletRequest request, String origin) {
try {
String username = request.getParameter("user");
String password = request.getParameter("password");
if (username == null || password == null) {
return false;
}
BasicCredentials credentials = new BasicCredentials(username, password);
Optional<Account> account = accountAuthenticator.authenticate(credentials);
if (!account.isPresent()) {
return false;
}
cache.put(credentials, account);
return true;
} catch (AuthenticationException e) {
logger.warn("Auth Failure", e);
return false;
}
}
}

View File

@@ -17,40 +17,47 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Max;
public class AccountAttributes {
@JsonProperty
@NotEmpty
private String signalingKey;
@JsonProperty
private boolean supportsSms;
@JsonProperty
private boolean fetchesMessages;
@JsonProperty
private int registrationId;
@JsonProperty
@Length(max = 50, message = "This field must be less than 50 characters")
private String name;
public AccountAttributes() {}
public AccountAttributes(String signalingKey, boolean supportsSms, boolean fetchesMessages, int registrationId) {
@VisibleForTesting
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId) {
this(signalingKey, fetchesMessages, registrationId, null);
}
@VisibleForTesting
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name) {
this.signalingKey = signalingKey;
this.supportsSms = supportsSms;
this.fetchesMessages = fetchesMessages;
this.registrationId = registrationId;
this.name = name;
}
public String getSignalingKey() {
return signalingKey;
}
public boolean getSupportsSms() {
return supportsSms;
}
public boolean getFetchesMessages() {
return fetchesMessages;
}
@@ -58,4 +65,8 @@ public class AccountAttributes {
public int getRegistrationId() {
return registrationId;
}
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,76 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
public class ApnMessage {
public static long MAX_EXPIRATION = Integer.MAX_VALUE * 1000L;
@JsonProperty
@NotEmpty
private String apnId;
@JsonProperty
@NotEmpty
private String number;
@JsonProperty
@Min(1)
private int deviceId;
@JsonProperty
@NotEmpty
private String message;
@JsonProperty
@NotNull
private boolean voip;
@JsonProperty
private long expirationTime;
public ApnMessage() {}
public ApnMessage(String apnId, String number, int deviceId, String message, boolean voip, long expirationTime) {
this.apnId = apnId;
this.number = number;
this.deviceId = deviceId;
this.message = message;
this.voip = voip;
this.expirationTime = expirationTime;
}
public ApnMessage(ApnMessage copy, String apnId, boolean voip, long expirationTime) {
this.apnId = apnId;
this.number = copy.number;
this.deviceId = copy.deviceId;
this.message = copy.message;
this.voip = voip;
this.expirationTime = expirationTime;
}
@VisibleForTesting
public String getApnId() {
return apnId;
}
@VisibleForTesting
public boolean isVoip() {
return voip;
}
@VisibleForTesting
public String getMessage() {
return message;
}
@VisibleForTesting
public long getExpirationTime() {
return expirationTime;
}
}

View File

@@ -25,7 +25,14 @@ public class ApnRegistrationId {
@NotEmpty
private String apnRegistrationId;
@JsonProperty
private String voipRegistrationId;
public String getApnRegistrationId() {
return apnRegistrationId;
}
public String getVoipRegistrationId() {
return voipRegistrationId;
}
}

View File

@@ -18,15 +18,10 @@ package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.gson.Gson;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.util.Arrays;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@@ -39,12 +34,10 @@ public class ClientContact {
private String relay;
private boolean inactive;
private boolean supportsSms;
public ClientContact(byte[] token, String relay, boolean supportsSms) {
this.token = token;
this.relay = relay;
this.supportsSms = supportsSms;
public ClientContact(byte[] token, String relay) {
this.token = token;
this.relay = relay;
}
public ClientContact() {}
@@ -61,10 +54,6 @@ public class ClientContact {
this.relay = relay;
}
public boolean isSupportsSms() {
return supportsSms;
}
public boolean isInactive() {
return inactive;
}
@@ -73,9 +62,9 @@ public class ClientContact {
this.inactive = inactive;
}
public String toString() {
return new Gson().toJson(this);
}
// public String toString() {
// return new Gson().toJson(this);
// }
@Override
public boolean equals(Object other) {
@@ -86,7 +75,6 @@ public class ClientContact {
return
Arrays.equals(this.token, that.token) &&
this.supportsSms == that.supportsSms &&
this.inactive == that.inactive &&
(this.relay == null ? (that.relay == null) : this.relay.equals(that.relay));
}

View File

@@ -0,0 +1,24 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DeviceInfo {
@JsonProperty
private long id;
@JsonProperty
private String name;
@JsonProperty
private long lastSeen;
@JsonProperty
private long created;
public DeviceInfo(long id, String name, long lastSeen, long created) {
this.id = id;
this.name = name;
this.lastSeen = lastSeen;
this.created = created;
}
}

View File

@@ -0,0 +1,15 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class DeviceInfoList {
@JsonProperty
private List<DeviceInfo> devices;
public DeviceInfoList(List<DeviceInfo> devices) {
this.devices = devices;
}
}

View File

@@ -18,7 +18,7 @@ package org.whispersystems.textsecuregcm.entities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.Util;
@@ -41,23 +41,26 @@ public class EncryptedOutgoingMessage {
private static final int MAC_KEY_SIZE = 20;
private static final int MAC_SIZE = 10;
private final OutgoingMessageSignal outgoingMessage;
private final String signalingKey;
private final byte[] serialized;
private final String serializedAndEncoded;
public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage,
String signalingKey)
public EncryptedOutgoingMessage(Envelope outgoingMessage, String signalingKey)
throws CryptoEncodingException
{
this.outgoingMessage = outgoingMessage;
this.signalingKey = signalingKey;
}
public String serialize() throws CryptoEncodingException {
byte[] plaintext = outgoingMessage.toByteArray();
SecretKeySpec cipherKey = getCipherKey (signalingKey);
SecretKeySpec macKey = getMacKey(signalingKey);
byte[] ciphertext = getCiphertext(plaintext, cipherKey, macKey);
return Base64.encodeBytes(ciphertext);
this.serialized = getCiphertext(plaintext, cipherKey, macKey);
this.serializedAndEncoded = Base64.encodeBytes(this.serialized);
}
public String toEncodedString() {
return serializedAndEncoded;
}
public byte[] toByteArray() {
return serialized;
}
private byte[] getCiphertext(byte[] plaintext, SecretKeySpec cipherKey, SecretKeySpec macKey)

View File

@@ -0,0 +1,42 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Min;
public class GcmMessage {
@JsonProperty
@NotEmpty
private String gcmId;
@JsonProperty
@NotEmpty
private String number;
@JsonProperty
@Min(1)
private int deviceId;
@JsonProperty
private String message;
@JsonProperty
private boolean receipt;
@JsonProperty
private boolean notification;
public GcmMessage() {}
public GcmMessage(String gcmId, String number, int deviceId, String message, boolean receipt, boolean notification) {
this.gcmId = gcmId;
this.number = number;
this.deviceId = deviceId;
this.message = message;
this.receipt = receipt;
this.notification = notification;
}
}

View File

@@ -25,9 +25,15 @@ public class GcmRegistrationId {
@NotEmpty
private String gcmRegistrationId;
@JsonProperty
private boolean webSocketChannel;
public String getGcmRegistrationId() {
return gcmRegistrationId;
}
public boolean isWebSocketChannel() {
return webSocketChannel;
}
}

View File

@@ -34,14 +34,16 @@ public class IncomingMessage {
private int destinationRegistrationId;
@JsonProperty
@NotEmpty
private String body;
@JsonProperty
private String content;
@JsonProperty
private String relay;
@JsonProperty
private long timestamp;
private long timestamp; // deprecated
public String getDestination() {
@@ -67,4 +69,8 @@ public class IncomingMessage {
public int getDestinationRegistrationId() {
return destinationRegistrationId;
}
public String getContent() {
return content;
}
}

View File

@@ -32,6 +32,9 @@ public class IncomingMessageList {
@JsonProperty
private String relay;
@JsonProperty
private long timestamp;
public IncomingMessageList() {}
public List<IncomingMessage> getMessages() {
@@ -45,4 +48,8 @@ public class IncomingMessageList {
public void setRelay(String relay) {
this.relay = relay;
}
public long getTimestamp() {
return timestamp;
}
}

View File

@@ -0,0 +1,80 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
public class OutgoingMessageEntity {
@JsonIgnore
private long id;
@JsonProperty
private int type;
@JsonProperty
private String relay;
@JsonProperty
private long timestamp;
@JsonProperty
private String source;
@JsonProperty
private int sourceDevice;
@JsonProperty
private byte[] message;
@JsonProperty
private byte[] content;
public OutgoingMessageEntity() {}
public OutgoingMessageEntity(long id, int type, String relay, long timestamp,
String source, int sourceDevice, byte[] message,
byte[] content)
{
this.id = id;
this.type = type;
this.relay = relay;
this.timestamp = timestamp;
this.source = source;
this.sourceDevice = sourceDevice;
this.message = message;
this.content = content;
}
public int getType() {
return type;
}
public String getRelay() {
return relay;
}
public long getTimestamp() {
return timestamp;
}
public String getSource() {
return source;
}
public int getSourceDevice() {
return sourceDevice;
}
public byte[] getMessage() {
return message;
}
public byte[] getContent() {
return content;
}
public long getId() {
return id;
}
}

View File

@@ -0,0 +1,23 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import java.util.List;
public class OutgoingMessageEntityList {
@JsonProperty
private List<OutgoingMessageEntity> messages;
public OutgoingMessageEntityList() {}
public OutgoingMessageEntityList(List<OutgoingMessageEntity> messages) {
this.messages = messages;
}
@VisibleForTesting
public List<OutgoingMessageEntity> getMessages() {
return messages;
}
}

View File

@@ -0,0 +1,8 @@
package org.whispersystems.textsecuregcm.entities;
public interface PreKeyBase {
public long getKeyId();
public String getPublicKey();
}

View File

@@ -3,16 +3,16 @@ package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
public class PreKeyStatus {
public class PreKeyCount {
@JsonProperty
private int count;
public PreKeyStatus(int count) {
public PreKeyCount(int count) {
this.count = count;
}
public PreKeyStatus() {}
public PreKeyCount() {}
public int getCount() {
return count;

View File

@@ -0,0 +1,64 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
public class PreKeyResponseItemV2 {
@JsonProperty
private long deviceId;
@JsonProperty
private int registrationId;
@JsonProperty
private SignedPreKey signedPreKey;
@JsonProperty
private PreKeyV2 preKey;
public PreKeyResponseItemV2() {}
public PreKeyResponseItemV2(long deviceId, int registrationId, SignedPreKey signedPreKey, PreKeyV2 preKey) {
this.deviceId = deviceId;
this.registrationId = registrationId;
this.signedPreKey = signedPreKey;
this.preKey = preKey;
}
@VisibleForTesting
public SignedPreKey getSignedPreKey() {
return signedPreKey;
}
@VisibleForTesting
public PreKeyV2 getPreKey() {
return preKey;
}
@VisibleForTesting
public int getRegistrationId() {
return registrationId;
}
@VisibleForTesting
public long getDeviceId() {
return deviceId;
}
}

View File

@@ -18,7 +18,6 @@ package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@@ -26,36 +25,36 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class UnstructuredPreKeyList {
public class PreKeyResponseV1 {
@JsonProperty
@NotNull
@Valid
private List<PreKey> keys;
private List<PreKeyV1> keys;
@VisibleForTesting
public UnstructuredPreKeyList() {}
public PreKeyResponseV1() {}
public UnstructuredPreKeyList(PreKey preKey) {
this.keys = new LinkedList<PreKey>();
public PreKeyResponseV1(PreKeyV1 preKey) {
this.keys = new LinkedList<>();
this.keys.add(preKey);
}
public UnstructuredPreKeyList(List<PreKey> preKeys) {
public PreKeyResponseV1(List<PreKeyV1> preKeys) {
this.keys = preKeys;
}
public List<PreKey> getKeys() {
public List<PreKeyV1> getKeys() {
return keys;
}
@VisibleForTesting
public boolean equals(Object o) {
if (!(o instanceof UnstructuredPreKeyList) ||
((UnstructuredPreKeyList) o).keys.size() != keys.size())
if (!(o instanceof PreKeyResponseV1) ||
((PreKeyResponseV1) o).keys.size() != keys.size())
return false;
Iterator<PreKey> otherKeys = ((UnstructuredPreKeyList) o).keys.iterator();
for (PreKey key : keys) {
Iterator<PreKeyV1> otherKeys = ((PreKeyResponseV1) o).keys.iterator();
for (PreKeyV1 key : keys) {
if (!otherKeys.next().equals(key))
return false;
}
@@ -64,7 +63,7 @@ public class UnstructuredPreKeyList {
public int hashCode() {
int ret = 0xFBA4C795 * keys.size();
for (PreKey key : keys)
for (PreKeyV1 key : keys)
ret ^= key.getPublicKey().hashCode();
return ret;
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import java.util.List;
public class PreKeyResponseV2 {
@JsonProperty
private String identityKey;
@JsonProperty
private List<PreKeyResponseItemV2> devices;
public PreKeyResponseV2() {}
public PreKeyResponseV2(String identityKey, List<PreKeyResponseItemV2> devices) {
this.identityKey = identityKey;
this.devices = devices;
}
@VisibleForTesting
public String getIdentityKey() {
return identityKey;
}
@VisibleForTesting
@JsonIgnore
public PreKeyResponseItemV2 getDevice(int deviceId) {
for (PreKeyResponseItemV2 device : devices) {
if (device.getDeviceId() == deviceId) return device;
}
return null;
}
@VisibleForTesting
@JsonIgnore
public int getDevicesCount() {
return devices.size();
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (C) 2013 Open WhisperSystems
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -17,28 +17,39 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import com.google.common.annotations.VisibleForTesting;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.List;
public class PreKeyList {
@JsonProperty
@NotNull
private PreKey lastResortKey;
public class PreKeyStateV1 {
@JsonProperty
@NotNull
@Valid
private List<PreKey> keys;
private PreKeyV1 lastResortKey;
public List<PreKey> getKeys() {
@JsonProperty
@NotNull
@Valid
private List<PreKeyV1> keys;
public List<PreKeyV1> getKeys() {
return keys;
}
public PreKey getLastResortKey() {
@VisibleForTesting
public void setKeys(List<PreKeyV1> keys) {
this.keys = keys;
}
public PreKeyV1 getLastResortKey() {
return lastResortKey;
}
@VisibleForTesting
public void setLastResortKey(PreKeyV1 lastResortKey) {
this.lastResortKey = lastResortKey;
}
}

View File

@@ -0,0 +1,76 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.List;
public class PreKeyStateV2 {
@JsonProperty
@NotNull
@Valid
private List<PreKeyV2> preKeys;
@JsonProperty
@NotNull
@Valid
private SignedPreKey signedPreKey;
@JsonProperty
@NotNull
@Valid
private PreKeyV2 lastResortKey;
@JsonProperty
@NotEmpty
private String identityKey;
public PreKeyStateV2() {}
@VisibleForTesting
public PreKeyStateV2(String identityKey, SignedPreKey signedPreKey,
List<PreKeyV2> keys, PreKeyV2 lastResortKey)
{
this.identityKey = identityKey;
this.signedPreKey = signedPreKey;
this.preKeys = keys;
this.lastResortKey = lastResortKey;
}
public List<PreKeyV2> getPreKeys() {
return preKeys;
}
public SignedPreKey getSignedPreKey() {
return signedPreKey;
}
public String getIdentityKey() {
return identityKey;
}
public PreKeyV2 getLastResortKey() {
return lastResortKey;
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (C) 2013 Open WhisperSystems
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -17,22 +17,14 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import javax.validation.constraints.NotNull;
import javax.xml.bind.annotation.XmlTransient;
import java.io.Serializable;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class PreKey {
@JsonIgnore
private long id;
@JsonIgnore
private String number;
public class PreKeyV1 implements PreKeyBase {
@JsonProperty
private long deviceId;
@@ -49,78 +41,43 @@ public class PreKey {
@NotNull
private String identityKey;
@JsonProperty
private boolean lastResort;
@JsonProperty
private int registrationId;
public PreKey() {}
public PreKeyV1() {}
public PreKey(long id, String number, long deviceId, long keyId,
String publicKey, String identityKey,
boolean lastResort)
public PreKeyV1(long deviceId, long keyId, String publicKey, String identityKey, int registrationId)
{
this.deviceId = deviceId;
this.keyId = keyId;
this.publicKey = publicKey;
this.identityKey = identityKey;
this.registrationId = registrationId;
}
@VisibleForTesting
public PreKeyV1(long deviceId, long keyId, String publicKey, String identityKey)
{
this.id = id;
this.number = number;
this.deviceId = deviceId;
this.keyId = keyId;
this.publicKey = publicKey;
this.identityKey = identityKey;
this.lastResort = lastResort;
}
@XmlTransient
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
@XmlTransient
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
@Override
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
@Override
public long getKeyId() {
return keyId;
}
public void setKeyId(long keyId) {
this.keyId = keyId;
}
public String getIdentityKey() {
return identityKey;
}
public void setIdentityKey(String identityKey) {
this.identityKey = identityKey;
}
@XmlTransient
public boolean isLastResort() {
return lastResort;
}
public void setLastResort(boolean lastResort) {
this.lastResort = lastResort;
}
public void setDeviceId(long deviceId) {
this.deviceId = deviceId;
}

View File

@@ -0,0 +1,82 @@
package org.whispersystems.textsecuregcm.entities;
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class PreKeyV2 implements PreKeyBase {
@JsonProperty
@NotNull
private long keyId;
@JsonProperty
@NotEmpty
private String publicKey;
public PreKeyV2() {}
public PreKeyV2(long keyId, String publicKey)
{
this.keyId = keyId;
this.publicKey = publicKey;
}
@Override
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
@Override
public long getKeyId() {
return keyId;
}
public void setKeyId(long keyId) {
this.keyId = keyId;
}
@Override
public boolean equals(Object object) {
if (object == null || !(object instanceof PreKeyV2)) return false;
PreKeyV2 that = (PreKeyV2)object;
if (publicKey == null) {
return this.keyId == that.keyId && that.publicKey == null;
} else {
return this.keyId == that.keyId && this.publicKey.equals(that.publicKey);
}
}
@Override
public int hashCode() {
if (publicKey == null) {
return (int)this.keyId;
} else {
return ((int)this.keyId) ^ publicKey.hashCode();
}
}
}

View File

@@ -0,0 +1,15 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
public class ProvisioningMessage {
@JsonProperty
@NotEmpty
private String body;
public String getBody() {
return body;
}
}

View File

@@ -0,0 +1,16 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
public class SendMessageResponse {
@JsonProperty
private boolean needsSync;
public SendMessageResponse() {}
public SendMessageResponse(boolean needsSync) {
this.needsSync = needsSync;
}
}

View File

@@ -0,0 +1,46 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import java.io.Serializable;
public class SignedPreKey extends PreKeyV2 {
@JsonProperty
@NotEmpty
private String signature;
public SignedPreKey() {}
public SignedPreKey(long keyId, String publicKey, String signature) {
super(keyId, publicKey);
this.signature = signature;
}
public String getSignature() {
return signature;
}
@Override
public boolean equals(Object object) {
if (object == null || !(object instanceof SignedPreKey)) return false;
SignedPreKey that = (SignedPreKey) object;
if (signature == null) {
return super.equals(object) && that.signature == null;
} else {
return super.equals(object) && this.signature.equals(that.signature);
}
}
@Override
public int hashCode() {
if (signature == null) {
return super.hashCode();
} else {
return super.hashCode() ^ signature.hashCode();
}
}
}

View File

@@ -0,0 +1,47 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Min;
public class UnregisteredEvent {
@JsonProperty
@NotEmpty
private String registrationId;
@JsonProperty
private String canonicalId;
@JsonProperty
@NotEmpty
private String number;
@JsonProperty
@Min(1)
private int deviceId;
@JsonProperty
private long timestamp;
public String getRegistrationId() {
return registrationId;
}
public String getCanonicalId() {
return canonicalId;
}
public String getNumber() {
return number;
}
public int getDeviceId() {
return deviceId;
}
public long getTimestamp() {
return timestamp;
}
}

View File

@@ -0,0 +1,17 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.LinkedList;
import java.util.List;
public class UnregisteredEventList {
@JsonProperty
private List<UnregisteredEvent> devices;
public List<UnregisteredEvent> getDevices() {
if (devices == null) return new LinkedList<>();
else return devices;
}
}

View File

@@ -36,7 +36,8 @@ import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.net.ssl.SSLContext;
@@ -62,11 +63,13 @@ public class FederatedClient {
private final Logger logger = LoggerFactory.getLogger(FederatedClient.class);
private static final String USER_COUNT_PATH = "/v1/federation/user_count";
private static final String USER_TOKENS_PATH = "/v1/federation/user_tokens/%d";
private static final String RELAY_MESSAGE_PATH = "/v1/federation/messages/%s/%d/%s";
private static final String PREKEY_PATH_DEVICE = "/v1/federation/key/%s/%s";
private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d";
private static final String USER_COUNT_PATH = "/v1/federation/user_count";
private static final String USER_TOKENS_PATH = "/v1/federation/user_tokens/%d";
private static final String RELAY_MESSAGE_PATH = "/v1/federation/messages/%s/%d/%s";
private static final String PREKEY_PATH_DEVICE_V1 = "/v1/federation/key/%s/%s";
private static final String PREKEY_PATH_DEVICE_V2 = "/v2/federation/key/%s/%s";
private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d";
private static final String RECEIPT_PATH = "/v1/receipt/%s/%d/%s/%d";
private final FederatedPeer peer;
private final Client client;
@@ -107,9 +110,9 @@ public class FederatedClient {
}
}
public Optional<UnstructuredPreKeyList> getKeys(String destination, String device) {
public Optional<PreKeyResponseV1> getKeysV1(String destination, String device) {
try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE, destination, device));
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V1, destination, device));
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
@@ -119,7 +122,7 @@ public class FederatedClient {
throw new WebApplicationException(clientResponseToResponse(response));
}
return Optional.of(response.getEntity(UnstructuredPreKeyList.class));
return Optional.of(response.getEntity(PreKeyResponseV1.class));
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("PreKey", e);
@@ -127,6 +130,27 @@ public class FederatedClient {
}
}
public Optional<PreKeyResponseV2> getKeysV2(String destination, String device) {
try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V2, destination, device));
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
.get(ClientResponse.class);
if (response.getStatus() < 200 || response.getStatus() >= 300) {
throw new WebApplicationException(clientResponseToResponse(response));
}
return Optional.of(response.getEntity(PreKeyResponseV2.class));
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("PreKey", e);
return Optional.absent();
}
}
public int getUserCount() {
try {
WebResource resource = client.resource(peer.getUrl()).path(USER_COUNT_PATH);
@@ -174,6 +198,25 @@ public class FederatedClient {
}
}
public void sendDeliveryReceipt(String source, long sourceDeviceId, String destination, long messageId)
throws IOException
{
try {
String path = String.format(RECEIPT_PATH, source, sourceDeviceId, destination, messageId);
WebResource resource = client.resource(peer.getUrl()).path(path);
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
.put(ClientResponse.class);
if (response.getStatus() != 200 && response.getStatus() != 204) {
throw new WebApplicationException(clientResponseToResponse(response));
}
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("sendMessage", e);
throw new IOException(e);
}
}
private String getAuthorizationHeader(String federationName, FederatedPeer peer) {
return "Basic " + Base64.encodeBytes((federationName + ":" + peer.getAuthenticationToken()).getBytes());
}

View File

@@ -40,6 +40,6 @@ public class NonLimitedAccount extends Account {
@Override
public Optional<Device> getAuthenticatedDevice() {
return Optional.of(new Device(deviceId, null, null, null, null, null, false, 0));
return Optional.of(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, System.currentTimeMillis(), System.currentTimeMillis()));
}
}

View File

@@ -16,9 +16,13 @@
*/
package org.whispersystems.textsecuregcm.limits;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class LeakyBucket implements Serializable {
import java.io.IOException;
public class LeakyBucket {
private final int bucketSize;
private final double leakRatePerMillis;
@@ -27,10 +31,14 @@ public class LeakyBucket implements Serializable {
private long lastUpdateTimeMillis;
public LeakyBucket(int bucketSize, double leakRatePerMillis) {
this(bucketSize, leakRatePerMillis, bucketSize, System.currentTimeMillis());
}
private LeakyBucket(int bucketSize, double leakRatePerMillis, int spaceRemaining, long lastUpdateTimeMillis) {
this.bucketSize = bucketSize;
this.leakRatePerMillis = leakRatePerMillis;
this.spaceRemaining = bucketSize;
this.lastUpdateTimeMillis = System.currentTimeMillis();
this.spaceRemaining = spaceRemaining;
this.lastUpdateTimeMillis = lastUpdateTimeMillis;
}
public boolean add(int amount) {
@@ -50,4 +58,40 @@ public class LeakyBucket implements Serializable {
return Math.min(this.bucketSize,
(int)Math.floor(this.spaceRemaining + (elapsedTime * this.leakRatePerMillis)));
}
public String serialize(ObjectMapper mapper) throws JsonProcessingException {
return mapper.writeValueAsString(new LeakyBucketEntity(bucketSize, leakRatePerMillis, spaceRemaining, lastUpdateTimeMillis));
}
public static LeakyBucket fromSerialized(ObjectMapper mapper, String serialized) throws IOException {
LeakyBucketEntity entity = mapper.readValue(serialized, LeakyBucketEntity.class);
return new LeakyBucket(entity.bucketSize, entity.leakRatePerMillis,
entity.spaceRemaining, entity.lastUpdateTimeMillis);
}
private static class LeakyBucketEntity {
@JsonProperty
private int bucketSize;
@JsonProperty
private double leakRatePerMillis;
@JsonProperty
private int spaceRemaining;
@JsonProperty
private long lastUpdateTimeMillis;
public LeakyBucketEntity() {}
private LeakyBucketEntity(int bucketSize, double leakRatePerMillis,
int spaceRemaining, long lastUpdateTimeMillis)
{
this.bucketSize = bucketSize;
this.leakRatePerMillis = leakRatePerMillis;
this.spaceRemaining = spaceRemaining;
this.lastUpdateTimeMillis = lastUpdateTimeMillis;
}
}
}

View File

@@ -16,27 +16,41 @@
*/
package org.whispersystems.textsecuregcm.limits;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import net.spy.memcached.MemcachedClient;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import java.util.concurrent.TimeUnit;
import java.io.IOException;
import static com.codahale.metrics.MetricRegistry.name;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class RateLimiter {
private final Meter meter;
private final MemcachedClient memcachedClient;
private final String name;
private final int bucketSize;
private final double leakRatePerMillis;
private final Logger logger = LoggerFactory.getLogger(RateLimiter.class);
private final ObjectMapper mapper = SystemMapper.getMapper();
public RateLimiter(MemcachedClient memcachedClient, String name,
private final Meter meter;
private final JedisPool cacheClient;
private final String name;
private final int bucketSize;
private final double leakRatePerMillis;
public RateLimiter(JedisPool cacheClient, String name,
int bucketSize, double leakRatePerMinute)
{
this.meter = Metrics.newMeter(RateLimiter.class, name, "exceeded", TimeUnit.MINUTES);
this.memcachedClient = memcachedClient;
MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
this.meter = metricRegistry.meter(name(getClass(), name, "exceeded"));
this.cacheClient = cacheClient;
this.name = name;
this.bucketSize = bucketSize;
this.leakRatePerMillis = leakRatePerMinute / (60.0 * 1000.0);
@@ -58,21 +72,29 @@ public class RateLimiter {
}
private void setBucket(String key, LeakyBucket bucket) {
memcachedClient.set(getBucketName(key),
(int)Math.ceil((bucketSize / leakRatePerMillis) / 1000), bucket);
}
private LeakyBucket getBucket(String key) {
LeakyBucket bucket = (LeakyBucket)memcachedClient.get(getBucketName(key));
if (bucket == null) {
return new LeakyBucket(bucketSize, leakRatePerMillis);
} else {
return bucket;
try (Jedis jedis = cacheClient.getResource()) {
String serialized = bucket.serialize(mapper);
jedis.setex(getBucketName(key), (int) Math.ceil((bucketSize / leakRatePerMillis) / 1000), serialized);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
}
private LeakyBucket getBucket(String key) {
try (Jedis jedis = cacheClient.getResource()) {
String serialized = jedis.get(getBucketName(key));
if (serialized != null) {
return LeakyBucket.fromSerialized(mapper, serialized);
}
} catch (IOException e) {
logger.warn("Deserialization error", e);
}
return new LeakyBucket(bucketSize, leakRatePerMillis);
}
private String getBucketName(String key) {
return LeakyBucket.class.getSimpleName() + name + key;
return "leaky_bucket::" + name + "::" + key;
}
}

View File

@@ -17,9 +17,10 @@
package org.whispersystems.textsecuregcm.limits;
import net.spy.memcached.MemcachedClient;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
import redis.clients.jedis.JedisPool;
public class RateLimiters {
private final RateLimiter smsDestinationLimiter;
@@ -34,40 +35,40 @@ public class RateLimiters {
private final RateLimiter allocateDeviceLimiter;
private final RateLimiter verifyDeviceLimiter;
public RateLimiters(RateLimitsConfiguration config, MemcachedClient memcachedClient) {
this.smsDestinationLimiter = new RateLimiter(memcachedClient, "smsDestination",
public RateLimiters(RateLimitsConfiguration config, JedisPool cacheClient) {
this.smsDestinationLimiter = new RateLimiter(cacheClient, "smsDestination",
config.getSmsDestination().getBucketSize(),
config.getSmsDestination().getLeakRatePerMinute());
this.voiceDestinationLimiter = new RateLimiter(memcachedClient, "voxDestination",
this.voiceDestinationLimiter = new RateLimiter(cacheClient, "voxDestination",
config.getVoiceDestination().getBucketSize(),
config.getVoiceDestination().getLeakRatePerMinute());
this.verifyLimiter = new RateLimiter(memcachedClient, "verify",
this.verifyLimiter = new RateLimiter(cacheClient, "verify",
config.getVerifyNumber().getBucketSize(),
config.getVerifyNumber().getLeakRatePerMinute());
this.attachmentLimiter = new RateLimiter(memcachedClient, "attachmentCreate",
this.attachmentLimiter = new RateLimiter(cacheClient, "attachmentCreate",
config.getAttachments().getBucketSize(),
config.getAttachments().getLeakRatePerMinute());
this.contactsLimiter = new RateLimiter(memcachedClient, "contactsQuery",
this.contactsLimiter = new RateLimiter(cacheClient, "contactsQuery",
config.getContactQueries().getBucketSize(),
config.getContactQueries().getLeakRatePerMinute());
this.preKeysLimiter = new RateLimiter(memcachedClient, "prekeys",
this.preKeysLimiter = new RateLimiter(cacheClient, "prekeys",
config.getPreKeys().getBucketSize(),
config.getPreKeys().getLeakRatePerMinute());
this.messagesLimiter = new RateLimiter(memcachedClient, "messages",
this.messagesLimiter = new RateLimiter(cacheClient, "messages",
config.getMessages().getBucketSize(),
config.getMessages().getLeakRatePerMinute());
this.allocateDeviceLimiter = new RateLimiter(memcachedClient, "allocateDevice",
this.allocateDeviceLimiter = new RateLimiter(cacheClient, "allocateDevice",
config.getAllocateDevice().getBucketSize(),
config.getAllocateDevice().getLeakRatePerMinute());
this.verifyDeviceLimiter = new RateLimiter(memcachedClient, "verifyDevice",
this.verifyDeviceLimiter = new RateLimiter(cacheClient, "verifyDevice",
config.getVerifyDevice().getBucketSize(),
config.getVerifyDevice().getLeakRatePerMinute());

View File

@@ -0,0 +1,65 @@
package org.whispersystems.textsecuregcm.liquibase;
import com.codahale.metrics.MetricRegistry;
import net.sourceforge.argparse4j.inf.Namespace;
import java.sql.SQLException;
import io.dropwizard.Configuration;
import io.dropwizard.cli.ConfiguredCommand;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.db.DatabaseConfiguration;
import io.dropwizard.db.ManagedDataSource;
import io.dropwizard.setup.Bootstrap;
import liquibase.Liquibase;
import liquibase.exception.LiquibaseException;
import liquibase.exception.ValidationFailedException;
public abstract class AbstractLiquibaseCommand<T extends Configuration> extends ConfiguredCommand<T> {
private final DatabaseConfiguration<T> strategy;
private final Class<T> configurationClass;
private final String migrations;
protected AbstractLiquibaseCommand(String name,
String description,
String migrations,
DatabaseConfiguration<T> strategy,
Class<T> configurationClass) {
super(name, description);
this.migrations = migrations;
this.strategy = strategy;
this.configurationClass = configurationClass;
}
@Override
protected Class<T> getConfigurationClass() {
return configurationClass;
}
@Override
@SuppressWarnings("UseOfSystemOutOrSystemErr")
protected void run(Bootstrap<T> bootstrap, Namespace namespace, T configuration) throws Exception {
final DataSourceFactory dbConfig = strategy.getDataSourceFactory(configuration);
dbConfig.setMaxSize(1);
dbConfig.setMinSize(1);
dbConfig.setInitialSize(1);
try (final CloseableLiquibase liquibase = openLiquibase(dbConfig, namespace)) {
run(namespace, liquibase);
} catch (ValidationFailedException e) {
e.printDescriptiveError(System.err);
throw e;
}
}
private CloseableLiquibase openLiquibase(final DataSourceFactory dataSourceFactory, final Namespace namespace)
throws ClassNotFoundException, SQLException, LiquibaseException
{
final ManagedDataSource dataSource = dataSourceFactory.build(new MetricRegistry(), "liquibase");
return new CloseableLiquibase(dataSource, migrations);
}
protected abstract void run(Namespace namespace, Liquibase liquibase) throws Exception;
}

View File

@@ -0,0 +1,28 @@
package org.whispersystems.textsecuregcm.liquibase;
import java.sql.SQLException;
import io.dropwizard.db.ManagedDataSource;
import liquibase.Liquibase;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.resource.ClassLoaderResourceAccessor;
public class CloseableLiquibase extends Liquibase implements AutoCloseable {
private final ManagedDataSource dataSource;
public CloseableLiquibase(ManagedDataSource dataSource, String migrations)
throws LiquibaseException, ClassNotFoundException, SQLException
{
super(migrations,
new ClassLoaderResourceAccessor(),
new JdbcConnection(dataSource.getConnection()));
this.dataSource = dataSource;
}
@Override
public void close() throws Exception {
dataSource.stop();
}
}

View File

@@ -0,0 +1,72 @@
package org.whispersystems.textsecuregcm.liquibase;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import java.io.OutputStreamWriter;
import java.util.List;
import io.dropwizard.Configuration;
import io.dropwizard.db.DatabaseConfiguration;
import liquibase.Liquibase;
public class DbMigrateCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
public DbMigrateCommand(String migration, DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
super("migrate", "Apply all pending change sets.", migration, strategy, configurationClass);
}
@Override
public void configure(Subparser subparser) {
super.configure(subparser);
subparser.addArgument("-n", "--dry-run")
.action(Arguments.storeTrue())
.dest("dry-run")
.setDefault(Boolean.FALSE)
.help("output the DDL to stdout, don't run it");
subparser.addArgument("-c", "--count")
.type(Integer.class)
.dest("count")
.help("only apply the next N change sets");
subparser.addArgument("-i", "--include")
.action(Arguments.append())
.dest("contexts")
.help("include change sets from the given context");
}
@Override
@SuppressWarnings("UseOfSystemOutOrSystemErr")
public void run(Namespace namespace, Liquibase liquibase) throws Exception {
final String context = getContext(namespace);
final Integer count = namespace.getInt("count");
final Boolean dryRun = namespace.getBoolean("dry-run");
if (count != null) {
if (dryRun) {
liquibase.update(count, context, new OutputStreamWriter(System.out, Charsets.UTF_8));
} else {
liquibase.update(count, context);
}
} else {
if (dryRun) {
liquibase.update(context, new OutputStreamWriter(System.out, Charsets.UTF_8));
} else {
liquibase.update(context);
}
}
}
private String getContext(Namespace namespace) {
final List<Object> contexts = namespace.getList("contexts");
if (contexts == null) {
return "";
}
return Joiner.on(',').join(contexts);
}
}

View File

@@ -0,0 +1,51 @@
package org.whispersystems.textsecuregcm.liquibase;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import java.io.OutputStreamWriter;
import java.util.List;
import io.dropwizard.Configuration;
import io.dropwizard.db.DatabaseConfiguration;
import liquibase.Liquibase;
public class DbStatusCommand <T extends Configuration> extends AbstractLiquibaseCommand<T> {
public DbStatusCommand(String migrations, DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
super("status", "Check for pending change sets.", migrations, strategy, configurationClass);
}
@Override
public void configure(Subparser subparser) {
super.configure(subparser);
subparser.addArgument("-v", "--verbose")
.action(Arguments.storeTrue())
.dest("verbose")
.help("Output verbose information");
subparser.addArgument("-i", "--include")
.action(Arguments.append())
.dest("contexts")
.help("include change sets from the given context");
}
@Override
@SuppressWarnings("UseOfSystemOutOrSystemErr")
public void run(Namespace namespace, Liquibase liquibase) throws Exception {
liquibase.reportStatus(namespace.getBoolean("verbose"),
getContext(namespace),
new OutputStreamWriter(System.out, Charsets.UTF_8));
}
private String getContext(Namespace namespace) {
final List<Object> contexts = namespace.getList("contexts");
if (contexts == null) {
return "";
}
return Joiner.on(',').join(contexts);
}
}

View File

@@ -0,0 +1,44 @@
package org.whispersystems.textsecuregcm.liquibase;
import com.google.common.collect.Maps;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import java.util.SortedMap;
import io.dropwizard.Configuration;
import io.dropwizard.db.DatabaseConfiguration;
import liquibase.Liquibase;
public class NameableDbCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
private static final String COMMAND_NAME_ATTR = "subcommand";
private final SortedMap<String, AbstractLiquibaseCommand<T>> subcommands;
public NameableDbCommand(String name, String migrations, DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
super(name, "Run database migrations tasks", migrations, strategy, configurationClass);
this.subcommands = Maps.newTreeMap();
addSubcommand(new DbMigrateCommand<>(migrations, strategy, configurationClass));
addSubcommand(new DbStatusCommand<>(migrations, strategy, configurationClass));
}
private void addSubcommand(AbstractLiquibaseCommand<T> subcommand) {
subcommands.put(subcommand.getName(), subcommand);
}
@Override
public void configure(Subparser subparser) {
for (AbstractLiquibaseCommand<T> subcommand : subcommands.values()) {
final Subparser cmdParser = subparser.addSubparsers()
.addParser(subcommand.getName())
.setDefault(COMMAND_NAME_ATTR, subcommand.getName())
.description(subcommand.getDescription());
subcommand.configure(cmdParser);
}
}
@Override
public void run(Namespace namespace, Liquibase liquibase) throws Exception {
final AbstractLiquibaseCommand<T> subcommand = subcommands.get(namespace.getString(COMMAND_NAME_ATTR));
subcommand.run(namespace, liquibase);
}
}

View File

@@ -0,0 +1,27 @@
package org.whispersystems.textsecuregcm.liquibase;
import io.dropwizard.Bundle;
import io.dropwizard.Configuration;
import io.dropwizard.db.DatabaseConfiguration;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import io.dropwizard.util.Generics;
public abstract class NameableMigrationsBundle<T extends Configuration> implements Bundle, DatabaseConfiguration<T> {
private final String name;
private final String migrations;
public NameableMigrationsBundle(String name, String migrations) {
this.name = name;
this.migrations = migrations;
}
public final void initialize(Bootstrap<?> bootstrap) {
Class klass = Generics.getTypeParameter(this.getClass(), Configuration.class);
bootstrap.addCommand(new NameableDbCommand(name, migrations, this, klass));
}
public final void run(Environment environment) {
}
}

View File

@@ -0,0 +1,33 @@
package org.whispersystems.textsecuregcm.mappers;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.controllers.DeviceLimitExceededException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class DeviceLimitExceededExceptionMapper implements ExceptionMapper<DeviceLimitExceededException> {
@Override
public Response toResponse(DeviceLimitExceededException exception) {
return Response.status(411)
.entity(new DeviceLimitExceededDetails(exception.getCurrentDevices(),
exception.getMaxDevices()))
.build();
}
private static class DeviceLimitExceededDetails {
@JsonProperty
private int current;
@JsonProperty
private int max;
public DeviceLimitExceededDetails(int current, int max) {
this.current = current;
this.max = max;
}
}
}

View File

@@ -31,7 +31,9 @@ public class IOExceptionMapper implements ExceptionMapper<IOException> {
@Override
public Response toResponse(IOException e) {
logger.warn("IOExceptionMapper", e);
if (!(e.getCause() instanceof java.util.concurrent.TimeoutException)) {
logger.warn("IOExceptionMapper", e);
}
return Response.status(503).build();
}
}

View File

@@ -0,0 +1,15 @@
package org.whispersystems.textsecuregcm.mappers;
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class InvalidWebsocketAddressExceptionMapper implements ExceptionMapper<InvalidWebsocketAddressException> {
@Override
public Response toResponse(InvalidWebsocketAddressException exception) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
}

View File

@@ -0,0 +1,16 @@
package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.Gauge;
import com.sun.management.OperatingSystemMXBean;
import java.lang.management.ManagementFactory;
public class CpuUsageGauge implements Gauge<Integer> {
@Override
public Integer getValue() {
OperatingSystemMXBean mbean = (com.sun.management.OperatingSystemMXBean)
ManagementFactory.getOperatingSystemMXBean();
return (int) Math.ceil(mbean.getSystemCpuLoad() * 100);
}
}

View File

@@ -0,0 +1,17 @@
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> {
@Override
public Long getValue() {
OperatingSystemMXBean mbean = (com.sun.management.OperatingSystemMXBean)
ManagementFactory.getOperatingSystemMXBean();
return mbean.getFreePhysicalMemorySize();
}
}

View File

@@ -1,23 +1,20 @@
package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Metered;
import com.codahale.metrics.MetricFilter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.ScheduledReporter;
import com.codahale.metrics.Snapshot;
import com.codahale.metrics.Timer;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.yammer.metrics.core.Clock;
import com.yammer.metrics.core.Counter;
import com.yammer.metrics.core.Gauge;
import com.yammer.metrics.core.Histogram;
import com.yammer.metrics.core.Metered;
import com.yammer.metrics.core.Metric;
import com.yammer.metrics.core.MetricName;
import com.yammer.metrics.core.MetricProcessor;
import com.yammer.metrics.core.MetricsRegistry;
import com.yammer.metrics.core.Sampling;
import com.yammer.metrics.core.Summarizable;
import com.yammer.metrics.core.Timer;
import com.yammer.metrics.core.VirtualMachineMetrics;
import com.yammer.metrics.reporting.AbstractPollingReporter;
import com.yammer.metrics.stats.Snapshot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
@@ -30,295 +27,214 @@ import java.util.SortedMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
* Adapted from MetricsServlet.
*/
public class JsonMetricsReporter extends AbstractPollingReporter implements MetricProcessor<JsonMetricsReporter.Context> {
private final Clock clock = Clock.defaultClock();
private final VirtualMachineMetrics vm = VirtualMachineMetrics.getInstance();
private final String service;
private final MetricsRegistry registry;
private final JsonFactory factory = new JsonFactory();
private final String table;
private final String sunnylabsHost;
private final String host;
private final boolean includeVMMetrics;
public JsonMetricsReporter(String service, MetricsRegistry registry, String token, String sunnylabsHost) throws UnknownHostException {
this(service, registry, token, sunnylabsHost, true);
}
public JsonMetricsReporter(String service, MetricsRegistry registry, String token, String sunnylabsHost, boolean includeVMMetrics) throws UnknownHostException {
super(registry, "jsonmetrics-reporter");
this.service = service;
this.registry = registry;
this.table = token;
this.sunnylabsHost = sunnylabsHost;
this.host = InetAddress.getLocalHost().getHostName();
this.includeVMMetrics = includeVMMetrics;
}
@Override
public void run() {
try {
URL http = new URL("https", sunnylabsHost, 443, "/report/metrics?t=" + table + "&h=" + host);
System.out.println("Reporting started to: " + http);
HttpURLConnection urlc = (HttpURLConnection) http.openConnection();
urlc.setDoOutput(true);
urlc.addRequestProperty("Content-Type", "application/json");
OutputStream outputStream = urlc.getOutputStream();
writeJson(outputStream);
outputStream.close();
System.out.println("Reporting complete: " + urlc.getResponseCode());
} catch (IOException e) {
e.printStackTrace();
}
}
static final class Context {
final boolean showFullSamples;
final JsonGenerator json;
Context(JsonGenerator json, boolean showFullSamples) {
this.json = json;
this.showFullSamples = showFullSamples;
}
}
public void writeJson(OutputStream out) throws IOException {
final JsonGenerator json = factory.createGenerator(out, JsonEncoding.UTF8);
json.writeStartObject();
if (includeVMMetrics) {
writeVmMetrics(json);
}
writeRegularMetrics(json, false);
json.writeEndObject();
json.close();
}
private void writeVmMetrics(JsonGenerator json) throws IOException {
json.writeFieldName(service);
json.writeStartObject();
json.writeFieldName("jvm");
json.writeStartObject();
{
json.writeFieldName("vm");
json.writeStartObject();
{
json.writeStringField("name", vm.name());
json.writeStringField("version", vm.version());
}
json.writeEndObject();
json.writeFieldName("memory");
json.writeStartObject();
{
json.writeNumberField("totalInit", vm.totalInit());
json.writeNumberField("totalUsed", vm.totalUsed());
json.writeNumberField("totalMax", vm.totalMax());
json.writeNumberField("totalCommitted", vm.totalCommitted());
json.writeNumberField("heapInit", vm.heapInit());
json.writeNumberField("heapUsed", vm.heapUsed());
json.writeNumberField("heapMax", vm.heapMax());
json.writeNumberField("heapCommitted", vm.heapCommitted());
json.writeNumberField("heap_usage", vm.heapUsage());
json.writeNumberField("non_heap_usage", vm.nonHeapUsage());
json.writeFieldName("memory_pool_usages");
json.writeStartObject();
{
for (Map.Entry<String, Double> pool : vm.memoryPoolUsage().entrySet()) {
json.writeNumberField(pool.getKey(), pool.getValue());
}
}
json.writeEndObject();
}
json.writeEndObject();
final Map<String, VirtualMachineMetrics.BufferPoolStats> bufferPoolStats = vm.getBufferPoolStats();
if (!bufferPoolStats.isEmpty()) {
json.writeFieldName("buffers");
json.writeStartObject();
{
json.writeFieldName("direct");
json.writeStartObject();
{
json.writeNumberField("count", bufferPoolStats.get("direct").getCount());
json.writeNumberField("memoryUsed", bufferPoolStats.get("direct").getMemoryUsed());
json.writeNumberField("totalCapacity", bufferPoolStats.get("direct").getTotalCapacity());
}
json.writeEndObject();
json.writeFieldName("mapped");
json.writeStartObject();
{
json.writeNumberField("count", bufferPoolStats.get("mapped").getCount());
json.writeNumberField("memoryUsed", bufferPoolStats.get("mapped").getMemoryUsed());
json.writeNumberField("totalCapacity", bufferPoolStats.get("mapped").getTotalCapacity());
}
json.writeEndObject();
}
json.writeEndObject();
}
json.writeNumberField("daemon_thread_count", vm.daemonThreadCount());
json.writeNumberField("thread_count", vm.threadCount());
json.writeNumberField("current_time", clock.time());
json.writeNumberField("uptime", vm.uptime());
json.writeNumberField("fd_usage", vm.fileDescriptorUsage());
json.writeFieldName("thread-states");
json.writeStartObject();
{
for (Map.Entry<Thread.State, Double> entry : vm.threadStatePercentages()
.entrySet()) {
json.writeNumberField(entry.getKey().toString().toLowerCase(),
entry.getValue());
}
}
json.writeEndObject();
json.writeFieldName("garbage-collectors");
json.writeStartObject();
{
for (Map.Entry<String, VirtualMachineMetrics.GarbageCollectorStats> entry : vm.garbageCollectors()
.entrySet()) {
json.writeFieldName(entry.getKey());
json.writeStartObject();
{
final VirtualMachineMetrics.GarbageCollectorStats gc = entry.getValue();
json.writeNumberField("runs", gc.getRuns());
json.writeNumberField("time", gc.getTime(TimeUnit.MILLISECONDS));
}
json.writeEndObject();
}
}
json.writeEndObject();
}
json.writeEndObject();
json.writeEndObject();
}
public void writeRegularMetrics(JsonGenerator json, boolean showFullSamples) throws IOException {
for (Map.Entry<String, SortedMap<MetricName, Metric>> entry : registry.groupedMetrics().entrySet()) {
for (Map.Entry<MetricName, Metric> subEntry : entry.getValue().entrySet()) {
json.writeFieldName(sanitize(subEntry.getKey()));
try {
subEntry.getValue()
.processWith(this,
subEntry.getKey(),
new Context(json, showFullSamples));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
@Override
public void processHistogram(MetricName name, Histogram histogram, Context context) throws Exception {
final JsonGenerator json = context.json;
json.writeStartObject();
{
json.writeNumberField("count", histogram.count());
writeSummarizable(histogram, json);
writeSampling(histogram, json);
if (context.showFullSamples) {
json.writeObjectField("values", histogram.getSnapshot().getValues());
}
histogram.clear();
}
json.writeEndObject();
}
@Override
public void processCounter(MetricName name, Counter counter, Context context) throws Exception {
final JsonGenerator json = context.json;
json.writeNumber(counter.count());
}
@Override
public void processGauge(MetricName name, Gauge<?> gauge, Context context) throws Exception {
final JsonGenerator json = context.json;
json.writeObject(evaluateGauge(gauge));
}
@Override
public void processMeter(MetricName name, Metered meter, Context context) throws Exception {
final JsonGenerator json = context.json;
json.writeStartObject();
{
writeMeteredFields(meter, json);
}
json.writeEndObject();
}
@Override
public void processTimer(MetricName name, Timer timer, Context context) throws Exception {
final JsonGenerator json = context.json;
json.writeStartObject();
{
json.writeFieldName("duration");
json.writeStartObject();
{
json.writeStringField("unit", timer.durationUnit().toString().toLowerCase());
writeSummarizable(timer, json);
writeSampling(timer, json);
if (context.showFullSamples) {
json.writeObjectField("values", timer.getSnapshot().getValues());
}
}
json.writeEndObject();
json.writeFieldName("rate");
json.writeStartObject();
{
writeMeteredFields(timer, json);
}
json.writeEndObject();
}
json.writeEndObject();
}
private static Object evaluateGauge(Gauge<?> gauge) {
try {
return gauge.value();
} catch (RuntimeException e) {
return "error reading gauge: " + e.getMessage();
}
}
private static void writeSummarizable(Summarizable metric, JsonGenerator json) throws IOException {
json.writeNumberField("min", metric.min());
json.writeNumberField("max", metric.max());
json.writeNumberField("mean", metric.mean());
}
private static void writeSampling(Sampling metric, JsonGenerator json) throws IOException {
final Snapshot snapshot = metric.getSnapshot();
json.writeNumberField("median", snapshot.getMedian());
json.writeNumberField("p75", snapshot.get75thPercentile());
json.writeNumberField("p95", snapshot.get95thPercentile());
json.writeNumberField("p99", snapshot.get99thPercentile());
json.writeNumberField("p999", snapshot.get999thPercentile());
}
private static void writeMeteredFields(Metered metered, JsonGenerator json) throws IOException {
json.writeNumberField("count", metered.count());
json.writeNumberField("mean", metered.meanRate());
json.writeNumberField("m1", metered.oneMinuteRate());
json.writeNumberField("m5", metered.fiveMinuteRate());
json.writeNumberField("m15", metered.fifteenMinuteRate());
}
public class JsonMetricsReporter extends ScheduledReporter {
private static final Pattern SIMPLE_NAMES = Pattern.compile("[^a-zA-Z0-9_.\\-~]");
private String sanitize(MetricName metricName) {
return SIMPLE_NAMES.matcher(metricName.getGroup() + "." + metricName.getName()).replaceAll("_");
private final Logger logger = LoggerFactory.getLogger(JsonMetricsReporter.class);
private final JsonFactory factory = new JsonFactory();
private final String token;
private final String hostname;
private final String host;
public JsonMetricsReporter(MetricRegistry registry, String token, String hostname,
MetricFilter filter, TimeUnit rateUnit, TimeUnit durationUnit)
throws UnknownHostException
{
super(registry, "json-reporter", filter, rateUnit, durationUnit);
this.token = token;
this.hostname = hostname;
this.host = InetAddress.getLocalHost().getHostName();
}
@Override
public void report(SortedMap<String, Gauge> stringGaugeSortedMap,
SortedMap<String, Counter> stringCounterSortedMap,
SortedMap<String, Histogram> stringHistogramSortedMap,
SortedMap<String, Meter> stringMeterSortedMap,
SortedMap<String, Timer> stringTimerSortedMap)
{
try {
logger.debug("Reporting metrics...");
URL url = new URL("https", hostname, 443, String.format("/report/metrics?t=%s&h=%s", token, host));
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.addRequestProperty("Content-Type", "application/json");
OutputStream outputStream = connection.getOutputStream();
JsonGenerator json = factory.createGenerator(outputStream, JsonEncoding.UTF8);
json.writeStartObject();
for (Map.Entry<String, Gauge> gauge : stringGaugeSortedMap.entrySet()) {
reportGauge(json, gauge.getKey(), gauge.getValue());
}
for (Map.Entry<String, Counter> counter : stringCounterSortedMap.entrySet()) {
reportCounter(json, counter.getKey(), counter.getValue());
}
for (Map.Entry<String, Histogram> histogram : stringHistogramSortedMap.entrySet()) {
reportHistogram(json, histogram.getKey(), histogram.getValue());
}
for (Map.Entry<String, Meter> meter : stringMeterSortedMap.entrySet()) {
reportMeter(json, meter.getKey(), meter.getValue());
}
for (Map.Entry<String, Timer> timer : stringTimerSortedMap.entrySet()) {
reportTimer(json, timer.getKey(), timer.getValue());
}
json.writeEndObject();
json.close();
outputStream.close();
logger.debug("Metrics server response: " + connection.getResponseCode());
} catch (IOException e) {
logger.warn("Error sending metrics", e);
} catch (Exception e) {
logger.warn("error", e);
}
}
private void reportGauge(JsonGenerator json, String name, Gauge gauge) throws IOException {
Object gaugeValue = evaluateGauge(gauge);
if (gaugeValue instanceof Number) {
json.writeFieldName(sanitize(name));
json.writeObject(gaugeValue);
}
}
private void reportCounter(JsonGenerator json, String name, Counter counter) throws IOException {
json.writeFieldName(sanitize(name));
json.writeNumber(counter.getCount());
}
private void reportHistogram(JsonGenerator json, String name, Histogram histogram) throws IOException {
Snapshot snapshot = histogram.getSnapshot();
json.writeFieldName(sanitize(name));
json.writeStartObject();
json.writeNumberField("count", histogram.getCount());
writeSnapshot(json, snapshot);
json.writeEndObject();
}
private void reportMeter(JsonGenerator json, String name, Meter meter) throws IOException {
json.writeFieldName(sanitize(name));
json.writeStartObject();
writeMetered(json, meter);
json.writeEndObject();
}
private void reportTimer(JsonGenerator json, String name, Timer timer) throws IOException {
json.writeFieldName(sanitize(name));
json.writeStartObject();
json.writeFieldName("rate");
json.writeStartObject();
writeMetered(json, timer);
json.writeEndObject();
json.writeFieldName("duration");
json.writeStartObject();
writeSnapshot(json, timer.getSnapshot());
json.writeEndObject();
json.writeEndObject();
}
private Object evaluateGauge(Gauge gauge) {
try {
return gauge.getValue();
} catch (RuntimeException e) {
logger.warn("Error reading gauge", e);
return "error reading gauge";
}
}
private void writeSnapshot(JsonGenerator json, Snapshot snapshot) throws IOException {
json.writeNumberField("max", convertDuration(snapshot.getMax()));
json.writeNumberField("mean", convertDuration(snapshot.getMean()));
json.writeNumberField("min", convertDuration(snapshot.getMin()));
json.writeNumberField("stddev", convertDuration(snapshot.getStdDev()));
json.writeNumberField("median", convertDuration(snapshot.getMedian()));
json.writeNumberField("p75", convertDuration(snapshot.get75thPercentile()));
json.writeNumberField("p95", convertDuration(snapshot.get95thPercentile()));
json.writeNumberField("p98", convertDuration(snapshot.get98thPercentile()));
json.writeNumberField("p99", convertDuration(snapshot.get99thPercentile()));
json.writeNumberField("p999", convertDuration(snapshot.get999thPercentile()));
}
private void writeMetered(JsonGenerator json, Metered meter) throws IOException {
json.writeNumberField("count", convertRate(meter.getCount()));
json.writeNumberField("mean", convertRate(meter.getMeanRate()));
json.writeNumberField("m1", convertRate(meter.getOneMinuteRate()));
json.writeNumberField("m5", convertRate(meter.getFiveMinuteRate()));
json.writeNumberField("m15", convertRate(meter.getFifteenMinuteRate()));
}
private String sanitize(String metricName) {
return SIMPLE_NAMES.matcher(metricName).replaceAll("_");
}
public static Builder forRegistry(MetricRegistry registry) {
return new Builder(registry);
}
public static class Builder {
private final MetricRegistry registry;
private MetricFilter filter = MetricFilter.ALL;
private TimeUnit rateUnit = TimeUnit.SECONDS;
private TimeUnit durationUnit = TimeUnit.MILLISECONDS;
private String token;
private String hostname;
private Builder(MetricRegistry registry) {
this.registry = registry;
this.rateUnit = TimeUnit.SECONDS;
this.durationUnit = TimeUnit.MILLISECONDS;
this.filter = MetricFilter.ALL;
}
public Builder convertRatesTo(TimeUnit rateUnit) {
this.rateUnit = rateUnit;
return this;
}
public Builder convertDurationsTo(TimeUnit durationUnit) {
this.durationUnit = durationUnit;
return this;
}
public Builder filter(MetricFilter filter) {
this.filter = filter;
return this;
}
public Builder withToken(String token) {
this.token = token;
return this;
}
public Builder withHostname(String hostname) {
this.hostname = hostname;
return this;
}
public JsonMetricsReporter build() throws UnknownHostException {
if (hostname == null) {
throw new IllegalArgumentException("No hostname specified!");
}
if (token == null) {
throw new IllegalArgumentException("No token specified!");
}
return new JsonMetricsReporter(registry, token, hostname, filter, rateUnit, durationUnit);
}
}
}

View File

@@ -0,0 +1,38 @@
package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.ScheduledReporter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import javax.validation.constraints.NotNull;
import java.net.UnknownHostException;
import io.dropwizard.metrics.BaseReporterFactory;
@JsonTypeName("json")
public class JsonMetricsReporterFactory extends BaseReporterFactory {
@JsonProperty
@NotNull
private String hostname;
@JsonProperty
@NotNull
private String token;
@Override
public ScheduledReporter build(MetricRegistry metricRegistry) {
try {
return JsonMetricsReporter.forRegistry(metricRegistry)
.withHostname(hostname)
.withToken(token)
.convertRatesTo(getRateUnit())
.convertDurationsTo(getDurationUnit())
.filter(getFilter())
.build();
} catch (UnknownHostException e) {
throw new IllegalArgumentException(e);
}
}
}

View File

@@ -0,0 +1,36 @@
package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.Gauge;
import org.whispersystems.textsecuregcm.util.Pair;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public abstract class NetworkGauge implements Gauge<Long> {
protected Pair<Long, Long> getSentReceived() throws IOException {
File proc = new File("/proc/net/dev");
BufferedReader reader = new BufferedReader(new FileReader(proc));
String header = reader.readLine();
String header2 = reader.readLine();
long bytesSent = 0;
long bytesReceived = 0;
String interfaceStats;
while ((interfaceStats = reader.readLine()) != null) {
String[] stats = interfaceStats.split("\\s+");
if (!stats[1].equals("lo:")) {
bytesReceived += Long.parseLong(stats[2]);
bytesSent += Long.parseLong(stats[10]);
}
}
return new Pair<>(bytesSent, bytesReceived);
}
}

View File

@@ -0,0 +1,36 @@
package org.whispersystems.textsecuregcm.metrics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Pair;
import java.io.IOException;
public class NetworkReceivedGauge extends NetworkGauge {
private final Logger logger = LoggerFactory.getLogger(NetworkSentGauge.class);
private long lastTimestamp;
private long lastReceived;
@Override
public Long getValue() {
try {
long timestamp = System.currentTimeMillis();
Pair<Long, Long> sentAndReceived = getSentReceived();
long result = 0;
if (lastTimestamp != 0) {
result = sentAndReceived.second() - lastReceived;
lastReceived = sentAndReceived.second();
}
lastTimestamp = timestamp;
return result;
} catch (IOException e) {
logger.warn("NetworkReceivedGauge", e);
return -1L;
}
}
}

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