Compare commits

...

139 Commits
v0.29 ... v0.93

Author SHA1 Message Date
Moxie Marlinspike
c410348278 Bump version to 0.93
// FREEBIE
2016-03-06 13:59:36 -08:00
Moxie Marlinspike
d8a758211f Make push sender queue depth configurable or disable-able.
// FREEBIE
2016-03-06 13:58:30 -08:00
Moxie Marlinspike
33e60f2527 Bump version to 0.92
// FREEBIE
2016-02-02 15:31:10 -08:00
Moxie Marlinspike
fb705eee23 Switch PushSender queue depth metrics to gauge
// FREEBIE
2016-02-02 15:21:15 -08:00
Moxie Marlinspike
635e16e934 Bump version to 0.91
// FREEBIE
2016-02-02 14:43:34 -08:00
Moxie Marlinspike
1deb3ae67f Asynchronous processing queue for incoming messages
// FREEBIE
2016-02-02 14:42:47 -08:00
Moxie Marlinspike
16ff40f420 Bump version to 0.90
// FREEBIE
2016-01-20 18:41:22 -08:00
Moxie Marlinspike
a8b5cb23fe Reduce pending max queue size to 1000 messages
// FREEBIE
2016-01-20 18:41:01 -08:00
Moxie Marlinspike
82f88d04ad Fuzz GCM write timestmap by 10 seconds
// FREEBIE
2015-12-21 16:59:54 -08:00
Moxie Marlinspike
0e1091e0ea Bump version to 0.89
// FREEBIE
2015-12-21 12:36:07 -08:00
Moxie Marlinspike
7b48f10cc9 Filter message deletes by device
// FREEBIE
2015-12-21 12:35:45 -08:00
Moxie Marlinspike
0be34b1135 Bump version to 0.88
// FREEBIE
2015-12-04 12:59:22 -08:00
Moxie Marlinspike
fb5e0242d0 Adjust requeue message logic to avoid redis assumptions
// FREEBIE
2015-12-04 11:41:23 -08:00
Moxie Marlinspike
d376035557 Bump version to 0.87 2015-12-03 16:40:23 -08:00
Moxie Marlinspike
747b2dc7c5 Jedis sanity checks
// FREEBIE
2015-12-03 16:40:04 -08:00
Moxie Marlinspike
fac2f1bee3 Bump version to 0.86
// FREEBIE
2015-12-02 15:06:54 -08:00
Moxie Marlinspike
a211f6aed9 Delete pending messages for an unlinked device
// FREEBIE
2015-12-02 15:06:09 -08:00
Moxie Marlinspike
0bc494245d Fix for broken string format
// FREEBIE
2015-12-01 11:54:50 -08:00
Frederic Jacobs
b31a88043e Adding Signal SMS verification strings.
- Changes the voice verification string.
- Keeps the TextSecure SMS String for matching in Signal for Android.
- Changes TextSecure to Signal for iOS, adding tap to verify link.
- Added test for iOS query parameter.
2015-12-01 11:54:14 -08:00
Moxie Marlinspike
85509c6d8b Don't need to send payload GCM messages any longer.
// FREEBIE
2015-12-01 10:58:43 -08:00
Moxie Marlinspike
2dd131cf79 Bump verison to 0.85
// FREEBIE
2015-11-12 10:42:28 -08:00
Moxie Marlinspike
51990d0b33 Lower chunk size
// FREEBIE
2015-11-12 10:42:16 -08:00
Moxie Marlinspike
00a49afc30 Bump version to 0.84
// FREEBIE
2015-11-09 17:20:28 -08:00
Moxie Marlinspike
faa0630851 Fix up MX numbers for SMS delivery
// FREEBIE
2015-11-09 17:18:59 -08:00
Moxie Marlinspike
aac3fc68fc Bump version to 0.83
// FREEBIE
2015-11-06 11:24:53 -08:00
Moxie Marlinspike
9c08b96b50 Bump version to 0.82
// FREEBIE
2015-11-04 11:20:39 -08:00
Moxie Marlinspike
15ddde1df4 Adjust log levels on delivery receipt failure.
// FREEBIE
2015-11-04 11:20:09 -08:00
Moxie Marlinspike
f2a9de3ba8 Retry serializable transaction.
// FREEBIE
2015-11-04 11:19:54 -08:00
Moxie Marlinspike
fd725206e2 Bump version to 0.81
// FREEBIE
2015-11-03 08:12:31 -08:00
Moxie Marlinspike
6368b9383a Stripe SMS/Vox across multiple numbers
// FREEBIE
2015-11-03 08:12:18 -08:00
Moxie Marlinspike
2b8a11b001 Bump version to 0.80
// FREEBIE
2015-09-30 17:53:47 -07:00
Moxie Marlinspike
c9e0339a30 Specify media type on attributes put
// FREEBIE
2015-09-30 17:53:09 -07:00
Moxie Marlinspike
8d11595290 Bump version to 0.79 2015-09-21 14:09:28 -07:00
Moxie Marlinspike
2fe9f3effa Generate as well as consume auth tokens. Also user agents.
// FREEBIE
2015-09-21 14:09:03 -07:00
Moxie Marlinspike
ae122ff8a2 Bump version to 0.76
// FREEBIE
2015-08-18 11:11:58 -07:00
Moxie Marlinspike
8b941ddd33 Make the messagedb a bounded queue at 5000 msgs/device
// FREEBIE
2015-08-18 11:10:42 -07:00
Moxie Marlinspike
2902ea6689 Get rid of deprecated API
// FREEBIE
2015-08-18 11:10:34 -07:00
Moxie Marlinspike
5ccbf355bd Chunk sending pending message queues > a chunk size.
// FREEBIE
2015-08-17 17:12:36 -07:00
Moxie Marlinspike
62d8f635b0 Track voice support on TS server.
// FREEBIE
2015-08-13 11:43:49 -07:00
Moxie Marlinspike
4c3aae63d3 Trim old messages
// FREEBIE
2015-08-11 20:15:05 -07:00
Moxie Marlinspike
8f94aa0c0d Actually vacuum messages
// FREEBIE
2015-08-11 20:00:11 -07:00
Moxie Marlinspike
0370306bb6 Map 411 to 413
// FREEBIE
2015-08-01 10:09:15 -07:00
Moxie Marlinspike
c9176efe6f Bump version to 0.71
// FREEBIE
2015-07-30 17:31:19 -07:00
Moxie Marlinspike
a3fd08b7ef Add gauge for reporting number of open fds
// FREEBIE
2015-07-30 16:55:19 -07:00
Moxie Marlinspike
83a9e36ef1 Update logging levels.
// FREEBIE
2015-07-30 16:39:55 -07:00
Moxie Marlinspike
9668decc84 Bump version to 0.70
// FREEBIE
2015-07-30 14:17:24 -07:00
Moxie Marlinspike
328bb47d44 Only handle dead letters to addresses, not connection info.
// FREEBIE
2015-07-30 14:16:39 -07:00
Moxie Marlinspike
c74e0b9eab Bump version to 0.69
// FREEBIE
2015-07-30 13:25:40 -07:00
Moxie Marlinspike
20dc32413f Soften some logging.
// FREEBIE
2015-07-30 13:25:29 -07:00
Moxie Marlinspike
d4e618893c Make APN fallback behave well in multi-server environments.
// FREEBIE
2015-07-30 13:18:22 -07:00
Moxie Marlinspike
8d0d934249 Bump version to 0.68
// FREEBIE
2015-07-29 15:19:09 -07:00
Moxie Marlinspike
ef2441ad82 Don't pass response objects back from federated client.
// FREEBIE
2015-07-29 15:18:40 -07:00
Moxie Marlinspike
bb7859138c Bump version to 0.67
// FREEBIE
2015-07-29 14:23:05 -07:00
Moxie Marlinspike
ebc4570941 Fix federated client connection leak.
// FREEBIE
2015-07-29 12:36:03 -07:00
Moxie Marlinspike
d04baed38b Bump version to 0.65
// FREEBIE
2015-07-28 15:23:49 -07:00
Moxie Marlinspike
001c81f797 Try to make JerseyClient put() include a content-length of 0.
// FREEBIE
2015-07-28 15:23:21 -07:00
Moxie Marlinspike
3327bf4788 Add provisioning keepalive endpoint.
// FREEBIE
2015-07-28 15:22:51 -07:00
Moxie Marlinspike
e0b480e232 Bump version to 0.64
// FREEBIE
2015-07-27 22:47:16 -07:00
Moxie Marlinspike
b328d85230 Increase timeout on push service socket.
// FREEBIE
2015-07-27 22:46:38 -07:00
Moxie Marlinspike
3afaa5c1e6 Fix bug with federated delivery receipts.
// FREEBIE
2015-07-27 22:46:18 -07:00
Moxie Marlinspike
f2c8699823 Remove unused provider.
// FREEBIE
2015-07-27 17:58:38 -07:00
Moxie Marlinspike
4c11315a3c Bump version to 0.63
// FREEBIE
2015-07-27 17:04:07 -07:00
Moxie Marlinspike
0e3a347d6b Disable FAIL_ON_UNKNOWN_PROPERTIES for directory command.
// FREEBIE
2015-07-27 17:03:39 -07:00
Moxie Marlinspike
dc723fadaa Bump version to 0.62
// FREEBIE
2015-07-27 16:41:23 -07:00
Moxie Marlinspike
6396958a31 Bump up FederatedClient timeouts.
// FREEBIE
2015-07-27 16:40:42 -07:00
Moxie Marlinspike
1fe57e4841 Bump version to 0.61
// FREEBIE
2015-07-27 14:03:42 -07:00
Moxie Marlinspike
3885ae6337 Dropwizard 9 compatibility!
// FREEBIE
2015-07-27 14:02:44 -07:00
Moxie Marlinspike
39e3366b3b Bump version to 0.54
// FREEBIE
2015-06-25 11:02:00 -07:00
Moxie Marlinspike
a5ffd47935 Gotta stub out message field for delivery receipts w/ old clients
// FREEBIE
2015-06-25 11:00:59 -07:00
Moxie Marlinspike
18a96a445b Bump version to 0.53 2015-06-25 08:51:09 -07:00
Moxie Marlinspike
de366b976e Ignore unknown properties from federated responses.
// FREEBIE
2015-06-25 08:50:10 -07:00
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
143 changed files with 6165 additions and 2777 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ config/production.yml
config/federated.yml
config/staging.yml
.opsmanage
put.sh

View File

@@ -1,32 +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:
senderId:
apiKey:
# Optional. Only if iOS clients are supported.
apn:
# In PEM format.
certificate:
# In PEM format.
key:
s3:
s3: # AWS S3 configuration
accessKey:
accessSecret:
@@ -35,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:
@@ -52,24 +60,3 @@ federation:
authenticationToken: foo
certificate: in pem format
# Optional address of graphite server to report metrics
graphite:
host:
port:
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

102
pom.xml
View File

@@ -9,12 +9,10 @@
<groupId>org.whispersystems.textsecure</groupId>
<artifactId>TextSecureServer</artifactId>
<version>0.29</version>
<version>0.93</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>
<dropwizard.version>0.9.0-rc3</dropwizard.version>
</properties>
<dependencies>
@@ -53,109 +51,87 @@
<artifactId>dropwizard-metrics-graphite</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-client</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>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>140</version>
</dependency>
<dependency>
<groupId>com.google.android.gcm</groupId>
<artifactId>gcm-server</artifactId>
<version>1.0.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>
<version>0.2.3</version>
<version>1.46</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk</artifactId>
<version>1.4.1</version>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.10.6</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>2.5.0</version>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.6.1</version>
<version>2.7.3</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.twilio.sdk</groupId>
<artifactId>twilio-java-sdk</artifactId>
<version>3.4.5</version>
<version>4.4.4</version>
</dependency>
<dependency>
<groupId>postgresql</groupId>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.1-901.jdbc4</version>
</dependency>
<dependency>
<groupId>org.igniterealtime.smack</groupId>
<artifactId>smack-tcp</artifactId>
<version>4.0.0</version>
<version>9.4-1201-jdbc41</version>
</dependency>
<dependency>
<groupId>org.whispersystems</groupId>
<artifactId>websocket-resources</artifactId>
<version>0.2.0</version>
<version>0.3.2</version>
</dependency>
<dependency>
<groupId>org.whispersystems</groupId>
<artifactId>dropwizard-simpleauth</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>2.19</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</exclusion>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.api.version}</version>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.4.1</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.1</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

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

View File

@@ -25,6 +25,8 @@ message PubSubMessage {
QUERY_DB = 1;
DELIVER = 2;
KEEPALIVE = 3;
CLOSE = 4;
CONNECTED = 5;
}
optional Type type = 1;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (C) 2013 Open WhisperSystems
* 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
@@ -19,21 +19,24 @@ package textsecure;
option java_package = "org.whispersystems.textsecuregcm.entities";
option java_outer_classname = "MessageProtos";
message OutgoingMessageSignal {
message Envelope {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
PLAINTEXT = 4;
RECEIPT = 5;
}
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;
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 XXX -- Remove after 10/01/15
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.info("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,21 +17,23 @@
package org.whispersystems.textsecuregcm;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration;
import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration;
import org.whispersystems.textsecuregcm.configuration.MessageStoreConfiguration;
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;
@@ -44,9 +46,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private TwilioConfiguration twilio;
@JsonProperty
private NexmoConfiguration nexmo;
@NotNull
@Valid
@JsonProperty
@@ -60,17 +59,22 @@ public class WhisperServerConfiguration extends Configuration {
@NotNull
@Valid
@JsonProperty
private MemcacheConfiguration memcache;
private RedisConfiguration cache;
@NotNull
@Valid
@JsonProperty
private DirectoryConfiguration directory;
private RedisConfiguration directory;
@NotNull
@Valid
@NotNull
@JsonProperty
private MessageStoreConfiguration messageStore;
private DataSourceFactory messageStore;
@Valid
@NotNull
@JsonProperty
private List<TestDeviceConfiguration> testDevices = new LinkedList<>();
@Valid
@JsonProperty
@@ -111,10 +115,6 @@ public class WhisperServerConfiguration extends Configuration {
return twilio;
}
public NexmoConfiguration getNexmoConfiguration() {
return nexmo;
}
public PushConfiguration getPushConfiguration() {
return push;
}
@@ -127,15 +127,15 @@ public class WhisperServerConfiguration extends Configuration {
return s3;
}
public MemcacheConfiguration getMemcacheConfiguration() {
return memcache;
public RedisConfiguration getCacheConfiguration() {
return cache;
}
public DirectoryConfiguration getDirectoryConfiguration() {
public RedisConfiguration getDirectoryConfiguration() {
return directory;
}
public MessageStoreConfiguration getMessageStoreConfiguration() {
public DataSourceFactory getMessageStoreConfiguration() {
return messageStore;
}
@@ -158,4 +158,15 @@ public class WhisperServerConfiguration extends Configuration {
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

@@ -18,17 +18,21 @@ package org.whispersystems.textsecuregcm;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.graphite.GraphiteReporter;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.google.common.base.Optional;
import com.sun.jersey.api.client.Client;
import net.spy.memcached.MemcachedClient;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.glassfish.jersey.client.ClientProperties;
import org.skife.jdbi.v2.DBI;
import org.whispersystems.dispatch.DispatchChannel;
import org.whispersystems.dispatch.DispatchManager;
import org.whispersystems.dropwizard.simpleauth.AuthDynamicFeature;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.dropwizard.simpleauth.BasicCredentialAuthFilter;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
import org.whispersystems.textsecuregcm.auth.MultiBasicAuthProvider;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.AttachmentController;
import org.whispersystems.textsecuregcm.controllers.DeviceController;
@@ -39,45 +43,52 @@ 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.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.CpuUsageGauge;
import org.whispersystems.textsecuregcm.metrics.FileDescriptorGauge;
import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge;
import org.whispersystems.textsecuregcm.metrics.JsonMetricsReporter;
import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
import org.whispersystems.textsecuregcm.providers.MemcacheHealthCheck;
import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory;
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;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Accounts;
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.StoredMessages;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.UrlSigner;
import org.whispersystems.textsecuregcm.websocket.ConnectListener;
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.TrimMessagesCommand;
import org.whispersystems.textsecuregcm.workers.VacuumCommand;
import org.whispersystems.websocket.WebSocketResourceProviderFactory;
import org.whispersystems.websocket.setup.WebSocketEnvironment;
@@ -85,6 +96,7 @@ import org.whispersystems.websocket.setup.WebSocketEnvironment;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletRegistration;
import javax.ws.rs.client.Client;
import java.security.Security;
import java.util.EnumSet;
import java.util.concurrent.TimeUnit;
@@ -95,7 +107,6 @@ import io.dropwizard.client.JerseyClientBuilder;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.jdbi.DBIFactory;
import io.dropwizard.metrics.graphite.GraphiteReporterFactory;
import io.dropwizard.migrations.MigrationsBundle;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import redis.clients.jedis.JedisPool;
@@ -110,12 +121,20 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
bootstrap.addCommand(new DirectoryCommand());
bootstrap.addCommand(new VacuumCommand());
bootstrap.addBundle(new MigrationsBundle<WhisperServerConfiguration>() {
bootstrap.addCommand(new TrimMessagesCommand());
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") {
@Override
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
@@ -129,97 +148,127 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
{
SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics());
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
environment.getObjectMapper().setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
environment.getObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
DBIFactory dbiFactory = new DBIFactory();
DBI jdbi = dbiFactory.build(environment, config.getDataSourceFactory(), "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);
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 directoryClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
JedisPool messageStoreClient = new RedisClientFactory(config.getMessageStoreConfiguration().getUrl()).getRedisClientPool();
Client httpClient = new JerseyClientBuilder(environment).using(config.getJerseyClientConfiguration())
.build(getName());
RedisClientFactory cacheClientFactory = new RedisClientFactory(config.getCacheConfiguration().getUrl());
JedisPool cacheClient = cacheClientFactory.getRedisClientPool();
JedisPool directoryClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
Client httpClient = initializeHttpClient(environment, config);
DirectoryManager directory = new DirectoryManager(directoryClient);
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());
StoredMessages storedMessages = new StoredMessages(messageStoreClient);
PubSubManager pubSubManager = new PubSubManager(messageStoreClient);
PushServiceClient pushServiceClient = new PushServiceClient(httpClient, config.getPushConfiguration());
WebsocketSender websocketSender = new WebsocketSender(storedMessages, pubSubManager);
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
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(environment, config.getJerseyClientConfiguration(), 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 );
FederatedPeerAuthenticator federatedPeerAuthenticator = new FederatedPeerAuthenticator(config.getFederationConfiguration());
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheClient);
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushServiceClient, pubSubManager);
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
SmsSender smsSender = new SmsSender(twilioSmsSender);
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
PushSender pushSender = new PushSender(pushServiceClient, websocketSender);
PushSender pushSender = new PushSender(apnFallbackManager, pushServiceClient, websocketSender, config.getPushConfiguration().getQueueSize());
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager);
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
environment.lifecycle().manage(apnFallbackManager);
environment.lifecycle().manage(pubSubManager);
environment.lifecycle().manage(feedbackHandler);
environment.lifecycle().manage(pushSender);
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager);
KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager);
MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager);
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, federatedClientManager);
environment.jersey().register(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
FederatedPeer.class,
deviceAuthenticator,
Device.class, "WhisperServer"));
environment.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<Account>()
.setAuthenticator(deviceAuthenticator)
.setPrincipal(Account.class)
.buildAuthFilter(),
new BasicCredentialAuthFilter.Builder<FederatedPeer>()
.setAuthenticator(federatedPeerAuthenticator)
.setPrincipal(FederatedPeer.class)
.buildAuthFilter()));
environment.jersey().register(new AuthValueFactoryProvider.Binder());
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, storedMessages, new TimeProvider(), authorizationKey));
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters));
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, messagesManager, new TimeProvider(), authorizationKey, config.getTestDevices()));
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, 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(accountsManager, federatedClientManager, pushSender));
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()) {
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config);
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config, 90000);
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator));
webSocketEnvironment.setConnectListener(new ConnectListener(accountsManager, pushSender, storedMessages, pubSubManager));
webSocketEnvironment.jersey().register(new KeepAliveController());
WebSocketResourceProviderFactory servlet = new WebSocketResourceProviderFactory(webSocketEnvironment);
webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(accountsManager, pushSender, receiptSender, messagesManager, pubSubManager, apnFallbackManager));
webSocketEnvironment.jersey().register(new KeepAliveController(pubSubManager));
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", servlet);
websocket.addMapping("/v1/websocket/*");
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);
servlet.start();
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("allowedHeaders", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin,X-Signal-Agent");
filter.setInitParameter("allowedMethods", "GET,PUT,POST,DELETE,OPTIONS");
filter.setInitParameter("preflightMaxAge", "5184000");
filter.setInitParameter("allowCredentials", "true");
}
environment.healthChecks().register("directory", new RedisHealthCheck(directoryClient));
environment.healthChecks().register("messagestore", new RedisHealthCheck(messageStoreClient));
environment.healthChecks().register("memcache", new MemcacheHealthCheck(memcachedClient));
environment.healthChecks().register("cache", new RedisHealthCheck(cacheClient));
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());
environment.metrics().register(name(FileDescriptorGauge.class, "fd_count"), new FileDescriptorGauge());
if (config.getGraphiteConfiguration().isEnabled()) {
GraphiteReporterFactory graphiteReporterFactory = new GraphiteReporterFactory();
@@ -231,16 +280,17 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
}
}
private Optional<NexmoSmsSender> initializeNexmoSmsSender(NexmoConfiguration configuration) {
if (configuration == null) {
return Optional.absent();
} else {
return Optional.of(new NexmoSmsSender(configuration));
}
private Client initializeHttpClient(Environment environment, WhisperServerConfiguration config) {
Client httpClient = new JerseyClientBuilder(environment).using(config.getJerseyClientConfiguration())
.build(getName());
httpClient.property(ClientProperties.CONNECT_TIMEOUT, 1000);
httpClient.property(ClientProperties.READ_TIMEOUT, 1000);
return httpClient;
}
public static void main(String[] args) throws Exception {
new WhisperServerService().run(args);
}
}

View File

@@ -22,6 +22,7 @@ import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dropwizard.simpleauth.Authenticator;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
@@ -29,7 +30,6 @@ import org.whispersystems.textsecuregcm.util.Constants;
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> {

View File

@@ -1,6 +1,7 @@
package org.whispersystems.textsecuregcm.auth;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
@@ -16,61 +17,13 @@ import java.util.concurrent.TimeUnit;
public class AuthorizationToken {
private final Logger logger = LoggerFactory.getLogger(AuthorizationToken.class);
@JsonProperty
private String token;
private final String token;
private final byte[] key;
public AuthorizationToken(String token, byte[] key) {
public AuthorizationToken(String token) {
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;
}
}
public AuthorizationToken() {}
}

View File

@@ -0,0 +1,90 @@
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 AuthorizationTokenGenerator {
private final Logger logger = LoggerFactory.getLogger(AuthorizationTokenGenerator.class);
private final byte[] key;
public AuthorizationTokenGenerator(byte[] key) {
this.key = key;
}
public AuthorizationToken generateFor(String number) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
long currentTimeSeconds = System.currentTimeMillis() / 1000;
String prefix = number + ":" + currentTimeSeconds;
mac.init(new SecretKeySpec(key, "HmacSHA256"));
String output = Hex.encodeHexString(Util.truncate(mac.doFinal(prefix.getBytes()), 10));
String token = prefix + ":" + output;
return new AuthorizationToken(token);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public boolean isValid(String token, 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

@@ -22,6 +22,7 @@ import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dropwizard.simpleauth.Authenticator;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.util.Constants;
@@ -30,7 +31,6 @@ import java.util.List;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;

View File

@@ -1,66 +0,0 @@
/**
* 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.auth;
import com.sun.jersey.api.model.Parameter;
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 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 BasicAuthProvider<T1> provider1;
private final BasicAuthProvider<T2> provider2;
private final Class<?> clazz1;
private final Class<?> clazz2;
public MultiBasicAuthProvider(Authenticator<BasicCredentials, T1> authenticator1,
Class<?> clazz1,
Authenticator<BasicCredentials, T2> authenticator2,
Class<?> clazz2,
String realm)
{
this.provider1 = new BasicAuthProvider<>(authenticator1, realm);
this.provider2 = new BasicAuthProvider<>(authenticator2, realm);
this.clazz1 = clazz1;
this.clazz2 = clazz2;
}
@Override
public ComponentScope getScope() {
return ComponentScope.PerRequest;
}
@Override
public Injectable<?> getInjectable(ComponentContext componentContext,
Auth auth, Parameter parameter)
{
if (parameter.getParameterClass().equals(clazz1)) {
return this.provider1.getInjectable(componentContext, auth, parameter);
} else {
return this.provider2.getInjectable(componentContext, auth, parameter);
}
}
}

View File

@@ -1,46 +0,0 @@
/**
* 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.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
public class MemcacheConfiguration {
@NotEmpty
@JsonProperty
private String servers;
@JsonProperty
private String user;
@JsonProperty
private String password;
public String getServers() {
return servers;
}
public String getUser() {
return user;
}
public String getPassword() {
return password;
}
}

View File

@@ -1,43 +0,0 @@
/**
* 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.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
public class NexmoConfiguration {
@JsonProperty
private String apiKey;
@JsonProperty
private String apiSecret;
@JsonProperty
private String number;
public String getApiKey() {
return apiKey;
}
public String getApiSecret() {
return apiSecret;
}
public String getNumber() {
return number;
}
}

View File

@@ -22,6 +22,10 @@ public class PushConfiguration {
@NotEmpty
private String password;
@JsonProperty
@Min(0)
private int queueSize = 200;
public String getHost() {
return host;
}
@@ -37,4 +41,8 @@ public class PushConfiguration {
public String getPassword() {
return password;
}
public int getQueueSize() {
return queueSize;
}
}

View File

@@ -21,7 +21,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.URL;
public class DirectoryConfiguration {
public class RedisConfiguration {
@JsonProperty
@NotEmpty

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

@@ -19,6 +19,9 @@ package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
public class TwilioConfiguration {
@NotEmpty
@@ -29,17 +32,14 @@ public class TwilioConfiguration {
@JsonProperty
private String accountToken;
@NotEmpty
@NotNull
@JsonProperty
private String number;
private List<String> numbers;
@NotEmpty
@JsonProperty
private String localDomain;
@JsonProperty
private boolean international;
public String getAccountId() {
return accountId;
}
@@ -48,15 +48,11 @@ public class TwilioConfiguration {
return accountToken;
}
public String getNumber() {
return number;
public List<String> getNumbers() {
return numbers;
}
public String getLocalDomain() {
return localDomain;
}
public boolean isInternational() {
return international;
}
}

View File

@@ -23,11 +23,12 @@ 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.AuthorizationTokenGenerator;
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.auth.AuthorizationToken;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.providers.TimeProvider;
import org.whispersystems.textsecuregcm.sms.SmsSender;
@@ -35,11 +36,10 @@ 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.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
@@ -51,12 +51,14 @@ 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.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Map;
import io.dropwizard.auth.Auth;
@@ -65,36 +67,45 @@ public class AccountController {
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
private final PendingAccountsManager pendingAccounts;
private final AccountsManager accounts;
private final RateLimiters rateLimiters;
private final SmsSender smsSender;
private final StoredMessages storedMessages;
private final TimeProvider timeProvider;
private final Optional<byte[]> authorizationKey;
private final PendingAccountsManager pendingAccounts;
private final AccountsManager accounts;
private final RateLimiters rateLimiters;
private final SmsSender smsSender;
private final MessagesManager messagesManager;
private final TimeProvider timeProvider;
private final Optional<AuthorizationTokenGenerator> tokenGenerator;
private final Map<String, Integer> testDevices;
public AccountController(PendingAccountsManager pendingAccounts,
AccountsManager accounts,
RateLimiters rateLimiters,
SmsSender smsSenderFactory,
StoredMessages storedMessages,
MessagesManager messagesManager,
TimeProvider timeProvider,
Optional<byte[]> authorizationKey)
Optional<byte[]> authorizationKey,
Map<String, Integer> testDevices)
{
this.pendingAccounts = pendingAccounts;
this.accounts = accounts;
this.rateLimiters = rateLimiters;
this.smsSender = smsSenderFactory;
this.storedMessages = storedMessages;
this.messagesManager = messagesManager;
this.timeProvider = timeProvider;
this.authorizationKey = authorizationKey;
this.testDevices = testDevices;
if (authorizationKey.isPresent()) {
tokenGenerator = Optional.of(new AuthorizationTokenGenerator(authorizationKey.get()));
} else {
tokenGenerator = Optional.absent();
}
}
@Timed
@GET
@Path("/{transport}/code/{number}")
public Response createAccount(@PathParam("transport") String transport,
@PathParam("number") String number)
@PathParam("number") String number,
@QueryParam("client") Optional<String> client)
throws IOException, RateLimitExceededException
{
if (!Util.isValidNumber(number)) {
@@ -113,11 +124,13 @@ 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")) {
smsSender.deliverSmsVerification(number, verificationCode.getVerificationCodeDisplay());
if (testDevices.containsKey(number)) {
// noop
} else if (transport.equals("sms")) {
smsSender.deliverSmsVerification(number, client, verificationCode.getVerificationCodeDisplay());
} else if (transport.equals("voice")) {
smsSender.deliverVoxVerification(number, verificationCode.getVerificationCodeSpeech());
}
@@ -131,6 +144,7 @@ public class AccountController {
@Path("/code/{verification_code}")
public void verifyAccount(@PathParam("verification_code") String verificationCode,
@HeaderParam("Authorization") String authorizationHeader,
@HeaderParam("X-Signal-Agent") String userAgent,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
{
@@ -153,7 +167,7 @@ public class AccountController {
throw new WebApplicationException(Response.status(417).build());
}
createAccount(number, password, accountAttributes);
createAccount(number, password, userAgent, accountAttributes);
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad Authorization Header", e);
throw new WebApplicationException(Response.status(401).build());
@@ -166,6 +180,7 @@ public class AccountController {
@Path("/token/{verification_token}")
public void verifyToken(@PathParam("verification_token") String verificationToken,
@HeaderParam("Authorization") String authorizationHeader,
@HeaderParam("X-Signal-Agent") String userAgent,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
{
@@ -176,32 +191,50 @@ public class AccountController {
rateLimiters.getVerifyLimiter().validate(number);
if (!authorizationKey.isPresent()) {
if (!tokenGenerator.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())) {
if (!tokenGenerator.get().isValid(verificationToken, number, timeProvider.getCurrentTimeMillis())) {
throw new WebApplicationException(Response.status(403).build());
}
createAccount(number, password, accountAttributes);
createAccount(number, password, userAgent, accountAttributes);
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad authorization header", e);
throw new WebApplicationException(Response.status(401).build());
}
}
@Timed
@GET
@Path("/token/")
@Produces(MediaType.APPLICATION_JSON)
public AuthorizationToken verifyToken(@Auth Account account)
throws RateLimitExceededException
{
if (!tokenGenerator.isPresent()) {
logger.debug("Attempt to authorize with key but not configured...");
throw new WebApplicationException(Response.status(404).build());
}
return tokenGenerator.get().generateFor(account.getNumber());
}
@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);
}
@@ -211,6 +244,7 @@ public class AccountController {
public void deleteGcmRegistrationId(@Auth Account account) {
Device device = account.getAuthenticatedDevice().get();
device.setGcmId(null);
device.setFetchesMessages(false);
accounts.update(account);
}
@@ -221,7 +255,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);
}
@@ -231,6 +267,28 @@ 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("/attributes/")
@Consumes(MediaType.APPLICATION_JSON)
public void setAccountAttributes(@Auth Account account,
@HeaderParam("X-Signal-Agent") String userAgent,
@Valid AccountAttributes attributes)
{
Device device = account.getAuthenticatedDevice().get();
device.setFetchesMessages(attributes.getFetchesMessages());
device.setName(attributes.getName());
device.setLastSeen(Util.todayInMillis());
device.setVoiceSupported(attributes.getVoice());
device.setRegistrationId(attributes.getRegistrationId());
device.setSignalingKey(attributes.getSignalingKey());
device.setUserAgent(userAgent);
accounts.update(account);
}
@@ -243,28 +301,34 @@ public class AccountController {
encodedVerificationText)).build();
}
private void createAccount(String number, String password, AccountAttributes accountAttributes) {
private void createAccount(String number, String password, String userAgent, 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.setVoiceSupported(accountAttributes.getVoice());
device.setCreated(System.currentTimeMillis());
device.setLastSeen(Util.todayInMillis());
device.setUserAgent(userAgent);
Account account = new Account();
account.setNumber(number);
account.setSupportsSms(accountAttributes.getSupportsSms());
account.addDevice(device);
accounts.create(account);
storedMessages.clear(new WebsocketAddress(number, Device.MASTER_ID));
messagesManager.clear(number);
pendingAccounts.remove(number);
logger.debug("Stored device...");
}
@VisibleForTesting protected VerificationCode generateVerificationCode() {
@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

@@ -25,16 +25,21 @@ 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.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
import 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;
@@ -44,8 +49,11 @@ 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;
@@ -54,28 +62,68 @@ 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 MessagesManager messages;
private final RateLimiters rateLimiters;
public DeviceController(PendingDevicesManager pendingDevices,
AccountsManager accounts,
MessagesManager messages,
RateLimiters rateLimiters)
{
this.pendingDevices = pendingDevices;
this.accounts = accounts;
this.messages = messages;
this.rateLimiters = rateLimiters;
}
@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);
messages.clear(account.getNumber(), deviceId);
}
@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());
@@ -90,7 +138,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);
@@ -102,7 +150,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());
}
@@ -113,11 +161,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

@@ -74,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();
@@ -100,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);
@@ -110,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

@@ -150,7 +150,7 @@ public class FederationControllerV1 extends FederationController {
for (Account account : accountList) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported());
if (!account.isActive()) {
clientContact.setInactive(true);

View File

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

View File

@@ -1,19 +1,54 @@
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.PUT;
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() {
public Response getKeepAlive(@Auth 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();
}
@Timed
@GET
@Path("/provisioning")
public Response getProvisioningKeepAlive() {
return Response.ok().build();
}

View File

@@ -16,8 +16,10 @@
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
@@ -38,6 +40,8 @@ import io.dropwizard.auth.Auth;
public class KeysController {
private static final Logger logger = LoggerFactory.getLogger(KeysController.class);
protected final RateLimiters rateLimiters;
protected final Keys keys;
protected final AccountsManager accounts;
@@ -52,7 +56,6 @@ public class KeysController {
this.federatedClientManager = federatedClientManager;
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
public PreKeyCount getStatus(@Auth Account account) {
@@ -87,8 +90,16 @@ public class KeysController {
throw new NoSuchUserException("Target device is inactive.");
}
Optional<List<KeyRecord>> preKeys = keys.get(number, deviceId);
return new TargetKeys(destination.get(), preKeys);
for (int i=0;i<20;i++) {
try {
Optional<List<KeyRecord>> preKeys = keys.get(number, deviceId);
return new TargetKeys(destination.get(), preKeys);
} catch (UnableToExecuteStatementException e) {
logger.info(e.getMessage());
}
}
throw new WebApplicationException(Response.status(500).build());
} catch (NumberFormatException e) {
throw new WebApplicationException(Response.status(422).build());
}

View File

@@ -23,9 +23,11 @@ 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.MessageResponse;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
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;
@@ -33,15 +35,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.POST;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@@ -64,17 +70,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;
}
@@ -82,16 +94,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) {
@@ -105,37 +122,58 @@ public class MessageController {
.type(MediaType.APPLICATION_JSON)
.entity(new StaleDevices(e.getStaleDevices()))
.build());
} catch (InvalidDestinationException e) {
throw new WebApplicationException(Response.status(400).build());
}
}
@Timed
@POST
@Consumes(MediaType.APPLICATION_JSON)
@GET
@Produces(MediaType.APPLICATION_JSON)
public MessageResponse sendMessageLegacy(@Auth Account source, @Valid IncomingMessageList messages)
throws IOException, RateLimitExceededException
public OutgoingMessageEntityList getPendingMessages(@Auth Account account) {
return 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 {
List<IncomingMessage> incomingMessages = messages.getMessages();
validateLegacyDestinations(incomingMessages);
Optional<OutgoingMessageEntity> message = messagesManager.delete(account.getNumber(),
account.getAuthenticatedDevice().get().getId(),
source, timestamp);
messages.setRelay(incomingMessages.get(0).getRelay());
sendMessage(source, incomingMessages.get(0).getDestination(), messages);
return new MessageResponse(new LinkedList<String>(), new LinkedList<String>());
} catch (ValidationException e) {
throw new WebApplicationException(Response.status(422).build());
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 (NotPushRegisteredException e) {
logger.info("User no longer push registered for delivery receipt: " + e.getMessage());
} catch (NoSuchUserException | TransientPushFailureException e) {
logger.warn("Sending delivery receipt", e);
}
}
private void sendLocalMessage(Account source,
String destinationName,
IncomingMessageList messages)
throws NoSuchUserException, MismatchedDevicesException, IOException, StaleDevicesException
IncomingMessageList messages,
boolean isSyncMessage)
throws NoSuchUserException, MismatchedDevicesException, 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()) {
@@ -152,19 +190,24 @@ public class MessageController {
Device destinationDevice,
long timestamp,
IncomingMessage incomingMessage)
throws NoSuchUserException, IOException
throws NoSuchUserException
{
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(timestamp == 0 ? System.currentTimeMillis() : timestamp)
.setSourceDevice((int)source.getAuthenticatedDevice().get().getId());
.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()) {
@@ -175,17 +218,17 @@ public class MessageController {
} catch (NotPushRegisteredException e) {
if (destinationDevice.isMaster()) throw new NoSuchUserException(e);
else logger.debug("Not registered", e);
} catch (TransientPushFailureException e) {
if (destinationDevice.isMaster()) throw new IOException(e);
else logger.debug("Transient failure", e);
}
}
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(),
@@ -228,7 +271,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<>();
@@ -242,7 +287,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())) {
@@ -262,23 +309,9 @@ public class MessageController {
}
}
private void validateLegacyDestinations(List<IncomingMessage> messages)
throws ValidationException
{
String destination = null;
for (IncomingMessage message : messages) {
if ((message.getDestination() == null) ||
(destination != null && !destination.equals(message.getDestination())))
{
throw new ValidationException("Multiple account destinations!");
}
destination = message.getDestination();
}
}
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) {
@@ -286,4 +319,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

@@ -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

@@ -2,14 +2,10 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
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 javax.ws.rs.PUT;
import javax.ws.rs.Path;
@@ -18,25 +14,16 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.List;
import io.dropwizard.auth.Auth;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
@Path("/v1/receipt")
public class ReceiptController {
private final AccountsManager accountManager;
private final PushSender pushSender;
private final FederatedClientManager federatedClientManager;
private final ReceiptSender receiptSender;
public ReceiptController(AccountsManager accountManager,
FederatedClientManager federatedClientManager,
PushSender pushSender)
{
this.accountManager = accountManager;
this.federatedClientManager = federatedClientManager;
this.pushSender = pushSender;
public ReceiptController(ReceiptSender receiptSender) {
this.receiptSender = receiptSender;
}
@Timed
@@ -49,8 +36,7 @@ public class ReceiptController {
throws IOException
{
try {
if (relay.isPresent()) sendRelayedReceipt(source, destination, messageId, relay.get());
else sendDirectReceipt(source, destination, messageId);
receiptSender.sendReceipt(source, destination, messageId, relay);
} catch (NoSuchUserException | NotPushRegisteredException e) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
} catch (TransientPushFailureException e) {
@@ -58,51 +44,4 @@ public class ReceiptController {
}
}
private void sendRelayedReceipt(Account source, String destination, long messageId, String relay)
throws NoSuchUserException, IOException
{
try {
federatedClientManager.getClient(relay)
.sendDeliveryReceipt(source.getNumber(),
source.getAuthenticatedDevice().get().getId(),
destination, messageId);
} catch (NoSuchPeerException e) {
throw new NoSuchUserException(e);
}
}
private void sendDirectReceipt(Account source, String destination, long messageId)
throws NotPushRegisteredException, TransientPushFailureException, NoSuchUserException
{
Account destinationAccount = getDestinationAccount(destination);
List<Device> destinationDevices = destinationAccount.getDevices();
OutgoingMessageSignal.Builder message =
OutgoingMessageSignal.newBuilder()
.setSource(source.getNumber())
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId())
.setTimestamp(messageId)
.setType(OutgoingMessageSignal.Type.RECEIPT_VALUE);
if (source.getRelay().isPresent()) {
message.setRelay(source.getRelay().get());
}
for (Device destinationDevice : destinationDevices) {
pushSender.sendMessage(destinationAccount, destinationDevice, message.build());
}
}
private Account getDestinationAccount(String destination)
throws NoSuchUserException
{
Optional<Account> account = accountManager.get(destination);
if (!account.isPresent()) {
throw new NoSuchUserException(destination);
}
return account.get();
}
}

View File

@@ -17,6 +17,8 @@
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;
public class AccountAttributes {
@@ -25,32 +27,39 @@ public class AccountAttributes {
@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;
@JsonProperty
private boolean voice;
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, false);
}
@VisibleForTesting
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name, boolean voice) {
this.signalingKey = signalingKey;
this.supportsSms = supportsSms;
this.fetchesMessages = fetchesMessages;
this.registrationId = registrationId;
this.name = name;
this.voice = voice;
}
public String getSignalingKey() {
return signalingKey;
}
public boolean getSupportsSms() {
return supportsSms;
}
public boolean getFetchesMessages() {
return fetchesMessages;
}
@@ -58,4 +67,13 @@ public class AccountAttributes {
public int getRegistrationId() {
return registrationId;
}
public String getName() {
return name;
}
public boolean getVoice() {
return voice;
}
}

View File

@@ -5,8 +5,12 @@ 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;
@@ -23,12 +27,50 @@ public class ApnMessage {
@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) {
this.apnId = apnId;
this.number = number;
this.deviceId = deviceId;
this.message = message;
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

@@ -32,14 +32,16 @@ public class ClientContact {
@JsonProperty
private byte[] token;
@JsonProperty
private boolean voice;
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, boolean voice) {
this.token = token;
this.relay = relay;
this.voice = voice;
}
public ClientContact() {}
@@ -56,10 +58,6 @@ public class ClientContact {
this.relay = relay;
}
public boolean isSupportsSms() {
return supportsSms;
}
public boolean isInactive() {
return inactive;
}
@@ -68,9 +66,13 @@ public class ClientContact {
this.inactive = inactive;
}
// public String toString() {
// return new Gson().toJson(this);
// }
public boolean isVoice() {
return voice;
}
public void setVoice(boolean voice) {
this.voice = voice;
}
@Override
public boolean equals(Object other) {
@@ -81,8 +83,8 @@ public class ClientContact {
return
Arrays.equals(this.token, that.token) &&
this.supportsSms == that.supportsSms &&
this.inactive == that.inactive &&
this.voice == that.voice &&
(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;
@@ -44,8 +44,7 @@ public class EncryptedOutgoingMessage {
private final byte[] serialized;
private final String serializedAndEncoded;
public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage,
String signalingKey)
public EncryptedOutgoingMessage(Envelope outgoingMessage, String signalingKey)
throws CryptoEncodingException
{
byte[] plaintext = outgoingMessage.toByteArray();

View File

@@ -20,20 +20,23 @@ public class GcmMessage {
private int deviceId;
@JsonProperty
@NotEmpty
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) {
this.gcmId = gcmId;
this.number = number;
this.deviceId = deviceId;
this.message = message;
this.receipt = receipt;
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,9 +34,11 @@ public class IncomingMessage {
private int destinationRegistrationId;
@JsonProperty
@NotEmpty
private String body;
@JsonProperty
private String content;
@JsonProperty
private String relay;
@@ -67,4 +69,8 @@ public class IncomingMessage {
public int getDestinationRegistrationId() {
return destinationRegistrationId;
}
public String getContent() {
return content;
}
}

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,30 @@
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;
@JsonProperty
private boolean more;
public OutgoingMessageEntityList() {}
public OutgoingMessageEntityList(List<OutgoingMessageEntity> messages, boolean more) {
this.messages = messages;
this.more = more;
}
public List<OutgoingMessageEntity> getMessages() {
return messages;
}
public boolean hasMore() {
return more;
}
}

View File

@@ -16,6 +16,7 @@
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
@@ -42,7 +43,19 @@ public class PreKeyResponseV2 {
}
@VisibleForTesting
public List<PreKeyResponseItemV2> getDevices() {
return devices;
@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

@@ -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

@@ -11,6 +11,9 @@ public class UnregisteredEvent {
@NotEmpty
private String registrationId;
@JsonProperty
private String canonicalId;
@JsonProperty
@NotEmpty
private String number;
@@ -26,6 +29,10 @@ public class UnregisteredEvent {
return registrationId;
}
public String getCanonicalId() {
return canonicalId;
}
public String getNumber() {
return number;
}

View File

@@ -18,17 +18,15 @@ package org.whispersystems.textsecuregcm.federation;
import com.google.common.base.Optional;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.UniformInterfaceException;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.api.json.JSONConfiguration;
import com.sun.jersey.client.urlconnection.HTTPSProperties;
import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.bouncycastle.openssl.PEMReader;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.RequestEntityProcessing;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AccountCount;
@@ -38,11 +36,13 @@ import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.ByteArrayInputStream;
@@ -57,7 +57,10 @@ import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
import io.dropwizard.client.JerseyClientBuilder;
import io.dropwizard.client.JerseyClientConfiguration;
import io.dropwizard.setup.Environment;
public class FederatedClient {
@@ -73,15 +76,14 @@ public class FederatedClient {
private final FederatedPeer peer;
private final Client client;
private final String authorizationHeader;
public FederatedClient(String federationName, FederatedPeer peer)
public FederatedClient(Environment environment, JerseyClientConfiguration configuration,
String federationName, FederatedPeer peer)
throws IOException
{
try {
this.client = Client.create(getClientConfig(peer));
this.peer = peer;
this.authorizationHeader = getAuthorizationHeader(federationName, peer);
this.client = createClient(environment, configuration, federationName, peer);
this.peer = peer;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (KeyStoreException | KeyManagementException | CertificateException e) {
@@ -91,20 +93,14 @@ public class FederatedClient {
public URL getSignedAttachmentUri(long attachmentId) throws IOException {
try {
WebResource resource = client.resource(peer.getUrl())
.path(String.format(ATTACHMENT_URI_PATH, attachmentId));
AttachmentUri response = client.target(peer.getUrl())
.path(String.format(ATTACHMENT_URI_PATH, attachmentId))
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(AttachmentUri.class);
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 response.getEntity(AttachmentUri.class).getLocation();
} catch (UniformInterfaceException | ClientHandlerException e) {
return response.getLocation();
} catch (ProcessingException e) {
logger.warn("Bad URI", e);
throw new IOException(e);
}
@@ -112,19 +108,14 @@ public class FederatedClient {
public Optional<PreKeyResponseV1> getKeysV1(String destination, String device) {
try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V1, destination, device));
PreKeyResponseV1 response = client.target(peer.getUrl())
.path(String.format(PREKEY_PATH_DEVICE_V1, destination, device))
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(PreKeyResponseV1.class);
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(PreKeyResponseV1.class));
} catch (UniformInterfaceException | ClientHandlerException e) {
return Optional.of(response);
} catch (ProcessingException e) {
logger.warn("PreKey", e);
return Optional.absent();
}
@@ -132,34 +123,29 @@ 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));
PreKeyResponseV2 response = client.target(peer.getUrl())
.path(String.format(PREKEY_PATH_DEVICE_V2, destination, device))
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(PreKeyResponseV2.class);
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) {
return Optional.of(response);
} catch (ProcessingException e) {
logger.warn("PreKey", e);
return Optional.absent();
}
}
public int getUserCount() {
try {
WebResource resource = client.resource(peer.getUrl()).path(USER_COUNT_PATH);
AccountCount count = resource.accept(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
.get(AccountCount.class);
AccountCount count = client.target(peer.getUrl())
.path(USER_COUNT_PATH)
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(AccountCount.class);
return count.getCount();
} catch (UniformInterfaceException | ClientHandlerException e) {
} catch (ProcessingException e) {
logger.warn("User Count", e);
return 0;
}
@@ -167,13 +153,14 @@ public class FederatedClient {
public List<ClientContact> getUserTokens(int offset) {
try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(USER_TOKENS_PATH, offset));
ClientContacts contacts = resource.accept(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
.get(ClientContacts.class);
ClientContacts contacts = client.target(peer.getUrl())
.path(String.format(USER_TOKENS_PATH, offset))
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(ClientContacts.class);
return contacts.getContacts();
} catch (UniformInterfaceException | ClientHandlerException e) {
} catch (ProcessingException e) {
logger.warn("User Tokens", e);
return null;
}
@@ -182,46 +169,53 @@ public class FederatedClient {
public void sendMessages(String source, long sourceDeviceId, String destination, IncomingMessageList messages)
throws IOException
{
Response response = null;
try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(RELAY_MESSAGE_PATH, source, sourceDeviceId, destination));
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
.entity(messages)
.put(ClientResponse.class);
response = client.target(peer.getUrl())
.path(String.format(RELAY_MESSAGE_PATH, source, sourceDeviceId, destination))
.request()
.put(Entity.json(messages));
if (response.getStatus() != 200 && response.getStatus() != 204) {
throw new WebApplicationException(clientResponseToResponse(response));
if (response.getStatus() == 411) throw new WebApplicationException(Response.status(413).build());
else throw new WebApplicationException(Response.status(response.getStatusInfo()).build());
}
} catch (UniformInterfaceException | ClientHandlerException e) {
} catch (ProcessingException e) {
logger.warn("sendMessage", e);
throw new IOException(e);
} finally {
if (response != null) response.close();
}
}
public void sendDeliveryReceipt(String source, long sourceDeviceId, String destination, long messageId)
throws IOException
{
Response response = null;
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);
response = client.target(peer.getUrl())
.path(String.format(RECEIPT_PATH, source, sourceDeviceId, destination, messageId))
.request()
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
.put(Entity.entity("", MediaType.APPLICATION_JSON_TYPE));
if (response.getStatus() != 200 && response.getStatus() != 204) {
throw new WebApplicationException(clientResponseToResponse(response));
if (response.getStatus() == 411) throw new WebApplicationException(Response.status(413).build());
else throw new WebApplicationException(Response.status(response.getStatusInfo()).build());
}
} catch (UniformInterfaceException | ClientHandlerException e) {
} catch (ProcessingException e) {
logger.warn("sendMessage", e);
throw new IOException(e);
} finally {
if (response != null) response.close();
}
}
private String getAuthorizationHeader(String federationName, FederatedPeer peer) {
return "Basic " + Base64.encodeBytes((federationName + ":" + peer.getAuthenticationToken()).getBytes());
}
private ClientConfig getClientConfig(FederatedPeer peer)
private Client createClient(Environment environment, JerseyClientConfiguration configuration,
String federationName, FederatedPeer peer)
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, CertificateException
{
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
@@ -230,12 +224,19 @@ public class FederatedClient {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), SecureRandom.getInstance("SHA1PRNG"));
ClientConfig config = new DefaultClientConfig();
config.getProperties().put(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES,
new HTTPSProperties(new StrictHostnameVerifier(), sslContext));
config.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE);
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new DefaultHostnameVerifier());
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register("https", sslConnectionSocketFactory).build();
return config;
Client client = new JerseyClientBuilder(environment).using(configuration)
.using(registry)
.build("FederatedClient");
client.property(ClientProperties.CONNECT_TIMEOUT, 5000);
client.property(ClientProperties.READ_TIMEOUT, 10000);
client.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED);
client.register(HttpAuthenticationFeature.basic(federationName, peer.getAuthenticationToken()));
return client;
}
private KeyStore initializeTrustStore(String name, String pemCertificate)
@@ -261,19 +262,6 @@ public class FederatedClient {
}
}
private Response clientResponseToResponse(ClientResponse r) {
Response.ResponseBuilder rb = Response.status(r.getStatus());
for (Map.Entry<String, List<String>> entry : r.getHeaders().entrySet()) {
for (String value : entry.getValue()) {
rb.header(entry.getKey(), value);
}
}
rb.entity(r.getEntityInputStream());
return rb.build();
}
public String getPeerName() {
return peer.getName();
}

View File

@@ -25,13 +25,18 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import io.dropwizard.client.JerseyClientConfiguration;
import io.dropwizard.setup.Environment;
public class FederatedClientManager {
private final Logger logger = LoggerFactory.getLogger(FederatedClientManager.class);
private final HashMap<String, FederatedClient> clients = new HashMap<>();
public FederatedClientManager(FederationConfiguration federationConfig)
public FederatedClientManager(Environment environment,
JerseyClientConfiguration clientConfig,
FederationConfiguration federationConfig)
throws IOException
{
List<FederatedPeer> peers = federationConfig.getPeers();
@@ -40,7 +45,7 @@ public class FederatedClientManager {
if (peers != null) {
for (FederatedPeer peer : peers) {
logger.info("Adding peer: " + peer.getName());
clients.put(peer.getName(), new FederatedClient(identity, peer));
clients.put(peer.getName(), new FederatedClient(environment, clientConfig, identity, peer));
}
}
}

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, null));
return Optional.of(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "NA"));
}
}

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

@@ -19,27 +19,38 @@ package org.whispersystems.textsecuregcm.limits;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import net.spy.memcached.MemcachedClient;
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.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)
{
MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
this.meter = metricRegistry.meter(name(getClass(), name, "exceeded"));
this.memcachedClient = memcachedClient;
this.cacheClient = cacheClient;
this.name = name;
this.bucketSize = bucketSize;
this.leakRatePerMillis = leakRatePerMinute / (60.0 * 1000.0);
@@ -61,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,63 @@
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.DatabaseConfiguration;
import io.dropwizard.db.ManagedDataSource;
import io.dropwizard.db.PooledDataSourceFactory;
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 PooledDataSourceFactory dbConfig = strategy.getDataSourceFactory(configuration);
dbConfig.asSingleConnectionPool();
try (final CloseableLiquibase liquibase = openLiquibase(dbConfig, namespace)) {
run(namespace, liquibase);
} catch (ValidationFailedException e) {
e.printDescriptiveError(System.err);
throw e;
}
}
private CloseableLiquibase openLiquibase(final PooledDataSourceFactory 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

@@ -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,19 @@
package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.Gauge;
import java.io.File;
public class FileDescriptorGauge implements Gauge<Integer> {
@Override
public Integer getValue() {
File file = new File("/proc/self/fd");
if (file.isDirectory() && file.exists()) {
return file.list().length;
}
return 0;
}
}

View File

@@ -1,53 +0,0 @@
/**
* 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.providers;
import com.codahale.metrics.health.HealthCheck;
import net.spy.memcached.MemcachedClient;
import java.security.SecureRandom;
public class MemcacheHealthCheck extends HealthCheck {
private final MemcachedClient client;
public MemcacheHealthCheck(MemcachedClient client) {
this.client = client;
}
@Override
protected Result check() throws Exception {
if (client == null) {
return Result.unhealthy("not configured");
}
int random = SecureRandom.getInstance("SHA1PRNG").nextInt();
int value = SecureRandom.getInstance("SHA1PRNG").nextInt();
this.client.set("HEALTH" + random, 2000, String.valueOf(value));
String result = (String)this.client.get("HEALTH" + random);
if (result == null || Integer.parseInt(result) != value) {
return Result.unhealthy("Fetch failed");
}
this.client.delete("HEALTH" + random);
return Result.healthy();
}
}

View File

@@ -1,54 +0,0 @@
/**
* 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.providers;
import net.spy.memcached.AddrUtil;
import net.spy.memcached.ConnectionFactoryBuilder;
import net.spy.memcached.MemcachedClient;
import net.spy.memcached.auth.AuthDescriptor;
import net.spy.memcached.auth.PlainCallbackHandler;
import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
public class MemcachedClientFactory {
private final MemcachedClient client;
public MemcachedClientFactory(MemcacheConfiguration config) throws IOException {
ConnectionFactoryBuilder builder = new ConnectionFactoryBuilder();
builder.setProtocol(ConnectionFactoryBuilder.Protocol.BINARY);
if (!Util.isEmpty(config.getUser())) {
AuthDescriptor ad = new AuthDescriptor(new String[] { "PLAIN" },
new PlainCallbackHandler(config.getUser(),
config.getPassword()));
builder.setAuthDescriptor(ad);
}
this.client = new MemcachedClient(builder.build(),
AddrUtil.getAddresses(config.getServers()));
}
public MemcachedClient getClient() {
return client;
}
}

View File

@@ -16,9 +16,14 @@
*/
package org.whispersystems.textsecuregcm.providers;
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dispatch.io.RedisPubSubConnectionFactory;
import org.whispersystems.dispatch.redis.PubSubConnection;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
@@ -26,29 +31,40 @@ import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;
public class RedisClientFactory {
public class RedisClientFactory implements RedisPubSubConnectionFactory {
private final Logger logger = LoggerFactory.getLogger(RedisClientFactory.class);
private final String host;
private final int port;
private final JedisPool jedisPool;
public RedisClientFactory(String url) throws URISyntaxException {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setTestOnBorrow(true);
URI redisURI = new URI(url);
String redisHost = redisURI.getHost();
int redisPort = redisURI.getPort();
String redisPassword = null;
URI redisURI = new URI(url);
if (!Util.isEmpty(redisURI.getUserInfo())) {
redisPassword = redisURI.getUserInfo().split(":",2)[1];
}
this.jedisPool = new JedisPool(poolConfig, redisHost, redisPort,
Protocol.DEFAULT_TIMEOUT, redisPassword);
this.host = redisURI.getHost();
this.port = redisURI.getPort();
this.jedisPool = new JedisPool(poolConfig, host, port,
Protocol.DEFAULT_TIMEOUT, null);
}
public JedisPool getRedisClientPool() {
return jedisPool;
}
@Override
public PubSubConnection connect() {
while (true) {
try {
Socket socket = new Socket(host, port);
return new PubSubConnection(socket);
} catch (IOException e) {
logger.warn("Error connecting", e);
Util.sleep(200);
}
}
}
}

View File

@@ -31,9 +31,7 @@ public class RedisHealthCheck extends HealthCheck {
@Override
protected Result check() throws Exception {
Jedis client = clientPool.getResource();
try {
try (Jedis client = clientPool.getResource()) {
client.set("HEALTH", "test");
if (!"test".equals(client.get("HEALTH"))) {
@@ -41,8 +39,6 @@ public class RedisHealthCheck extends HealthCheck {
}
return Result.healthy();
} finally {
clientPool.returnResource(client);
}
}
}

View File

@@ -0,0 +1,220 @@
package org.whispersystems.textsecuregcm.push;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.RatioGauge;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.InvalidProtocolBufferException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dispatch.DispatchChannel;
import org.whispersystems.textsecuregcm.entities.ApnMessage;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.websocket.WebSocketConnectionInfo;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.lifecycle.Managed;
public class ApnFallbackManager implements Managed, Runnable, DispatchChannel {
private static final Logger logger = LoggerFactory.getLogger(ApnFallbackManager.class);
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private static final Meter voipOneSuccess = metricRegistry.meter(name(ApnFallbackManager.class, "voip_one_success"));
private static final Meter voipOneDelivery = metricRegistry.meter(name(ApnFallbackManager.class, "voip_one_failure"));
private static final Histogram voipOneSuccessHistogram = metricRegistry.histogram(name(ApnFallbackManager.class, "voip_one_success_histogram"));
static {
metricRegistry.register(name(ApnFallbackManager.class, "voip_one_success_ratio"), new VoipRatioGauge(voipOneSuccess, voipOneDelivery));
}
private final ApnFallbackTaskQueue taskQueue = new ApnFallbackTaskQueue();
private final PushServiceClient pushServiceClient;
private final PubSubManager pubSubManager;
public ApnFallbackManager(PushServiceClient pushServiceClient, PubSubManager pubSubManager) {
this.pushServiceClient = pushServiceClient;
this.pubSubManager = pubSubManager;
}
public void schedule(final WebsocketAddress address, ApnFallbackTask task) {
voipOneDelivery.mark();
if (taskQueue.put(address, task)) {
pubSubManager.subscribe(new WebSocketConnectionInfo(address), this);
}
}
private void cancel(WebsocketAddress address) {
ApnFallbackTask task = taskQueue.remove(address);
if (task != null) {
pubSubManager.unsubscribe(new WebSocketConnectionInfo(address), this);
voipOneSuccess.mark();
voipOneSuccessHistogram.update(System.currentTimeMillis() - task.getScheduledTime());
}
}
@Override
public void start() throws Exception {
new Thread(this).start();
}
@Override
public void stop() throws Exception {
}
@Override
public void run() {
while (true) {
try {
Entry<WebsocketAddress, ApnFallbackTask> taskEntry = taskQueue.get();
ApnFallbackTask task = taskEntry.getValue();
pubSubManager.unsubscribe(new WebSocketConnectionInfo(taskEntry.getKey()), this);
pushServiceClient.send(new ApnMessage(task.getMessage(), task.getApnId(),
false, ApnMessage.MAX_EXPIRATION));
} catch (Throwable e) {
logger.warn("ApnFallbackThread", e);
}
}
}
@Override
public void onDispatchMessage(String channel, byte[] message) {
try {
PubSubMessage notification = PubSubMessage.parseFrom(message);
if (notification.getType().getNumber() == PubSubMessage.Type.CONNECTED_VALUE) {
WebSocketConnectionInfo address = new WebSocketConnectionInfo(channel);
cancel(address.getWebsocketAddress());
} else {
logger.warn("Got strange pubsub type: " + notification.getType().getNumber());
}
} catch (WebSocketConnectionInfo.FormattingException e) {
logger.warn("Bad formatting?", e);
} catch (InvalidProtocolBufferException e) {
logger.warn("Bad protobuf", e);
}
}
@Override
public void onDispatchSubscribed(String channel) {}
@Override
public void onDispatchUnsubscribed(String channel) {}
public static class ApnFallbackTask {
private final long delay;
private final long scheduledTime;
private final String apnId;
private final ApnMessage message;
public ApnFallbackTask(String apnId, ApnMessage message) {
this(apnId, message, TimeUnit.SECONDS.toMillis(30));
}
@VisibleForTesting
public ApnFallbackTask(String apnId, ApnMessage message, long delay) {
this.scheduledTime = System.currentTimeMillis();
this.delay = delay;
this.apnId = apnId;
this.message = message;
}
public String getApnId() {
return apnId;
}
public ApnMessage getMessage() {
return message;
}
public long getScheduledTime() {
return scheduledTime;
}
public long getExecutionTime() {
return scheduledTime + delay;
}
public long getDelay() {
return delay;
}
}
@VisibleForTesting
public static class ApnFallbackTaskQueue {
private final LinkedHashMap<WebsocketAddress, ApnFallbackTask> tasks = new LinkedHashMap<>();
public Entry<WebsocketAddress, ApnFallbackTask> get() {
while (true) {
long timeDelta;
synchronized (tasks) {
while (tasks.isEmpty()) Util.wait(tasks);
Iterator<Entry<WebsocketAddress, ApnFallbackTask>> iterator = tasks.entrySet().iterator();
Entry<WebsocketAddress, ApnFallbackTask> nextTask = iterator.next();
timeDelta = nextTask.getValue().getExecutionTime() - System.currentTimeMillis();
if (timeDelta <= 0) {
iterator.remove();
return nextTask;
}
}
Util.sleep(timeDelta);
}
}
public boolean put(WebsocketAddress address, ApnFallbackTask task) {
synchronized (tasks) {
ApnFallbackTask previous = tasks.put(address, task);
tasks.notifyAll();
return previous == null;
}
}
public ApnFallbackTask remove(WebsocketAddress address) {
synchronized (tasks) {
return tasks.remove(address);
}
}
}
private static class VoipRatioGauge extends RatioGauge {
private final Meter success;
private final Meter attempts;
private VoipRatioGauge(Meter success, Meter attempts) {
this.success = success;
this.attempts = attempts;
}
@Override
protected Ratio getRatio() {
return Ratio.of(success.getFiveMinuteRate(), attempts.getFiveMinuteRate());
}
}
}

View File

@@ -73,10 +73,17 @@ public class FeedbackHandler implements Managed, Runnable {
if (event.getRegistrationId().equals(device.get().getGcmId())) {
logger.info("GCM Unregister GCM ID matches!");
if (device.get().getPushTimestamp() == 0 ||
event.getTimestamp() > device.get().getPushTimestamp())
event.getTimestamp() > (device.get().getPushTimestamp() + TimeUnit.SECONDS.toMillis(10)))
{
logger.info("GCM Unregister Timestamp matches!");
device.get().setGcmId(null);
if (event.getCanonicalId() != null && !event.getCanonicalId().isEmpty()) {
logger.info("It's a canonical ID update...");
device.get().setGcmId(event.getCanonicalId());
} else {
device.get().setGcmId(null);
device.get().setFetchesMessages(false);
}
accountsManager.update(account.get());
}
}

View File

@@ -16,72 +16,159 @@
*/
package org.whispersystems.textsecuregcm.push;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.SharedMetricRegistries;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ApnMessage;
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.entities.GcmMessage;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager.ApnFallbackTask;
import org.whispersystems.textsecuregcm.push.WebsocketSender.DeliveryStatus;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.BlockingThreadPoolExecutor;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import java.util.concurrent.TimeUnit;
public class PushSender {
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.lifecycle.Managed;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
public class PushSender implements Managed {
private final Logger logger = LoggerFactory.getLogger(PushSender.class);
private static final String APN_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"},\"content-available\":1,\"category\":\"Signal_Message\"}}";
private static final String APN_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"badge\":%d,\"alert\":{\"loc-key\":\"APN_Message\"}}}";
private final PushServiceClient pushServiceClient;
private final WebsocketSender webSocketSender;
private final ApnFallbackManager apnFallbackManager;
private final PushServiceClient pushServiceClient;
private final WebsocketSender webSocketSender;
private final BlockingThreadPoolExecutor executor;
private final int queueSize;
public PushSender(PushServiceClient pushServiceClient, WebsocketSender websocketSender) {
this.pushServiceClient = pushServiceClient;
this.webSocketSender = websocketSender;
public PushSender(ApnFallbackManager apnFallbackManager, PushServiceClient pushServiceClient,
WebsocketSender websocketSender, int queueSize)
{
this.apnFallbackManager = apnFallbackManager;
this.pushServiceClient = pushServiceClient;
this.webSocketSender = websocketSender;
this.queueSize = queueSize;
this.executor = new BlockingThreadPoolExecutor(50, queueSize);
SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME)
.register(name(PushSender.class, "send_queue_depth"),
new Gauge<Integer>() {
@Override
public Integer getValue() {
return executor.getSize();
}
});
}
public void sendMessage(Account account, Device device, OutgoingMessageSignal message)
public void sendMessage(final Account account, final Device device, final Envelope message)
throws NotPushRegisteredException
{
if (device.getGcmId() == null && device.getApnId() == null && !device.getFetchesMessages()) {
throw new NotPushRegisteredException("No delivery possible!");
}
if (queueSize > 0) {
executor.execute(new Runnable() {
@Override
public void run() {
sendSynchronousMessage(account, device, message);
}
});
} else {
sendSynchronousMessage(account, device, message);
}
}
public void sendQueuedNotification(Account account, Device device, int messageQueueDepth)
throws NotPushRegisteredException, TransientPushFailureException
{
if (device.getGcmId() != null) sendGcmNotification(account, device);
else if (device.getApnId() != null) sendApnNotification(account, device, messageQueueDepth);
else if (!device.getFetchesMessages()) throw new NotPushRegisteredException("No notification possible!");
}
public WebsocketSender getWebSocketSender() {
return webSocketSender;
}
private void sendSynchronousMessage(Account account, Device device, Envelope message) {
if (device.getGcmId() != null) sendGcmMessage(account, device, message);
else if (device.getApnId() != null) sendApnMessage(account, device, message);
else if (device.getFetchesMessages()) sendWebSocketMessage(account, device, message);
else throw new NotPushRegisteredException("No delivery possible!");
else throw new AssertionError();
}
private void sendGcmMessage(Account account, Device device, OutgoingMessageSignal message)
throws TransientPushFailureException, NotPushRegisteredException
{
private void sendGcmMessage(Account account, Device device, Envelope message) {
DeliveryStatus deliveryStatus = webSocketSender.sendMessage(account, device, message, WebsocketSender.Type.GCM);
if (!deliveryStatus.isDelivered()) {
sendGcmNotification(account, device);
}
}
private void sendGcmNotification(Account account, Device device) {
try {
String number = account.getNumber();
long deviceId = device.getId();
String registrationId = device.getGcmId();
boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey());
GcmMessage gcmMessage = new GcmMessage(registrationId, number, (int) deviceId,
encryptedMessage.toEncodedString(), isReceipt);
GcmMessage gcmMessage = new GcmMessage(device.getGcmId(), account.getNumber(),
(int)device.getId(), "", false, true);
pushServiceClient.send(gcmMessage);
} catch (CryptoEncodingException e) {
throw new NotPushRegisteredException(e);
} catch (TransientPushFailureException e) {
logger.warn("SILENT PUSH LOSS", e);
}
}
private void sendApnMessage(Account account, Device device, OutgoingMessageSignal outgoingMessage)
throws TransientPushFailureException
{
boolean online = webSocketSender.sendMessage(account, device, outgoingMessage, true);
private void sendApnMessage(Account account, Device device, Envelope outgoingMessage) {
DeliveryStatus deliveryStatus = webSocketSender.sendMessage(account, device, outgoingMessage, WebsocketSender.Type.APN);
if (!online && outgoingMessage.getType() != OutgoingMessageSignal.Type.RECEIPT_VALUE) {
ApnMessage apnMessage = new ApnMessage(device.getApnId(), account.getNumber(),
(int)device.getId(), APN_PAYLOAD);
if (!deliveryStatus.isDelivered() && outgoingMessage.getType() != Envelope.Type.RECEIPT) {
sendApnNotification(account, device, deliveryStatus.getMessageQueueDepth());
}
}
private void sendApnNotification(Account account, Device device, int messageQueueDepth) {
ApnMessage apnMessage;
if (!Util.isEmpty(device.getVoipApnId())) {
apnMessage = new ApnMessage(device.getVoipApnId(), account.getNumber(), (int)device.getId(),
String.format(APN_PAYLOAD, messageQueueDepth),
true, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30));
apnFallbackManager.schedule(new WebsocketAddress(account.getNumber(), device.getId()),
new ApnFallbackTask(device.getApnId(), apnMessage));
} else {
apnMessage = new ApnMessage(device.getApnId(), account.getNumber(), (int)device.getId(),
String.format(APN_PAYLOAD, messageQueueDepth),
false, ApnMessage.MAX_EXPIRATION);
}
try {
pushServiceClient.send(apnMessage);
} catch (TransientPushFailureException e) {
logger.warn("SILENT PUSH LOSS", e);
}
}
private void sendWebSocketMessage(Account account, Device device, OutgoingMessageSignal outgoingMessage)
private void sendWebSocketMessage(Account account, Device device, Envelope outgoingMessage)
{
webSocketSender.sendMessage(account, device, outgoingMessage, false);
webSocketSender.sendMessage(account, device, outgoingMessage, WebsocketSender.Type.WEB);
}
@Override
public void start() throws Exception {
}
@Override
public void stop() throws Exception {
executor.shutdown();
executor.awaitTermination(5, TimeUnit.MINUTES);
}
}

View File

@@ -1,9 +1,5 @@
package org.whispersystems.textsecuregcm.push;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.UniformInterfaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
@@ -13,7 +9,11 @@ import org.whispersystems.textsecuregcm.entities.UnregisteredEvent;
import org.whispersystems.textsecuregcm.entities.UnregisteredEventList;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.List;
@@ -57,16 +57,17 @@ public class PushServiceClient {
private void sendPush(String path, Object entity) throws TransientPushFailureException {
try {
ClientResponse response = client.resource("http://" + host + ":" + port + path)
.header("Authorization", authorization)
.entity(entity, MediaType.APPLICATION_JSON)
.put(ClientResponse.class);
Response response = client.target("http://" + host + ":" + port)
.path(path)
.request()
.header("Authorization", authorization)
.put(Entity.entity(entity, MediaType.APPLICATION_JSON_TYPE));
if (response.getStatus() != 204 && response.getStatus() != 200) {
logger.warn("PushServer response: " + response.getStatus() + " " + response.getStatusInfo().getReasonPhrase());
throw new TransientPushFailureException("Bad response: " + response.getStatus());
}
} catch (UniformInterfaceException | ClientHandlerException e) {
} catch (ProcessingException e) {
logger.warn("Push error: ", e);
throw new TransientPushFailureException(e);
}
@@ -74,12 +75,14 @@ public class PushServiceClient {
private List<UnregisteredEvent> getFeedback(String path) throws IOException {
try {
UnregisteredEventList unregisteredEvents = client.resource("http://" + host + ":" + port + path)
UnregisteredEventList unregisteredEvents = client.target("http://" + host + ":" + port)
.path(path)
.request()
.header("Authorization", authorization)
.get(UnregisteredEventList.class);
return unregisteredEvents.getDevices();
} catch (UniformInterfaceException | ClientHandlerException e) {
} catch (ProcessingException e) {
logger.warn("Request error:", e);
throw new IOException(e);
}

View File

@@ -0,0 +1,87 @@
package org.whispersystems.textsecuregcm.push;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import java.io.IOException;
import java.util.Set;
public class ReceiptSender {
private final PushSender pushSender;
private final FederatedClientManager federatedClientManager;
private final AccountsManager accountManager;
public ReceiptSender(AccountsManager accountManager,
PushSender pushSender,
FederatedClientManager federatedClientManager)
{
this.federatedClientManager = federatedClientManager;
this.accountManager = accountManager;
this.pushSender = pushSender;
}
public void sendReceipt(Account source, String destination,
long messageId, Optional<String> relay)
throws IOException, NoSuchUserException,
NotPushRegisteredException, TransientPushFailureException
{
if (relay.isPresent() && !relay.get().isEmpty()) {
sendRelayedReceipt(source, destination, messageId, relay.get());
} else {
sendDirectReceipt(source, destination, messageId);
}
}
private void sendRelayedReceipt(Account source, String destination, long messageId, String relay)
throws NoSuchUserException, IOException
{
try {
federatedClientManager.getClient(relay)
.sendDeliveryReceipt(source.getNumber(),
source.getAuthenticatedDevice().get().getId(),
destination, messageId);
} catch (NoSuchPeerException e) {
throw new NoSuchUserException(e);
}
}
private void sendDirectReceipt(Account source, String destination, long messageId)
throws NotPushRegisteredException, TransientPushFailureException, NoSuchUserException
{
Account destinationAccount = getDestinationAccount(destination);
Set<Device> destinationDevices = destinationAccount.getDevices();
Envelope.Builder message = Envelope.newBuilder()
.setSource(source.getNumber())
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId())
.setTimestamp(messageId)
.setType(Envelope.Type.RECEIPT);
if (source.getRelay().isPresent()) {
message.setRelay(source.getRelay().get());
}
for (Device destinationDevice : destinationDevices) {
pushSender.sendMessage(destinationAccount, destinationDevice, message.build());
}
}
private Account getDestinationAccount(String destination)
throws NoSuchUserException
{
Optional<Account> account = accountManager.get(destination);
if (!account.isPresent()) {
throw new NoSuchUserException(destination);
}
return account.get();
}
}

View File

@@ -19,40 +19,55 @@ package org.whispersystems.textsecuregcm.push;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.protobuf.ByteString;
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.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import static com.codahale.metrics.MetricRegistry.name;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
public class WebsocketSender {
public static enum Type {
APN,
GCM,
WEB
}
private static final Logger logger = LoggerFactory.getLogger(WebsocketSender.class);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter websocketRequeueMeter = metricRegistry.meter(name(getClass(), "ws_requeue"));
private final Meter websocketOnlineMeter = metricRegistry.meter(name(getClass(), "ws_online" ));
private final Meter websocketOfflineMeter = metricRegistry.meter(name(getClass(), "ws_offline" ));
private final Meter apnOnlineMeter = metricRegistry.meter(name(getClass(), "apn_online" ));
private final Meter apnOfflineMeter = metricRegistry.meter(name(getClass(), "apn_offline"));
private final StoredMessages storedMessages;
private final PubSubManager pubSubManager;
private final Meter gcmOnlineMeter = metricRegistry.meter(name(getClass(), "gcm_online" ));
private final Meter gcmOfflineMeter = metricRegistry.meter(name(getClass(), "gcm_offline"));
public WebsocketSender(StoredMessages storedMessages, PubSubManager pubSubManager) {
this.storedMessages = storedMessages;
this.pubSubManager = pubSubManager;
private final Meter provisioningOnlineMeter = metricRegistry.meter(name(getClass(), "provisioning_online" ));
private final Meter provisioningOfflineMeter = metricRegistry.meter(name(getClass(), "provisioning_offline"));
private final MessagesManager messagesManager;
private final PubSubManager pubSubManager;
public WebsocketSender(MessagesManager messagesManager, PubSubManager pubSubManager) {
this.messagesManager = messagesManager;
this.pubSubManager = pubSubManager;
}
public boolean sendMessage(Account account, Device device, OutgoingMessageSignal message, boolean apn) {
public DeliveryStatus sendMessage(Account account, Device device, Envelope message, Type channel) {
WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
PubSubMessage pubSubMessage = PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.DELIVER)
@@ -60,20 +75,65 @@ public class WebsocketSender {
.build();
if (pubSubManager.publish(address, pubSubMessage)) {
if (apn) apnOnlineMeter.mark();
else websocketOnlineMeter.mark();
if (channel == Type.APN) apnOnlineMeter.mark();
else if (channel == Type.GCM) gcmOnlineMeter.mark();
else websocketOnlineMeter.mark();
return new DeliveryStatus(true, 0);
} else {
if (channel == Type.APN) apnOfflineMeter.mark();
else if (channel == Type.GCM) gcmOfflineMeter.mark();
else websocketOfflineMeter.mark();
int queueDepth = queueMessage(account, device, message);
return new DeliveryStatus(false, queueDepth);
}
}
public int queueMessage(Account account, Device device, Envelope message) {
websocketRequeueMeter.mark();
WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
int queueDepth = messagesManager.insert(account.getNumber(), device.getId(), message);
pubSubManager.publish(address, PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.QUERY_DB)
.build());
return queueDepth;
}
public boolean sendProvisioningMessage(ProvisioningAddress address, byte[] body) {
PubSubMessage pubSubMessage = PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.DELIVER)
.setContent(ByteString.copyFrom(body))
.build();
if (pubSubManager.publish(address, pubSubMessage)) {
provisioningOnlineMeter.mark();
return true;
} else {
if (apn) apnOfflineMeter.mark();
else websocketOfflineMeter.mark();
storedMessages.insert(address, message);
pubSubManager.publish(address, PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.QUERY_DB)
.build());
provisioningOfflineMeter.mark();
return false;
}
}
public static class DeliveryStatus {
private final boolean delivered;
private final int messageQueueDepth;
public DeliveryStatus(boolean delivered, int messageQueueDepth) {
this.delivered = delivered;
this.messageQueueDepth = messageQueueDepth;
}
public boolean isDelivered() {
return delivered;
}
public int getMessageQueueDepth() {
return messageQueueDepth;
}
}
}

View File

@@ -1,91 +0,0 @@
/**
* 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.sms;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.util.Constants;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import static com.codahale.metrics.MetricRegistry.name;
public class NexmoSmsSender {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter smsMeter = metricRegistry.meter(name(getClass(), "sms", "delivered"));
private final Meter voxMeter = metricRegistry.meter(name(getClass(), "vox", "delivered"));
private final Logger logger = LoggerFactory.getLogger(NexmoSmsSender.class);
private static final String NEXMO_SMS_URL =
"https://rest.nexmo.com/sms/json?api_key=%s&api_secret=%s&from=%s&to=%s&text=%s";
private static final String NEXMO_VOX_URL =
"https://rest.nexmo.com/tts/json?api_key=%s&api_secret=%s&to=%s&text=%s";
private final String apiKey;
private final String apiSecret;
private final String number;
public NexmoSmsSender(NexmoConfiguration config) {
this.apiKey = config.getApiKey();
this.apiSecret = config.getApiSecret();
this.number = config.getNumber();
}
public void deliverSmsVerification(String destination, String verificationCode) throws IOException {
URL url = new URL(String.format(NEXMO_SMS_URL, apiKey, apiSecret, number, destination,
URLEncoder.encode(SmsSender.SMS_VERIFICATION_TEXT + verificationCode, "UTF-8")));
URLConnection connection = url.openConnection();
connection.setDoInput(true);
connection.connect();
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
while (reader.readLine() != null) {}
reader.close();
smsMeter.mark();
}
public void deliverVoxVerification(String destination, String message) throws IOException {
URL url = new URL(String.format(NEXMO_VOX_URL, apiKey, apiSecret, destination,
URLEncoder.encode(SmsSender.VOX_VERIFICATION_TEXT + message, "UTF-8")));
URLConnection connection = url.openConnection();
connection.setDoInput(true);
connection.connect();
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
logger.debug(line);
}
reader.close();
voxMeter.mark();
}
}

View File

@@ -26,59 +26,41 @@ import java.io.IOException;
public class SmsSender {
static final String SMS_VERIFICATION_TEXT = "Your TextSecure verification code: ";
static final String VOX_VERIFICATION_TEXT = "Your TextSecure verification code is: ";
static final String SMS_IOS_VERIFICATION_TEXT = "Your Signal verification code: %s\n\nOr tap: sgnl://verify/%s";
static final String SMS_VERIFICATION_TEXT = "Your TextSecure verification code: %s";
static final String VOX_VERIFICATION_TEXT = "Your Signal verification code is: ";
private final Logger logger = LoggerFactory.getLogger(SmsSender.class);
private final TwilioSmsSender twilioSender;
private final Optional<NexmoSmsSender> nexmoSender;
private final boolean isTwilioInternational;
private final TwilioSmsSender twilioSender;
public SmsSender(TwilioSmsSender twilioSender,
Optional<NexmoSmsSender> nexmoSender,
boolean isTwilioInternational)
public SmsSender(TwilioSmsSender twilioSender)
{
this.isTwilioInternational = isTwilioInternational;
this.twilioSender = twilioSender;
this.nexmoSender = nexmoSender;
this.twilioSender = twilioSender;
}
public void deliverSmsVerification(String destination, String verificationCode)
public void deliverSmsVerification(String destination, Optional<String> clientType, String verificationCode)
throws IOException
{
if (!isTwilioDestination(destination) && nexmoSender.isPresent()) {
nexmoSender.get().deliverSmsVerification(destination, verificationCode);
} else {
try {
twilioSender.deliverSmsVerification(destination, verificationCode);
} catch (TwilioRestException e) {
logger.info("Twilio SMS Fallback", e);
if (nexmoSender.isPresent()) {
nexmoSender.get().deliverSmsVerification(destination, verificationCode);
}
}
// Fix up mexico numbers to 'mobile' format just for SMS delivery.
if (destination.startsWith("+42") && !destination.startsWith("+421")) {
destination = "+421" + destination.substring(3);
}
try {
twilioSender.deliverSmsVerification(destination, clientType, verificationCode);
} catch (TwilioRestException e) {
logger.info("Twilio SMS Failed: " + e.getErrorMessage());
}
}
public void deliverVoxVerification(String destination, String verificationCode)
throws IOException
{
if (!isTwilioDestination(destination) && nexmoSender.isPresent()) {
nexmoSender.get().deliverVoxVerification(destination, verificationCode);
} else {
try {
twilioSender.deliverVoxVerification(destination, verificationCode);
} catch (TwilioRestException e) {
logger.info("Twilio Vox Fallback", e);
if (nexmoSender.isPresent()) {
nexmoSender.get().deliverVoxVerification(destination, verificationCode);
}
}
try {
twilioSender.deliverVoxVerification(destination, verificationCode);
} catch (TwilioRestException e) {
logger.info("Twilio Vox Failed: " + e.getErrorMessage());
}
}
private boolean isTwilioDestination(String number) {
return isTwilioInternational || number.length() == 12 && number.startsWith("+1");
}
}

View File

@@ -19,6 +19,7 @@ package org.whispersystems.textsecuregcm.sms;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional;
import com.twilio.sdk.TwilioRestClient;
import com.twilio.sdk.TwilioRestException;
import com.twilio.sdk.resource.factory.CallFactory;
@@ -29,10 +30,12 @@ import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.util.Constants;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import static com.codahale.metrics.MetricRegistry.name;
@@ -40,35 +43,42 @@ public class TwilioSmsSender {
public static final String SAY_TWIML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<Response>\n" +
" <Say voice=\"woman\" language=\"en\">" + SmsSender.VOX_VERIFICATION_TEXT + "%s</Say>\n" +
" <Say voice=\"woman\" language=\"en\" loop=\"3\">" + SmsSender.VOX_VERIFICATION_TEXT + "%s.</Say>\n" +
"</Response>";
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter smsMeter = metricRegistry.meter(name(getClass(), "sms", "delivered"));
private final Meter voxMeter = metricRegistry.meter(name(getClass(), "vox", "delivered"));
private final String accountId;
private final String accountToken;
private final String number;
private final String localDomain;
private final String accountId;
private final String accountToken;
private final ArrayList<String> numbers;
private final String localDomain;
private final Random random;
public TwilioSmsSender(TwilioConfiguration config) {
this.accountId = config.getAccountId();
this.accountToken = config.getAccountToken();
this.number = config.getNumber();
this.numbers = new ArrayList<>(config.getNumbers());
this.localDomain = config.getLocalDomain();
this.random = new Random(System.currentTimeMillis());
}
public void deliverSmsVerification(String destination, String verificationCode)
public void deliverSmsVerification(String destination, Optional<String> clientType, String verificationCode)
throws IOException, TwilioRestException
{
TwilioRestClient client = new TwilioRestClient(accountId, accountToken);
MessageFactory messageFactory = client.getAccount().getMessageFactory();
List<NameValuePair> messageParams = new LinkedList<>();
messageParams.add(new BasicNameValuePair("To", destination));
messageParams.add(new BasicNameValuePair("From", number));
messageParams.add(new BasicNameValuePair("Body", SmsSender.SMS_VERIFICATION_TEXT + verificationCode));
messageParams.add(new BasicNameValuePair("From", getRandom(random, numbers)));
if ("ios".equals(clientType.orNull())) {
messageParams.add(new BasicNameValuePair("Body", String.format(SmsSender.SMS_IOS_VERIFICATION_TEXT, verificationCode, verificationCode)));
} else {
messageParams.add(new BasicNameValuePair("Body", String.format(SmsSender.SMS_VERIFICATION_TEXT, verificationCode)));
}
try {
messageFactory.create(messageParams);
} catch (RuntimeException damnYouTwilio) {
@@ -85,7 +95,7 @@ public class TwilioSmsSender {
CallFactory callFactory = client.getAccount().getCallFactory();
Map<String, String> callParams = new HashMap<>();
callParams.put("To", destination);
callParams.put("From", number);
callParams.put("From", getRandom(random, numbers));
callParams.put("Url", "https://" + localDomain + "/v1/accounts/voice/twiml/" + verificationCode);
try {
@@ -96,4 +106,9 @@ public class TwilioSmsSender {
voxMeter.mark();
}
private String getRandom(Random random, ArrayList<String> elements) {
return elements.get(random.nextInt(elements.size()));
}
}

View File

@@ -22,8 +22,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import java.util.LinkedList;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
public class Account {
@@ -33,10 +33,7 @@ public class Account {
private String number;
@JsonProperty
private boolean supportsSms;
@JsonProperty
private List<Device> devices = new LinkedList<>();
private Set<Device> devices = new HashSet<>();
@JsonProperty
private String identityKey;
@@ -47,10 +44,9 @@ public class Account {
public Account() {}
@VisibleForTesting
public Account(String number, boolean supportsSms, List<Device> devices) {
this.number = number;
this.supportsSms = supportsSms;
this.devices = devices;
public Account(String number, Set<Device> devices) {
this.number = number;
this.devices = devices;
}
public Optional<Device> getAuthenticatedDevice() {
@@ -69,23 +65,16 @@ public class Account {
return number;
}
public boolean getSupportsSms() {
return supportsSms;
}
public void setSupportsSms(boolean supportsSms) {
this.supportsSms = supportsSms;
}
public void addDevice(Device device) {
this.devices.remove(device);
this.devices.add(device);
}
public void setDevices(List<Device> devices) {
this.devices = devices;
public void removeDevice(long deviceId) {
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, false, "NA"));
}
public List<Device> getDevices() {
public Set<Device> getDevices() {
return devices;
}
@@ -103,6 +92,16 @@ public class Account {
return Optional.absent();
}
public boolean isVoiceSupported() {
for (Device device : devices) {
if (device.isActive() && device.isVoiceSupported()) {
return true;
}
}
return false;
}
public boolean isActive() {
return
getMasterDevice().isPresent() &&
@@ -113,7 +112,9 @@ public class Account {
long highestDevice = Device.MASTER_ID;
for (Device device : devices) {
if (device.getId() > highestDevice) {
if (!device.isActive()) {
return device.getId();
} else if (device.getId() > highestDevice) {
highestDevice = device.getId();
}
}
@@ -121,6 +122,16 @@ public class Account {
return highestDevice + 1;
}
public int getActiveDeviceCount() {
int count = 0;
for (Device device : devices) {
if (device.isActive()) count++;
}
return count;
}
public boolean isRateLimited() {
return true;
}

View File

@@ -16,8 +16,6 @@
*/
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.skife.jdbi.v2.SQLStatement;

View File

@@ -17,12 +17,9 @@
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import net.spy.memcached.MemcachedClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact;
@@ -33,23 +30,26 @@ import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class AccountsManager {
private final Logger logger = LoggerFactory.getLogger(AccountsManager.class);
private final Accounts accounts;
private final MemcachedClient memcachedClient;
private final JedisPool cacheClient;
private final DirectoryManager directory;
private final ObjectMapper mapper;
public AccountsManager(Accounts accounts,
DirectoryManager directory,
MemcachedClient memcachedClient)
JedisPool cacheClient)
{
this.accounts = accounts;
this.directory = directory;
this.memcachedClient = memcachedClient;
this.mapper = SystemMapper.getMapper();
this.accounts = accounts;
this.directory = directory;
this.cacheClient = cacheClient;
this.mapper = SystemMapper.getMapper();
}
public long getCount() {
@@ -100,7 +100,7 @@ public class AccountsManager {
private void updateDirectory(Account account) {
if (account.isActive()) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported());
directory.add(clientContact);
} else {
directory.remove(account.getNumber());
@@ -112,25 +112,19 @@ public class AccountsManager {
}
private void memcacheSet(String number, Account account) {
if (memcachedClient != null) {
try {
String json = mapper.writeValueAsString(account);
memcachedClient.set(getKey(number), 0, json);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
try (Jedis jedis = cacheClient.getResource()) {
jedis.set(getKey(number), mapper.writeValueAsString(account));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
}
private Optional<Account> memcacheGet(String number) {
if (memcachedClient == null) return Optional.absent();
try {
String json = (String)memcachedClient.get(getKey(number));
try (Jedis jedis = cacheClient.getResource()) {
String json = jedis.get(getKey(number));
if (json != null) return Optional.of(mapper.readValue(json, Account.class));
else return Optional.absent();
} catch (IOException e) {
logger.warn("AccountsManager", "Deserialization error", e);
return Optional.absent();

View File

@@ -22,6 +22,8 @@ import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.concurrent.TimeUnit;
public class Device {
public static final long MASTER_ID = 1;
@@ -29,6 +31,9 @@ public class Device {
@JsonProperty
private long id;
@JsonProperty
private String name;
@JsonProperty
private String authToken;
@@ -44,6 +49,9 @@ public class Device {
@JsonProperty
private String apnId;
@JsonProperty
private String voipApnId;
@JsonProperty
private long pushTimestamp;
@@ -56,22 +64,42 @@ public class Device {
@JsonProperty
private SignedPreKey signedPreKey;
@JsonProperty
private long lastSeen;
@JsonProperty
private long created;
@JsonProperty
private boolean voice;
@JsonProperty
private String userAgent;
public Device() {}
public Device(long id, String authToken, String salt,
public Device(long id, String name, String authToken, String salt,
String signalingKey, String gcmId, String apnId,
boolean fetchesMessages, int registrationId,
SignedPreKey signedPreKey)
String voipApnId, boolean fetchesMessages,
int registrationId, SignedPreKey signedPreKey,
long lastSeen, long created, boolean voice,
String userAgent)
{
this.id = id;
this.name = name;
this.authToken = authToken;
this.salt = salt;
this.signalingKey = signalingKey;
this.gcmId = gcmId;
this.apnId = apnId;
this.voipApnId = voipApnId;
this.fetchesMessages = fetchesMessages;
this.registrationId = registrationId;
this.signedPreKey = signedPreKey;
this.lastSeen = lastSeen;
this.created = created;
this.voice = voice;
this.userAgent = userAgent;
}
public String getApnId() {
@@ -86,6 +114,30 @@ public class Device {
}
}
public String getVoipApnId() {
return voipApnId;
}
public void setVoipApnId(String voipApnId) {
this.voipApnId = voipApnId;
}
public void setLastSeen(long lastSeen) {
this.lastSeen = lastSeen;
}
public long getLastSeen() {
return lastSeen;
}
public void setCreated(long created) {
this.created = created;
}
public long getCreated() {
return this.created;
}
public String getGcmId() {
return gcmId;
}
@@ -106,6 +158,22 @@ public class Device {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isVoiceSupported() {
return voice;
}
public void setVoiceSupported(boolean voice) {
this.voice = voice;
}
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
this.authToken = credentials.getHashedAuthenticationToken();
this.salt = credentials.getSalt();
@@ -124,7 +192,10 @@ public class Device {
}
public boolean isActive() {
return fetchesMessages || !Util.isEmpty(getApnId()) || !Util.isEmpty(getGcmId());
boolean hasChannel = fetchesMessages || !Util.isEmpty(getApnId()) || !Util.isEmpty(getGcmId());
return (id == MASTER_ID && hasChannel) ||
(id != MASTER_ID && hasChannel && signedPreKey != null && lastSeen > (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30)));
}
public boolean getFetchesMessages() {
@@ -158,4 +229,25 @@ public class Device {
public long getPushTimestamp() {
return pushTimestamp;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
public String getUserAgent() {
return this.userAgent;
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof Device)) return false;
Device that = (Device)other;
return this.id == that.id;
}
@Override
public int hashCode() {
return (int)this.id;
}
}

View File

@@ -61,9 +61,9 @@ public class DirectoryManager {
}
public void remove(byte[] token) {
Jedis jedis = redisPool.getResource();
jedis.hdel(DIRECTORY_KEY, token);
redisPool.returnResource(jedis);
try (Jedis jedis = redisPool.getResource()) {
jedis.hdel(DIRECTORY_KEY, token);
}
}
public void remove(BatchOperationHandle handle, byte[] token) {
@@ -72,7 +72,7 @@ public class DirectoryManager {
}
public void add(ClientContact contact) {
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms());
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isVoice());
try (Jedis jedis = redisPool.getResource()) {
jedis.hset(DIRECTORY_KEY, contact.getToken(), objectMapper.writeValueAsBytes(tokenValue));
@@ -84,7 +84,7 @@ public class DirectoryManager {
public void add(BatchOperationHandle handle, ClientContact contact) {
try {
Pipeline pipeline = handle.pipeline;
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms());
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isVoice());
pipeline.hset(DIRECTORY_KEY, contact.getToken(), objectMapper.writeValueAsBytes(tokenValue));
} catch (JsonProcessingException e) {
@@ -106,7 +106,7 @@ public class DirectoryManager {
}
TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms));
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.voice));
} catch (IOException e) {
logger.warn("JSON Error", e);
return Optional.absent();
@@ -133,7 +133,7 @@ public class DirectoryManager {
try {
if (pair.second().get() != null) {
TokenValue tokenValue = objectMapper.readValue(pair.second().get(), TokenValue.class);
ClientContact clientContact = new ClientContact(pair.first(), tokenValue.relay, tokenValue.supportsSms);
ClientContact clientContact = new ClientContact(pair.first(), tokenValue.relay, tokenValue.voice);
results.add(clientContact);
}
@@ -175,14 +175,14 @@ public class DirectoryManager {
@JsonProperty(value = "r")
private String relay;
@JsonProperty(value = "s")
private boolean supportsSms;
@JsonProperty(value = "v")
private boolean voice;
public TokenValue() {}
public TokenValue(String relay, boolean supportsSms) {
this.relay = relay;
this.supportsSms = supportsSms;
public TokenValue(String relay, boolean voice) {
this.relay = relay;
this.voice = voice;
}
}
@@ -205,7 +205,7 @@ public class DirectoryManager {
}
TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms));
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.voice));
}
}

View File

@@ -0,0 +1,127 @@
package org.whispersystems.textsecuregcm.storage;
import org.skife.jdbi.v2.SQLStatement;
import org.skife.jdbi.v2.StatementContext;
import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.Binder;
import org.skife.jdbi.v2.sqlobject.BinderFactory;
import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
import org.skife.jdbi.v2.tweak.ResultSetMapper;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
public abstract class Messages {
public static final int RESULT_SET_CHUNK_SIZE = 100;
private static final String ID = "id";
private static final String TYPE = "type";
private static final String RELAY = "relay";
private static final String TIMESTAMP = "timestamp";
private static final String SOURCE = "source";
private static final String SOURCE_DEVICE = "source_device";
private static final String DESTINATION = "destination";
private static final String DESTINATION_DEVICE = "destination_device";
private static final String MESSAGE = "message";
private static final String CONTENT = "content";
@SqlQuery("INSERT INTO messages (" + TYPE + ", " + RELAY + ", " + TIMESTAMP + ", " + SOURCE + ", " + SOURCE_DEVICE + ", " + DESTINATION + ", " + DESTINATION_DEVICE + ", " + MESSAGE + ", " + CONTENT + ") " +
"VALUES (:type, :relay, :timestamp, :source, :source_device, :destination, :destination_device, :message, :content) " +
"RETURNING (SELECT COUNT(id) FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device AND " + TYPE + " != " + Envelope.Type.RECEIPT_VALUE + ")")
abstract int store(@MessageBinder Envelope message,
@Bind("destination") String destination,
@Bind("destination_device") long destinationDevice);
@Mapper(MessageMapper.class)
@SqlQuery("SELECT * FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device ORDER BY " + TIMESTAMP + " ASC LIMIT " + RESULT_SET_CHUNK_SIZE)
abstract List<OutgoingMessageEntity> load(@Bind("destination") String destination,
@Bind("destination_device") long destinationDevice);
@Mapper(MessageMapper.class)
@SqlQuery("DELETE FROM messages WHERE " + ID + " IN (SELECT " + ID + " FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device AND " + SOURCE + " = :source AND " + TIMESTAMP + " = :timestamp ORDER BY " + ID + " LIMIT 1) RETURNING *")
abstract OutgoingMessageEntity remove(@Bind("destination") String destination,
@Bind("destination_device") long destinationDevice,
@Bind("source") String source,
@Bind("timestamp") long timestamp);
@Mapper(MessageMapper.class)
@SqlUpdate("DELETE FROM messages WHERE " + ID + " = :id AND " + DESTINATION + " = :destination")
abstract void remove(@Bind("destination") String destination, @Bind("id") long id);
@SqlUpdate("DELETE FROM messages WHERE " + DESTINATION + " = :destination")
abstract void clear(@Bind("destination") String destination);
@SqlUpdate("DELETE FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device")
abstract void clear(@Bind("destination") String destination, @Bind("destination_device") long destinationDevice);
@SqlUpdate("DELETE FROM messages WHERE " + TIMESTAMP + " < :timestamp")
public abstract void removeOld(@Bind("timestamp") long timestamp);
@SqlUpdate("VACUUM messages")
public abstract void vacuum();
public static class MessageMapper implements ResultSetMapper<OutgoingMessageEntity> {
@Override
public OutgoingMessageEntity map(int i, ResultSet resultSet, StatementContext statementContext)
throws SQLException
{
int type = resultSet.getInt(TYPE);
byte[] legacyMessage = resultSet.getBytes(MESSAGE);
if (type == Envelope.Type.RECEIPT_VALUE && legacyMessage == null) {
/// XXX - REMOVE AFTER 10/01/15
legacyMessage = new byte[0];
}
return new OutgoingMessageEntity(resultSet.getLong(ID),
type,
resultSet.getString(RELAY),
resultSet.getLong(TIMESTAMP),
resultSet.getString(SOURCE),
resultSet.getInt(SOURCE_DEVICE),
legacyMessage,
resultSet.getBytes(CONTENT));
}
}
@BindingAnnotation(MessageBinder.AccountBinderFactory.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface MessageBinder {
public static class AccountBinderFactory implements BinderFactory {
@Override
public Binder build(Annotation annotation) {
return new Binder<MessageBinder, Envelope>() {
@Override
public void bind(SQLStatement<?> sql,
MessageBinder accountBinder,
Envelope message)
{
sql.bind(TYPE, message.getType().getNumber());
sql.bind(RELAY, message.getRelay());
sql.bind(TIMESTAMP, message.getTimestamp());
sql.bind(SOURCE, message.getSource());
sql.bind(SOURCE_DEVICE, message.getSourceDevice());
sql.bind(MESSAGE, message.hasLegacyMessage() ? message.getLegacyMessage().toByteArray() : null);
sql.bind(CONTENT, message.hasContent() ? message.getContent().toByteArray() : null);
}
};
}
}
}
}

View File

@@ -0,0 +1,44 @@
package org.whispersystems.textsecuregcm.storage;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
import java.util.List;
public class MessagesManager {
private final Messages messages;
public MessagesManager(Messages messages) {
this.messages = messages;
}
public int insert(String destination, long destinationDevice, Envelope message) {
return this.messages.store(message, destination, destinationDevice) + 1;
}
public OutgoingMessageEntityList getMessagesForDevice(String destination, long destinationDevice) {
List<OutgoingMessageEntity> messages = this.messages.load(destination, destinationDevice);
return new OutgoingMessageEntityList(messages, messages.size() >= Messages.RESULT_SET_CHUNK_SIZE);
}
public void clear(String destination) {
this.messages.clear(destination);
}
public void clear(String destination, long deviceId) {
this.messages.clear(destination, deviceId);
}
public Optional<OutgoingMessageEntity> delete(String destination, long destinationDevice, String source, long timestamp)
{
return Optional.fromNullable(this.messages.remove(destination, destinationDevice, source, timestamp));
}
public void delete(String destination, long id) {
this.messages.remove(destination, id);
}
}

View File

@@ -17,20 +17,21 @@
package org.whispersystems.textsecuregcm.storage;
import com.google.common.base.Optional;
import net.spy.memcached.MemcachedClient;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class PendingAccountsManager {
private static final String MEMCACHE_PREFIX = "pending_account";
private static final String CACHE_PREFIX = "pending_account::";
private final PendingAccounts pendingAccounts;
private final MemcachedClient memcachedClient;
private final JedisPool cacheClient;
public PendingAccountsManager(PendingAccounts pendingAccounts,
MemcachedClient memcachedClient)
public PendingAccountsManager(PendingAccounts pendingAccounts, JedisPool cacheClient)
{
this.pendingAccounts = pendingAccounts;
this.memcachedClient = memcachedClient;
this.cacheClient = cacheClient;
}
public void store(String number, String code) {
@@ -58,22 +59,20 @@ public class PendingAccountsManager {
}
private void memcacheSet(String number, String code) {
if (memcachedClient != null) {
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
try (Jedis jedis = cacheClient.getResource()) {
jedis.set(CACHE_PREFIX + number, code);
}
}
private Optional<String> memcacheGet(String number) {
if (memcachedClient != null) {
return Optional.fromNullable((String)memcachedClient.get(MEMCACHE_PREFIX + number));
} else {
return Optional.absent();
try (Jedis jedis = cacheClient.getResource()) {
return Optional.fromNullable(jedis.get(CACHE_PREFIX + number));
}
}
private void memcacheDelete(String number) {
if (memcachedClient != null) {
memcachedClient.delete(MEMCACHE_PREFIX + number);
try (Jedis jedis = cacheClient.getResource()) {
jedis.del(CACHE_PREFIX + number);
}
}
}

View File

@@ -17,20 +17,22 @@
package org.whispersystems.textsecuregcm.storage;
import com.google.common.base.Optional;
import net.spy.memcached.MemcachedClient;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class PendingDevicesManager {
private static final String MEMCACHE_PREFIX = "pending_devices";
private static final String CACHE_PREFIX = "pending_devices::";
private final PendingDevices pendingDevices;
private final MemcachedClient memcachedClient;
private final JedisPool cacheClient;
public PendingDevicesManager(PendingDevices pendingDevices,
MemcachedClient memcachedClient)
JedisPool cacheClient)
{
this.pendingDevices = pendingDevices;
this.memcachedClient = memcachedClient;
this.pendingDevices = pendingDevices;
this.cacheClient = cacheClient;
}
public void store(String number, String code) {
@@ -58,22 +60,20 @@ public class PendingDevicesManager {
}
private void memcacheSet(String number, String code) {
if (memcachedClient != null) {
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
try (Jedis jedis = cacheClient.getResource()) {
jedis.set(CACHE_PREFIX + number, code);
}
}
private Optional<String> memcacheGet(String number) {
if (memcachedClient != null) {
return Optional.fromNullable((String)memcachedClient.get(MEMCACHE_PREFIX + number));
} else {
return Optional.absent();
try (Jedis jedis = cacheClient.getResource()) {
return Optional.fromNullable(jedis.get(CACHE_PREFIX + number));
}
}
private void memcacheDelete(String number) {
if (memcachedClient != null) {
memcachedClient.delete(MEMCACHE_PREFIX + number);
try (Jedis jedis = cacheClient.getResource()) {
jedis.del(CACHE_PREFIX + number);
}
}

View File

@@ -0,0 +1,5 @@
package org.whispersystems.textsecuregcm.storage;
public interface PubSubAddress {
public String serialize();
}

View File

@@ -1,9 +0,0 @@
package org.whispersystems.textsecuregcm.storage;
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
public interface PubSubListener {
public void onPubSubMessage(PubSubMessage outgoingMessage);
}

View File

@@ -1,129 +1,90 @@
package org.whispersystems.textsecuregcm.storage;
import com.google.protobuf.InvalidProtocolBufferException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import org.whispersystems.dispatch.DispatchChannel;
import org.whispersystems.dispatch.DispatchManager;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import io.dropwizard.lifecycle.Managed;
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
import redis.clients.jedis.BinaryJedisPubSub;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class PubSubManager {
public class PubSubManager implements Managed {
private static final byte[] KEEPALIVE_CHANNEL = "KEEPALIVE".getBytes();
private static final String KEEPALIVE_CHANNEL = "KEEPALIVE";
private final Logger logger = LoggerFactory.getLogger(PubSubManager.class);
private final SubscriptionListener baseListener = new SubscriptionListener();
private final Map<String, PubSubListener> listeners = new HashMap<>();
private final Logger logger = LoggerFactory.getLogger(PubSubManager.class);
private final DispatchManager dispatchManager;
private final JedisPool jedisPool;
private final JedisPool jedisPool;
private boolean subscribed = false;
public PubSubManager(JedisPool jedisPool) {
this.jedisPool = jedisPool;
initializePubSubWorker();
waitForSubscription();
public PubSubManager(JedisPool jedisPool, DispatchManager dispatchManager) {
this.dispatchManager = dispatchManager;
this.jedisPool = jedisPool;
}
public synchronized void subscribe(WebsocketAddress address, PubSubListener listener) {
String serializedAddress = address.serialize();
@Override
public void start() throws Exception {
this.dispatchManager.start();
listeners.put(serializedAddress, listener);
baseListener.subscribe(serializedAddress.getBytes());
}
KeepaliveDispatchChannel keepaliveDispatchChannel = new KeepaliveDispatchChannel();
this.dispatchManager.subscribe(KEEPALIVE_CHANNEL, keepaliveDispatchChannel);
public synchronized void unsubscribe(WebsocketAddress address, PubSubListener listener) {
String serializedAddress = address.serialize();
if (listeners.get(serializedAddress) == listener) {
listeners.remove(serializedAddress);
baseListener.unsubscribe(serializedAddress.getBytes());
synchronized (this) {
while (!subscribed) wait(0);
}
new KeepaliveSender().start();
}
public synchronized boolean publish(WebsocketAddress address, PubSubMessage message) {
@Override
public void stop() throws Exception {
dispatchManager.shutdown();
}
public void subscribe(PubSubAddress address, DispatchChannel channel) {
dispatchManager.subscribe(address.serialize(), channel);
}
public void unsubscribe(PubSubAddress address, DispatchChannel dispatchChannel) {
dispatchManager.unsubscribe(address.serialize(), dispatchChannel);
}
public boolean hasLocalSubscription(PubSubAddress address) {
return dispatchManager.hasSubscription(address.serialize());
}
public boolean publish(PubSubAddress address, PubSubMessage message) {
return publish(address.serialize().getBytes(), message);
}
private synchronized boolean publish(byte[] channel, PubSubMessage message) {
private boolean publish(byte[] channel, PubSubMessage message) {
try (Jedis jedis = jedisPool.getResource()) {
return jedis.publish(channel, message.toByteArray()) != 0;
long result = jedis.publish(channel, message.toByteArray());
if (result < 0) {
logger.warn("**** Jedis publish result < 0");
}
return result > 0;
}
}
private synchronized void waitForSubscription() {
try {
while (!subscribed) {
wait();
}
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
private void initializePubSubWorker() {
new Thread("PubSubListener") {
@Override
public void run() {
for (;;) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.subscribe(baseListener, KEEPALIVE_CHANNEL);
logger.warn("**** Unsubscribed from holding channel!!! ******");
}
}
}
}.start();
new Thread("PubSubKeepAlive") {
@Override
public void run() {
for (;;) {
try {
Thread.sleep(20000);
publish(KEEPALIVE_CHANNEL, PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.KEEPALIVE)
.build());
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
}
}.start();
}
private class SubscriptionListener extends BinaryJedisPubSub {
private class KeepaliveDispatchChannel implements DispatchChannel {
@Override
public void onMessage(byte[] channel, byte[] message) {
try {
PubSubListener listener;
synchronized (PubSubManager.this) {
listener = listeners.get(new String(channel));
}
if (listener != null) {
listener.onPubSubMessage(PubSubMessage.parseFrom(message));
}
} catch (InvalidProtocolBufferException e) {
logger.warn("Error parsing PubSub protobuf", e);
}
public void onDispatchMessage(String channel, byte[] message) {
// Good
}
@Override
public void onPMessage(byte[] s, byte[] s2, byte[] s3) {
logger.warn("Received PMessage!");
}
@Override
public void onSubscribe(byte[] channel, int count) {
if (Arrays.equals(KEEPALIVE_CHANNEL, channel)) {
public void onDispatchSubscribed(String channel) {
if (KEEPALIVE_CHANNEL.equals(channel)) {
synchronized (PubSubManager.this) {
subscribed = true;
PubSubManager.this.notifyAll();
@@ -132,12 +93,24 @@ public class PubSubManager {
}
@Override
public void onUnsubscribe(byte[] s, int i) {}
public void onDispatchUnsubscribed(String channel) {
logger.warn("***** KEEPALIVE CHANNEL UNSUBSCRIBED *****");
}
}
private class KeepaliveSender extends Thread {
@Override
public void onPUnsubscribe(byte[] s, int i) {}
@Override
public void onPSubscribe(byte[] s, int i) {}
public void run() {
while (true) {
try {
Thread.sleep(20000);
publish(KEEPALIVE_CHANNEL.getBytes(), PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.KEEPALIVE)
.build());
} catch (Throwable e) {
logger.warn("***** KEEPALIVE EXCEPTION ******", e);
}
}
}
}
}

View File

@@ -158,6 +158,14 @@ public final class PubSubProtos {
* <code>KEEPALIVE = 3;</code>
*/
KEEPALIVE(3, 3),
/**
* <code>CLOSE = 4;</code>
*/
CLOSE(4, 4),
/**
* <code>CONNECTED = 5;</code>
*/
CONNECTED(5, 5),
;
/**
@@ -176,6 +184,14 @@ public final class PubSubProtos {
* <code>KEEPALIVE = 3;</code>
*/
public static final int KEEPALIVE_VALUE = 3;
/**
* <code>CLOSE = 4;</code>
*/
public static final int CLOSE_VALUE = 4;
/**
* <code>CONNECTED = 5;</code>
*/
public static final int CONNECTED_VALUE = 5;
public final int getNumber() { return value; }
@@ -186,6 +202,8 @@ public final class PubSubProtos {
case 1: return QUERY_DB;
case 2: return DELIVER;
case 3: return KEEPALIVE;
case 4: return CLOSE;
case 5: return CONNECTED;
default: return null;
}
}
@@ -611,12 +629,13 @@ public final class PubSubProtos {
descriptor;
static {
java.lang.String[] descriptorData = {
"\n\023PubSubMessage.proto\022\ntextsecure\"\215\001\n\rPu" +
"\n\023PubSubMessage.proto\022\ntextsecure\"\247\001\n\rPu" +
"bSubMessage\022,\n\004type\030\001 \001(\0162\036.textsecure.P" +
"ubSubMessage.Type\022\017\n\007content\030\002 \001(\014\"=\n\004Ty" +
"ubSubMessage.Type\022\017\n\007content\030\002 \001(\014\"W\n\004Ty" +
"pe\022\013\n\007UNKNOWN\020\000\022\014\n\010QUERY_DB\020\001\022\013\n\007DELIVER" +
"\020\002\022\r\n\tKEEPALIVE\020\003B8\n(org.whispersystems." +
"textsecuregcm.storageB\014PubSubProtos"
"\020\002\022\r\n\tKEEPALIVE\020\003\022\t\n\005CLOSE\020\004\022\r\n\tCONNECTE" +
"D\020\005B8\n(org.whispersystems.textsecuregcm." +
"storageB\014PubSubProtos"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {

View File

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

View File

@@ -1,107 +0,0 @@
/**
* Copyright (C) 2014 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.storage;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.protobuf.InvalidProtocolBufferException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class StoredMessages {
private static final Logger logger = LoggerFactory.getLogger(StoredMessages.class);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Histogram queueSizeHistogram = metricRegistry.histogram(name(getClass(), "queue_size"));
private static final String QUEUE_PREFIX = "msgs";
private final JedisPool jedisPool;
public StoredMessages(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
public void clear(WebsocketAddress address) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.del(getKey(address));
}
}
public void insert(WebsocketAddress address, OutgoingMessageSignal message) {
try (Jedis jedis = jedisPool.getResource()) {
byte[] queue = getKey(address);
StoredMessage storedMessage = StoredMessage.newBuilder()
.setType(StoredMessage.Type.MESSAGE)
.setContent(message.toByteString())
.build();
long queueSize = jedis.lpush(queue, storedMessage.toByteArray());
queueSizeHistogram.update(queueSize);
jedis.expireAt(queue, (System.currentTimeMillis() / 1000) + TimeUnit.DAYS.toSeconds(30));
if (queueSize > 1000) {
jedis.ltrim(getKey(address), 0, 999);
}
}
}
public List<OutgoingMessageSignal> getMessagesForDevice(WebsocketAddress address) {
List<OutgoingMessageSignal> messages = new LinkedList<>();
try (Jedis jedis = jedisPool.getResource()) {
byte[] message;
while ((message = jedis.rpop(getKey(address))) != null) {
try {
StoredMessage storedMessage = StoredMessage.parseFrom(message);
if (storedMessage.getType().getNumber() == StoredMessage.Type.MESSAGE_VALUE) {
messages.add(OutgoingMessageSignal.parseFrom(storedMessage.getContent()));
} else {
logger.warn("Unkown stored message type: " + storedMessage.getType().getNumber());
}
} catch (InvalidProtocolBufferException e) {
logger.warn("Error parsing protobuf", e);
}
}
return messages;
}
}
private byte[] getKey(WebsocketAddress address) {
return (QUEUE_PREFIX + ":" + address.serialize()).getBytes();
}
}

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