Squashed History

This commit is contained in:
Moxie Marlinspike 2013-12-08 23:11:09 -08:00
commit 4ad0dad3d9
103 changed files with 9737 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
target
local.properties
.idea
*.iml
run.sh
*~
local.yml
config/production.yml
config/federated.yml

Binary file not shown.

Binary file not shown.

2
Procfile Normal file
View File

@ -0,0 +1,2 @@
web: java $JAVA_OPTS -Ddw.http.port=$PORT -Ddw.http.adminPort=$PORT -Ddw.federation.name=$FEDERATION_NAME -Ddw.federation.herokuPeers="$FEDERATED_PEERS" -Ddw.twilio.accountId=$TWILIO_ACCOUNT_SID -Ddw.twilio.accountToken=$TWILIO_ACCOUNT_TOKEN -Ddw.twilio.number=$TWILIO_NUMBER -Ddw.nexmo.apiKey=$NEXMO_KEY -Ddw.nexmo.apiSecret=$NEXMO_SECRET -Ddw.nexmo.number=$NEXMO_NUMBER -Ddw.gcm.apiKey=$GCM_KEY -Ddw.apn.certificate="$APN_CERTIFICATE" -Ddw.apn.key="$APN_KEY" -Ddw.s3.accessKey=$AWS_ACCESS_KEY -Ddw.s3.accessSecret=$AWS_SECRET_KEY -Ddw.s3.attachmentsBucket=$AWS_ATTACHMENTS_BUCKET -Ddw.memcache.servers=$MEMCACHIER_SERVERS -Ddw.memcache.user=$MEMCACHIER_USERNAME -Ddw.memcache.password=$MEMCACHIER_PASSWORD -Ddw.redis.url=$REDIS_URL -Ddw.database.driverClass=org.postgresql.Driver -Ddw.database.user=`echo $DATABASE_URL | awk -F'://' {'print $2'} | awk -F':' {'print $1'}` -Ddw.database.password=`echo $DATABASE_URL | awk -F'://' {'print $2'} | awk -F':' {'print $2'} | awk -F'@' {'print $1'}` -Ddw.database.url=jdbc:postgresql://`echo $DATABASE_URL | awk -F'@' {'print $2'}` -jar target/TextSecure-MGCM-1.0-SNAPSHOT.jar server
dir: java $JAVA_OPTS -Ddw.http.port=$PORT -Ddw.http.adminPort=$PORT -Ddw.federation.name=$FEDERATION_NAME -Ddw.federation.herokuPeers="$FEDERATED_PEERS" -Ddw.twilio.accountId=$TWILIO_ACCOUNT_SID -Ddw.twilio.accountToken=$TWILIO_ACCOUNT_TOKEN -Ddw.twilio.number=$TWILIO_NUMBER -Ddw.nexmo.apiKey=$NEXMO_KEY -Ddw.nexmo.apiSecret=$NEXMO_SECRET -Ddw.nexmo.number=$NEXMO_NUMBER -Ddw.gcm.apiKey=$GCM_KEY -Ddw.apn.certificate="$APN_CERTIFICATE" -Ddw.apn.key="$APN_KEY" -Ddw.s3.accessKey=$AWS_ACCESS_KEY -Ddw.s3.accessSecret=$AWS_SECRET_KEY -Ddw.s3.attachmentsBucket=$AWS_ATTACHMENTS_BUCKET -Ddw.memcache.servers=$MEMCACHIER_SERVERS -Ddw.memcache.user=$MEMCACHIER_USERNAME -Ddw.memcache.password=$MEMCACHIER_PASSWORD -Ddw.redis.url=$REDIS_URL -Ddw.database.driverClass=org.postgresql.Driver -Ddw.database.user=`echo $DATABASE_URL | awk -F'://' {'print $2'} | awk -F':' {'print $1'}` -Ddw.database.password=`echo $DATABASE_URL | awk -F'://' {'print $2'} | awk -F':' {'print $2'} | awk -F'@' {'print $1'}` -Ddw.database.url=jdbc:postgresql://`echo $DATABASE_URL | awk -F'@' {'print $2'}` -jar target/TextSecure-MGCM-1.0-SNAPSHOT.jar directory

49
README.md Normal file
View File

@ -0,0 +1,49 @@
TextSecure-Server
=================
The server that handles message routing for the
[TextSecure](https://github.com/whispersystems/TextSecure/) data channel. Communication
is handled by a REST API and Push messaging (both GCM and APN).
Documentation
-------------
Looking for protocol documentation? Check out the wiki!
https://github.com/WhisperSystems/TextSecure-Server/wiki/API-Protocol
Bug tracker
-----------
Have a bug? Please create an issue here on GitHub!
https://github.com/WhisperSystems/TextSecure-Server/issues
Mailing list
------------
Have a question? Ask on our mailing list!
whispersystems@lists.riseup.net
https://lists.riseup.net/www/info/whispersystems
Cryptography Notice
------------
This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software.
BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted.
See <http://www.wassenaar.org/> for more information.
The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms.
The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code.
License
---------------------
Copyright 2013 Open Whisper Systems
Licensed under the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html

25
assembly.xml Normal file
View File

@ -0,0 +1,25 @@
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
<id>bin</id>
<includeBaseDirectory>false</includeBaseDirectory>
<formats>
<format>tar.gz</format>
</formats>
<fileSets>
<fileSet>
<directory>${project.basedir}/config</directory>
<outputDirectory>/config</outputDirectory>
<includes>
<include>*</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.build.directory}</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>${project.name}-${project.version}.jar</include>
</includes>
</fileSet>
</fileSets>
</assembly>

94
config/sample.yml Normal file
View File

@ -0,0 +1,94 @@
twilio:
accountId:
accountToken:
number:
# Optional. If specified, Nexmo will be used for non-US SMS and
# voice verification.
nexmo:
apiKey:
apiSecret:
number:
gcm:
apiKey:
# Optional. Only if iOS clients are supported.
apn:
# In PEM format.
certificate:
# In PEM format.
key:
s3:
accessKey:
accessSecret:
# Name of the S3 bucket (needs to have been created)
# for attachments to go. Should be configured with
# correct permissions.
attachmentsBucket:
memcache:
servers:
user:
password:
redis:
url:
federation:
name:
peers:
-
name: somepeer
url: https://foo.com
authenticationToken: foo
certificate: in pem format
# Optional address of graphite server to report metrics
graphite:
host:
port:
http:
shutdownGracePeriod: 0s
database:
# the name of your JDBC driver
driverClass: org.postgresql.Driver
# the username
user:
# the password
password:
# the JDBC URL
url: jdbc:postgresql://somehost:somport/somedb
# any properties specific to your JDBC driver:
properties:
charSet: UTF-8
# the maximum amount of time to wait on an empty pool before throwing an exception
maxWaitForConnection: 1s
# the SQL query to run when validating a connection's liveness
validationQuery: "/* MyService Health Check */ SELECT 1"
# the minimum number of connections to keep open
minSize: 8
# the maximum number of connections to keep open
maxSize: 32
# whether or not idle connections should be validated
checkConnectionWhileIdle: false
# how long a connection must be held before it can be validated
checkConnectionHealthWhenIdleFor: 10s
# the maximum lifetime of an idle connection
closeConnectionIfIdleFor: 1 minute

208
pom.xml Normal file
View File

@ -0,0 +1,208 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<prerequisites>
<maven>3.0.0</maven>
</prerequisites>
<groupId>org.whispersystems.textsecure</groupId>
<artifactId>TextSecureServer</artifactId>
<version>0.1</version>
<dependencies>
<dependency>
<groupId>bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>140</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.metrics</groupId>
<artifactId>metrics-graphite</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>com.google.android.gcm</groupId>
<artifactId>gcm-server</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>net.spy</groupId>
<artifactId>spymemcached</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>com.notnoop.apns</groupId>
<artifactId>apns</artifactId>
<version>0.2.3</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.2.1</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-jdbi</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-auth</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-client</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-migrations</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-testing</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.1-901.jdbc4</version>
</dependency>
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-json</artifactId>
<version>1.17.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>1.6</version>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.whispersystems.textsecuregcm.WhisperServerService</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<configuration>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>gcm-server-repository</id>
<url>https://raw.github.com/whispersystems/maven/master/gcm-server/releases/</url>
</repository>
</repositories>
</project>

3
protobuf/Makefile Normal file
View File

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

View File

@ -0,0 +1,29 @@
/**
* 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 textsecure;
option java_package = "org.whispersystems.textsecuregcm.entities";
option java_outer_classname = "MessageProtos";
message OutgoingMessageSignal {
optional uint32 type = 1;
optional string source = 2;
optional string relay = 3;
repeated string destinations = 4;
optional uint64 timestamp = 5;
optional bytes message = 6;
}

View File

@ -0,0 +1,129 @@
/**
* 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;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.yammer.dropwizard.config.Configuration;
import com.yammer.dropwizard.db.DatabaseConfiguration;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration;
import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
public class WhisperServerConfiguration extends Configuration {
@NotNull
@Valid
@JsonProperty
private TwilioConfiguration twilio;
@JsonProperty
private NexmoConfiguration nexmo = new NexmoConfiguration();
@NotNull
@JsonProperty
private GcmConfiguration gcm;
@NotNull
@Valid
@JsonProperty
private S3Configuration s3;
@NotNull
@Valid
@JsonProperty
private MemcacheConfiguration memcache;
@NotNull
@Valid
@JsonProperty
private RedisConfiguration redis;
@JsonProperty
private ApnConfiguration apn = new ApnConfiguration();
@Valid
@JsonProperty
private FederationConfiguration federation = new FederationConfiguration();
@Valid
@NotNull
@JsonProperty
private DatabaseConfiguration database = new DatabaseConfiguration();
@Valid
@NotNull
@JsonProperty
private RateLimitsConfiguration limits = new RateLimitsConfiguration();
@Valid
@JsonProperty
private GraphiteConfiguration graphite = new GraphiteConfiguration();
public TwilioConfiguration getTwilioConfiguration() {
return twilio;
}
public NexmoConfiguration getNexmoConfiguration() {
return nexmo;
}
public GcmConfiguration getGcmConfiguration() {
return gcm;
}
public ApnConfiguration getApnConfiguration() {
return apn;
}
public S3Configuration getS3Configuration() {
return s3;
}
public MemcacheConfiguration getMemcacheConfiguration() {
return memcache;
}
public RedisConfiguration getRedisConfiguration() {
return redis;
}
public DatabaseConfiguration getDatabaseConfiguration() {
return database;
}
public RateLimitsConfiguration getLimitsConfiguration() {
return limits;
}
public FederationConfiguration getFederationConfiguration() {
return federation;
}
public GraphiteConfiguration getGraphiteConfiguration() {
return graphite;
}
}

View File

@ -0,0 +1,140 @@
/**
* 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;
import com.yammer.dropwizard.Service;
import com.yammer.dropwizard.config.Bootstrap;
import com.yammer.dropwizard.config.Environment;
import com.yammer.dropwizard.db.DatabaseConfiguration;
import com.yammer.dropwizard.jdbi.DBIFactory;
import com.yammer.dropwizard.migrations.MigrationsBundle;
import com.yammer.metrics.reporting.GraphiteReporter;
import net.spy.memcached.MemcachedClient;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.skife.jdbi.v2.DBI;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
import org.whispersystems.textsecuregcm.auth.MultiBasicAuthProvider;
import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.AttachmentController;
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
import org.whispersystems.textsecuregcm.controllers.FederationController;
import org.whispersystems.textsecuregcm.controllers.KeysController;
import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
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.push.PushSender;
import org.whispersystems.textsecuregcm.sms.SenderFactory;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.util.UrlSigner;
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
import java.security.Security;
import java.util.concurrent.TimeUnit;
import redis.clients.jedis.JedisPool;
public class WhisperServerService extends Service<WhisperServerConfiguration> {
static {
Security.addProvider(new BouncyCastleProvider());
}
@Override
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
bootstrap.setName("whisper-server");
bootstrap.addCommand(new DirectoryCommand());
bootstrap.addBundle(new MigrationsBundle<WhisperServerConfiguration>() {
@Override
public DatabaseConfiguration getDatabaseConfiguration(WhisperServerConfiguration configuration) {
return configuration.getDatabaseConfiguration();
}
});
}
@Override
public void run(WhisperServerConfiguration config, Environment environment)
throws Exception
{
DBIFactory dbiFactory = new DBIFactory();
DBI jdbi = dbiFactory.build(environment, config.getDatabaseConfiguration(), "postgresql");
Accounts accounts = jdbi.onDemand(Accounts.class);
PendingAccounts pendingAccounts = jdbi.onDemand(PendingAccounts.class);
Keys keys = jdbi.onDemand(Keys.class);
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool();
DirectoryManager directory = new DirectoryManager(redisClient);
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient);
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
SenderFactory senderFactory = new SenderFactory(config.getTwilioConfiguration(), config.getNexmoConfiguration());
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
PushSender pushSender = new PushSender(config.getGcmConfiguration(),
config.getApnConfiguration(),
accountsManager, directory);
environment.addProvider(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
FederatedPeer.class,
accountAuthenticator,
Account.class, "WhisperServer"));
environment.addResource(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, senderFactory));
environment.addResource(new DirectoryController(rateLimiters, directory));
environment.addResource(new AttachmentController(rateLimiters, federatedClientManager, urlSigner));
environment.addResource(new KeysController(rateLimiters, keys, federatedClientManager));
environment.addResource(new FederationController(keys, accountsManager, pushSender, urlSigner));
environment.addServlet(new MessageController(rateLimiters, accountAuthenticator,
pushSender, federatedClientManager),
MessageController.PATH);
environment.addHealthCheck(new RedisHealthCheck(redisClient));
environment.addHealthCheck(new MemcacheHealthCheck(memcachedClient));
environment.addProvider(new IOExceptionMapper());
environment.addProvider(new RateLimitExceededExceptionMapper());
if (config.getGraphiteConfiguration().isEnabled()) {
GraphiteReporter.enable(15, TimeUnit.SECONDS,
config.getGraphiteConfiguration().getHost(),
config.getGraphiteConfiguration().getPort());
}
}
public static void main(String[] args) throws Exception {
new WhisperServerService().run(args);
}
}

View File

@ -0,0 +1,68 @@
/**
* 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.google.common.base.Optional;
import com.yammer.dropwizard.auth.AuthenticationException;
import com.yammer.dropwizard.auth.Authenticator;
import com.yammer.dropwizard.auth.basic.BasicCredentials;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import java.util.concurrent.TimeUnit;
public class AccountAuthenticator implements Authenticator<BasicCredentials, Account> {
private final Meter authenticationFailedMeter = Metrics.newMeter(AccountAuthenticator.class,
"authentication", "failed",
TimeUnit.MINUTES);
private final Meter authenticationSucceededMeter = Metrics.newMeter(AccountAuthenticator.class,
"authentication", "succeeded",
TimeUnit.MINUTES);
private final Logger logger = LoggerFactory.getLogger(AccountAuthenticator.class);
private final AccountsManager accountsManager;
public AccountAuthenticator(AccountsManager accountsManager) {
this.accountsManager = accountsManager;
}
@Override
public Optional<Account> authenticate(BasicCredentials basicCredentials)
throws AuthenticationException
{
Optional<Account> account = accountsManager.get(basicCredentials.getUsername());
if (!account.isPresent()) {
return Optional.absent();
}
if (account.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
authenticationSucceededMeter.mark();
return account;
}
authenticationFailedMeter.mark();
return Optional.absent();
}
}

View File

@ -0,0 +1,76 @@
/**
* 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 org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class AuthenticationCredentials {
private final Logger logger = LoggerFactory.getLogger(AuthenticationCredentials.class);
private final String hashedAuthenticationToken;
private final String salt;
public AuthenticationCredentials(String hashedAuthenticationToken, String salt) {
this.hashedAuthenticationToken = hashedAuthenticationToken;
this.salt = salt;
}
public AuthenticationCredentials(String authenticationToken) {
try {
this.salt = Math.abs(SecureRandom.getInstance("SHA1PRNG").nextInt()) + "";
this.hashedAuthenticationToken = getHashedValue(salt, authenticationToken);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public String getHashedAuthenticationToken() {
return hashedAuthenticationToken;
}
public String getSalt() {
return salt;
}
public boolean verify(String authenticationToken) {
String theirValue = getHashedValue(salt, authenticationToken);
logger.debug("Comparing: " + theirValue + " , " + this.hashedAuthenticationToken);
return theirValue.equals(this.hashedAuthenticationToken);
}
private static String getHashedValue(String salt, String token) {
Logger logger = LoggerFactory.getLogger(AuthenticationCredentials.class);
logger.debug("Getting hashed token: " + salt + " , " + token);
try {
return new String(Hex.encodeHex(MessageDigest.getInstance("SHA1").digest((salt + token).getBytes("UTF-8"))));
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,73 @@
/**
* 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 org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
public class AuthorizationHeader {
private final String user;
private final String password;
public AuthorizationHeader(String header) throws InvalidAuthorizationHeaderException {
try {
if (header == null) {
throw new InvalidAuthorizationHeaderException("Null header");
}
String[] headerParts = header.split(" ");
if (headerParts == null || headerParts.length < 2) {
throw new InvalidAuthorizationHeaderException("Invalid authorization header: " + header);
}
if (!"Basic".equals(headerParts[0])) {
throw new InvalidAuthorizationHeaderException("Unsupported authorization method: " + headerParts[0]);
}
String concatenatedValues = new String(Base64.decode(headerParts[1]));
if (Util.isEmpty(concatenatedValues)) {
throw new InvalidAuthorizationHeaderException("Bad decoded value: " + concatenatedValues);
}
String[] credentialParts = concatenatedValues.split(":");
if (credentialParts == null || credentialParts.length < 2) {
throw new InvalidAuthorizationHeaderException("Badly formated credentials: " + concatenatedValues);
}
this.user = credentialParts[0];
this.password = credentialParts[1];
} catch (IOException ioe) {
throw new InvalidAuthorizationHeaderException(ioe);
}
}
public String getUserName() {
return user;
}
public String getPassword() {
return password;
}
}

View File

@ -0,0 +1,74 @@
/**
* 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.google.common.base.Optional;
import com.yammer.dropwizard.auth.AuthenticationException;
import com.yammer.dropwizard.auth.Authenticator;
import com.yammer.dropwizard.auth.basic.BasicCredentials;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class FederatedPeerAuthenticator implements Authenticator<BasicCredentials, FederatedPeer> {
private final Meter authenticationFailedMeter = Metrics.newMeter(FederatedPeerAuthenticator.class,
"authentication", "failed",
TimeUnit.MINUTES);
private final Meter authenticationSucceededMeter = Metrics.newMeter(FederatedPeerAuthenticator.class,
"authentication", "succeeded",
TimeUnit.MINUTES);
private final Logger logger = LoggerFactory.getLogger(FederatedPeerAuthenticator.class);
private final List<FederatedPeer> peers;
public FederatedPeerAuthenticator(FederationConfiguration config) {
this.peers = config.getPeers();
}
@Override
public Optional<FederatedPeer> authenticate(BasicCredentials basicCredentials)
throws AuthenticationException
{
if (peers == null) {
authenticationFailedMeter.mark();
return Optional.absent();
}
for (FederatedPeer peer : peers) {
if (basicCredentials.getUsername().equals(peer.getName()) &&
basicCredentials.getPassword().equals(peer.getAuthenticationToken()))
{
authenticationSucceededMeter.mark();
return Optional.of(peer);
}
}
authenticationFailedMeter.mark();
return Optional.absent();
}
}

View File

@ -0,0 +1,28 @@
/**
* 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;
public class InvalidAuthorizationHeaderException extends Exception {
public InvalidAuthorizationHeaderException(String s) {
super(s);
}
public InvalidAuthorizationHeaderException(Exception e) {
super(e);
}
}

View File

@ -0,0 +1,69 @@
/**
* 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 com.yammer.dropwizard.auth.Auth;
import com.yammer.dropwizard.auth.Authenticator;
import com.yammer.dropwizard.auth.basic.BasicAuthProvider;
import com.yammer.dropwizard.auth.basic.BasicCredentials;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MultiBasicAuthProvider<T1,T2> implements InjectableProvider<Auth, Parameter> {
private final Logger logger = LoggerFactory.getLogger(MultiBasicAuthProvider.class);
private final BasicAuthProvider<T1> provider1;
private final BasicAuthProvider<T2> provider2;
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<T1>(authenticator1, realm);
this.provider2 = new BasicAuthProvider<T2>(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

@ -0,0 +1,40 @@
/**
* 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 ApnConfiguration {
@NotEmpty
@JsonProperty
private String certificate;
@NotEmpty
@JsonProperty
private String key;
public String getCertificate() {
return certificate;
}
public String getKey() {
return key;
}
}

View File

@ -0,0 +1,68 @@
/**
* 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 com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import java.util.LinkedList;
import java.util.List;
public class FederationConfiguration {
@JsonProperty
private List<FederatedPeer> peers;
@JsonProperty
private String name;
@JsonProperty
private String herokuPeers;
public List<FederatedPeer> getPeers() {
if (peers != null) {
return peers;
}
if (herokuPeers != null) {
List<FederatedPeer> peers = new LinkedList<>();
JsonElement root = new JsonParser().parse(herokuPeers);
JsonArray peerElements = root.getAsJsonArray();
for (JsonElement peer : peerElements) {
String name = peer.getAsJsonObject().get("name").getAsString();
String url = peer.getAsJsonObject().get("url").getAsString();
String authenticationToken = peer.getAsJsonObject().get("authenticationToken").getAsString();
String certificate = peer.getAsJsonObject().get("certificate").getAsString();
peers.add(new FederatedPeer(name, url, authenticationToken, certificate));
}
return peers;
}
return peers;
}
public String getName() {
return name;
}
}

View File

@ -0,0 +1,31 @@
/**
* 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 GcmConfiguration {
@NotEmpty
@JsonProperty
private String apiKey;
public String getApiKey() {
return apiKey;
}
}

View File

@ -0,0 +1,39 @@
/**
* 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 GraphiteConfiguration {
@JsonProperty
private String host;
@JsonProperty
private int port;
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public boolean isEnabled() {
return host != null && port != 0;
}
}

View File

@ -0,0 +1,46 @@
/**
* 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

@ -0,0 +1,43 @@
/**
* 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

@ -0,0 +1,94 @@
/**
* 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 RateLimitsConfiguration {
@JsonProperty
private RateLimitConfiguration smsDestination = new RateLimitConfiguration(2, 2);
@JsonProperty
private RateLimitConfiguration voiceDestination = new RateLimitConfiguration(2, 1.0 / 2.0);
@JsonProperty
private RateLimitConfiguration verifyNumber = new RateLimitConfiguration(2, 2);
@JsonProperty
private RateLimitConfiguration attachments = new RateLimitConfiguration(50, 50);
@JsonProperty
private RateLimitConfiguration contactQueries = new RateLimitConfiguration(50000, 50000);
@JsonProperty
private RateLimitConfiguration prekeys = new RateLimitConfiguration(3, 1.0 / 10.0);
@JsonProperty
private RateLimitConfiguration messages = new RateLimitConfiguration(60, 60);
public RateLimitConfiguration getMessages() {
return messages;
}
public RateLimitConfiguration getPreKeys() {
return prekeys;
}
public RateLimitConfiguration getContactQueries() {
return contactQueries;
}
public RateLimitConfiguration getAttachments() {
return attachments;
}
public RateLimitConfiguration getSmsDestination() {
return smsDestination;
}
public RateLimitConfiguration getVoiceDestination() {
return voiceDestination;
}
public RateLimitConfiguration getVerifyNumber() {
return verifyNumber;
}
public static class RateLimitConfiguration {
@JsonProperty
private int bucketSize;
@JsonProperty
private double leakRatePerMinute;
public RateLimitConfiguration(int bucketSize, double leakRatePerMinute) {
this.bucketSize = bucketSize;
this.leakRatePerMinute = leakRatePerMinute;
}
public RateLimitConfiguration() {}
public int getBucketSize() {
return bucketSize;
}
public double getLeakRatePerMinute() {
return leakRatePerMinute;
}
}
}

View File

@ -0,0 +1,33 @@
/**
* 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;
import org.hibernate.validator.constraints.URL;
public class RedisConfiguration {
@JsonProperty
@NotEmpty
private String url;
public String getUrl() {
return url;
}
}

View File

@ -0,0 +1,47 @@
/**
* 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 S3Configuration {
@NotEmpty
@JsonProperty
private String accessKey;
@NotEmpty
@JsonProperty
private String accessSecret;
@NotEmpty
@JsonProperty
private String attachmentsBucket;
public String getAccessKey() {
return accessKey;
}
public String getAccessSecret() {
return accessSecret;
}
public String getAttachmentsBucket() {
return attachmentsBucket;
}
}

View File

@ -0,0 +1,47 @@
/**
* 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 TwilioConfiguration {
@NotEmpty
@JsonProperty
private String accountId;
@NotEmpty
@JsonProperty
private String accountToken;
@NotEmpty
@JsonProperty
private String number;
public String getAccountId() {
return accountId;
}
public String getAccountToken() {
return accountToken;
}
public String getNumber() {
return number;
}
}

View File

@ -0,0 +1,195 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.controllers;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.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.ApnRegistrationId;
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.sms.SenderFactory;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
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;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
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;
@Path("/v1/accounts")
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 SenderFactory senderFactory;
public AccountController(PendingAccountsManager pendingAccounts,
AccountsManager accounts,
RateLimiters rateLimiters,
SenderFactory smsSenderFactory)
{
this.pendingAccounts = pendingAccounts;
this.accounts = accounts;
this.rateLimiters = rateLimiters;
this.senderFactory = smsSenderFactory;
}
@Timed
@GET
@Path("/{transport}/code/{number}")
public Response createAccount(@PathParam("transport") String transport,
@PathParam("number") String number)
throws IOException, RateLimitExceededException
{
if (!Util.isValidNumber(number)) {
logger.debug("Invalid number: " + number);
throw new WebApplicationException(Response.status(400).build());
}
switch (transport) {
case "sms":
rateLimiters.getSmsDestinationLimiter().validate(number);
break;
case "voice":
rateLimiters.getVoiceDestinationLimiter().validate(number);
break;
default:
throw new WebApplicationException(Response.status(415).build());
}
VerificationCode verificationCode = generateVerificationCode();
pendingAccounts.store(number, verificationCode.getVerificationCode());
if (transport.equals("sms")) {
senderFactory.getSmsSender(number).deliverSmsVerification(number, verificationCode.getVerificationCodeDisplay());
} else if (transport.equals("voice")) {
senderFactory.getVoxSender(number).deliverVoxVerification(number, verificationCode.getVerificationCodeSpeech());
}
return Response.ok().build();
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Path("/code/{verification_code}")
public void verifyAccount(@PathParam("verification_code") String verificationCode,
@HeaderParam("Authorization") String authorizationHeader,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
{
try {
AuthorizationHeader header = new AuthorizationHeader(authorizationHeader);
String number = header.getUserName();
String password = header.getPassword();
rateLimiters.getVerifyLimiter().validate(number);
Optional<String> storedVerificationCode = pendingAccounts.getCodeForNumber(number);
if (!storedVerificationCode.isPresent() ||
!verificationCode.equals(storedVerificationCode.get()))
{
throw new WebApplicationException(Response.status(403).build());
}
Account account = new Account();
account.setNumber(number);
account.setAuthenticationCredentials(new AuthenticationCredentials(password));
account.setSignalingKey(accountAttributes.getSignalingKey());
account.setSupportsSms(accountAttributes.getSupportsSms());
accounts.create(account);
logger.debug("Stored account...");
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad Authorization Header", e);
throw new WebApplicationException(Response.status(401).build());
}
}
@Timed
@PUT
@Path("/gcm/")
@Consumes(MediaType.APPLICATION_JSON)
public void setGcmRegistrationId(@Auth Account account, @Valid GcmRegistrationId registrationId) {
account.setApnRegistrationId(null);
account.setGcmRegistrationId(registrationId.getGcmRegistrationId());
accounts.update(account);
}
@Timed
@DELETE
@Path("/gcm/")
public void deleteGcmRegistrationId(@Auth Account account) {
account.setGcmRegistrationId(null);
accounts.update(account);
}
@Timed
@PUT
@Path("/apn/")
@Consumes(MediaType.APPLICATION_JSON)
public void setApnRegistrationId(@Auth Account account, @Valid ApnRegistrationId registrationId) {
account.setApnRegistrationId(registrationId.getApnRegistrationId());
account.setGcmRegistrationId(null);
accounts.update(account);
}
@Timed
@DELETE
@Path("/apn/")
public void deleteApnRegistrationId(@Auth Account account) {
account.setApnRegistrationId(null);
accounts.update(account);
}
private VerificationCode generateVerificationCode() {
try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
int randomInt = 100000 + random.nextInt(900000);
return new VerificationCode(randomInt);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,113 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.controllers;
import com.amazonaws.HttpMethod;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptor;
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.Conversions;
import org.whispersystems.textsecuregcm.util.UrlSigner;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@Path("/v1/attachments")
public class AttachmentController {
private final Logger logger = LoggerFactory.getLogger(AttachmentController.class);
private final RateLimiters rateLimiters;
private final FederatedClientManager federatedClientManager;
private final UrlSigner urlSigner;
public AttachmentController(RateLimiters rateLimiters,
FederatedClientManager federatedClientManager,
UrlSigner urlSigner)
{
this.rateLimiters = rateLimiters;
this.federatedClientManager = federatedClientManager;
this.urlSigner = urlSigner;
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response allocateAttachment(@Auth Account account) throws RateLimitExceededException {
rateLimiters.getAttachmentLimiter().validate(account.getNumber());
long attachmentId = generateAttachmentId();
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.PUT);
AttachmentDescriptor descriptor = new AttachmentDescriptor(attachmentId, url.toExternalForm());
return Response.ok().entity(descriptor).build();
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/{attachmentId}")
public Response redirectToAttachment(@Auth Account account,
@PathParam("attachmentId") long attachmentId,
@QueryParam("relay") String relay)
{
try {
URL url;
if (relay == null) url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET);
else url = federatedClientManager.getClient(relay).getSignedAttachmentUri(attachmentId);
return Response.ok().entity(new AttachmentUri(url)).build();
} catch (IOException e) {
logger.warn("No conectivity", e);
return Response.status(500).build();
} catch (NoSuchPeerException e) {
logger.info("No such peer: " + relay);
return Response.status(404).build();
}
}
private long generateAttachmentId() {
try {
byte[] attachmentBytes = new byte[8];
SecureRandom.getInstance("SHA1PRNG").nextBytes(attachmentBytes);
attachmentBytes[0] = (byte)(attachmentBytes[0] & 0x7F);
return Conversions.byteArrayToLong(attachmentBytes);
} catch (NoSuchAlgorithmException nsae) {
throw new AssertionError(nsae);
}
}
}

View File

@ -0,0 +1,105 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.controllers;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Metered;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
@Path("/v1/directory")
public class DirectoryController {
private final Logger logger = LoggerFactory.getLogger(DirectoryController.class);
private final RateLimiters rateLimiters;
private final DirectoryManager directory;
public DirectoryController(RateLimiters rateLimiters, DirectoryManager directory) {
this.directory = directory;
this.rateLimiters = rateLimiters;
}
@Timed()
@GET
@Path("/{token}")
@Produces(MediaType.APPLICATION_JSON)
public Response getTokenPresence(@Auth Account account, @PathParam("token") String token)
throws RateLimitExceededException
{
rateLimiters.getContactsLimiter().validate(account.getNumber());
try {
Optional<ClientContact> contact = directory.get(Base64.decodeWithoutPadding(token));
if (contact.isPresent()) return Response.ok().entity(contact.get()).build();
else return Response.status(404).build();
} catch (IOException e) {
logger.info("Bad token", e);
return Response.status(404).build();
}
}
@Timed()
@PUT
@Path("/tokens")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public ClientContacts getContactIntersection(@Auth Account account, @Valid ClientContactTokens contacts)
throws RateLimitExceededException
{
rateLimiters.getContactsLimiter().validate(account.getNumber(), contacts.getContacts().size());
try {
List<byte[]> tokens = new LinkedList<>();
for (String encodedContact : contacts.getContacts()) {
tokens.add(Base64.decodeWithoutPadding(encodedContact));
}
List<ClientContact> intersection = directory.get(tokens);
return new ClientContacts(intersection);
} catch (IOException e) {
logger.info("Bad token", e);
throw new WebApplicationException(Response.status(400).build());
}
}
}

View File

@ -0,0 +1,157 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.controllers;
import com.amazonaws.HttpMethod;
import com.google.protobuf.InvalidProtocolBufferException;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AccountCount;
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.RelayMessage;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.util.UrlSigner;
import org.whispersystems.textsecuregcm.util.Util;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
@Path("/v1/federation")
public class FederationController {
private final Logger logger = LoggerFactory.getLogger(FederationController.class);
private static final int ACCOUNT_CHUNK_SIZE = 10000;
private final PushSender pushSender;
private final Keys keys;
private final AccountsManager accounts;
private final UrlSigner urlSigner;
public FederationController(Keys keys, AccountsManager accounts, PushSender pushSender, UrlSigner urlSigner) {
this.keys = keys;
this.accounts = accounts;
this.pushSender = pushSender;
this.urlSigner = urlSigner;
}
@Timed
@GET
@Path("/attachment/{attachmentId}")
@Produces(MediaType.APPLICATION_JSON)
public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer,
@PathParam("attachmentId") long attachmentId)
{
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET);
return new AttachmentUri(url);
}
@Timed
@GET
@Path("/key/{number}")
@Produces(MediaType.APPLICATION_JSON)
public PreKey getKey(@Auth FederatedPeer peer,
@PathParam("number") String number)
{
PreKey preKey = keys.get(number);
if (preKey == null) {
throw new WebApplicationException(Response.status(404).build());
}
return preKey;
}
@Timed
@PUT
@Path("/message")
@Consumes(MediaType.APPLICATION_JSON)
public void relayMessage(@Auth FederatedPeer peer, @Valid RelayMessage message)
throws IOException
{
try {
OutgoingMessageSignal signal = OutgoingMessageSignal.parseFrom(message.getOutgoingMessageSignal())
.toBuilder()
.setRelay(peer.getName())
.build();
pushSender.sendMessage(message.getDestination(), signal);
} catch (InvalidProtocolBufferException ipe) {
logger.warn("ProtoBuf", ipe);
throw new WebApplicationException(Response.status(400).build());
} catch (NoSuchUserException e) {
logger.debug("No User", e);
throw new WebApplicationException(Response.status(404).build());
}
}
@Timed
@GET
@Path("/user_count")
@Produces(MediaType.APPLICATION_JSON)
public AccountCount getUserCount(@Auth FederatedPeer peer) {
return new AccountCount((int)accounts.getCount());
}
@Timed
@GET
@Path("/user_tokens/{offset}")
@Produces(MediaType.APPLICATION_JSON)
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
@PathParam("offset") int offset)
{
List<Account> accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE);
List<ClientContact> clientContacts = new LinkedList<>();
for (Account account : accountList) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
if (Util.isEmpty(account.getApnRegistrationId()) &&
Util.isEmpty(account.getGcmRegistrationId()))
{
clientContact.setInactive(true);
}
clientContacts.add(clientContact);
}
return new ClientContacts(clientContacts);
}
}

View File

@ -0,0 +1,92 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.controllers;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.PreKeyList;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Keys;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path("/v1/keys")
public class KeysController {
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
private final RateLimiters rateLimiters;
private final Keys keys;
private final FederatedClientManager federatedClientManager;
public KeysController(RateLimiters rateLimiters, Keys keys,
FederatedClientManager federatedClientManager)
{
this.rateLimiters = rateLimiters;
this.keys = keys;
this.federatedClientManager = federatedClientManager;
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void setKeys(@Auth Account account, @Valid PreKeyList preKeys) {
keys.store(account.getNumber(), preKeys.getLastResortKey(), preKeys.getKeys());
}
@Timed
@GET
@Path("/{number}")
@Produces(MediaType.APPLICATION_JSON)
public PreKey get(@Auth Account account,
@PathParam("number") String number,
@QueryParam("relay") String relay)
throws RateLimitExceededException
{
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number);
try {
PreKey key;
if (relay == null) key = keys.get(number);
else key = federatedClientManager.getClient(relay).getKey(number);
if (key == null) throw new WebApplicationException(Response.status(404).build());
else return key;
} catch (NoSuchPeerException e) {
logger.info("No peer: " + relay);
throw new WebApplicationException(Response.status(404).build());
}
}
}

View File

@ -0,0 +1,313 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.controllers;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import com.google.protobuf.ByteString;
import com.yammer.dropwizard.auth.AuthenticationException;
import com.yammer.dropwizard.auth.basic.BasicCredentials;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import com.yammer.metrics.core.Timer;
import com.yammer.metrics.core.TimerContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
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.federation.FederatedClient;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.IterablePair;
import org.whispersystems.textsecuregcm.util.IterablePair.Pair;
import org.whispersystems.textsecuregcm.util.Util;
import javax.servlet.AsyncContext;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class MessageController extends HttpServlet {
public static final String PATH = "/v1/messages/";
private final Meter successMeter = Metrics.newMeter(MessageController.class, "deliver_message", "success", TimeUnit.MINUTES);
private final Meter failureMeter = Metrics.newMeter(MessageController.class, "deliver_message", "failure", TimeUnit.MINUTES);
private final Timer timer = Metrics.newTimer(MessageController.class, "deliver_message_time", TimeUnit.MILLISECONDS, TimeUnit.MINUTES);
private final Logger logger = LoggerFactory.getLogger(MessageController.class);
private final RateLimiters rateLimiters;
private final AccountAuthenticator accountAuthenticator;
private final PushSender pushSender;
private final FederatedClientManager federatedClientManager;
private final ObjectMapper objectMapper;
private final ExecutorService executor;
public MessageController(RateLimiters rateLimiters,
AccountAuthenticator accountAuthenticator,
PushSender pushSender,
FederatedClientManager federatedClientManager)
{
this.rateLimiters = rateLimiters;
this.accountAuthenticator = accountAuthenticator;
this.pushSender = pushSender;
this.federatedClientManager = federatedClientManager;
this.objectMapper = new ObjectMapper();
this.executor = Executors.newFixedThreadPool(10);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
TimerContext timerContext = timer.time();
try {
Account sender = authenticate(req);
IncomingMessageList messages = parseIncomingMessages(req);
rateLimiters.getMessagesLimiter().validate(sender.getNumber());
List<IncomingMessage> incomingMessages = messages.getMessages();
List<OutgoingMessageSignal> outgoingMessages = getOutgoingMessageSignals(sender.getNumber(),
incomingMessages);
IterablePair<IncomingMessage, OutgoingMessageSignal> listPair = new IterablePair<>(incomingMessages,
outgoingMessages);
handleAsyncDelivery(timerContext, req.startAsync(), listPair);
} catch (AuthenticationException e) {
failureMeter.mark();
timerContext.stop();
resp.setStatus(401);
} catch (ValidationException e) {
failureMeter.mark();
timerContext.stop();
resp.setStatus(415);
} catch (IOException e) {
logger.warn("IOE", e);
failureMeter.mark();
timerContext.stop();
resp.setStatus(501);
} catch (RateLimitExceededException e) {
timerContext.stop();
failureMeter.mark();
resp.setStatus(413);
}
}
private void handleAsyncDelivery(final TimerContext timerContext,
final AsyncContext context,
final IterablePair<IncomingMessage, OutgoingMessageSignal> listPair)
{
executor.submit(new Runnable() {
@Override
public void run() {
List<String> success = new LinkedList<>();
List<String> failure = new LinkedList<>();
HttpServletResponse response = (HttpServletResponse) context.getResponse();
try {
for (Pair<IncomingMessage, OutgoingMessageSignal> messagePair : listPair) {
String destination = messagePair.first().getDestination();
String relay = messagePair.first().getRelay();
try {
if (Util.isEmpty(relay)) sendLocalMessage(destination, messagePair.second());
else sendRelayMessage(relay, destination, messagePair.second());
success.add(destination);
} catch (NoSuchUserException e) {
logger.debug("No such user", e);
failure.add(destination);
}
}
byte[] responseData = serializeResponse(new MessageResponse(success, failure));
response.setContentLength(responseData.length);
response.getOutputStream().write(responseData);
context.complete();
successMeter.mark();
} catch (IOException e) {
logger.warn("Async Handler", e);
failureMeter.mark();
response.setStatus(501);
context.complete();
}
timerContext.stop();
}
});
}
private void sendLocalMessage(String destination, OutgoingMessageSignal outgoingMessage)
throws IOException, NoSuchUserException
{
pushSender.sendMessage(destination, outgoingMessage);
}
private void sendRelayMessage(String relay, String destination, OutgoingMessageSignal outgoingMessage)
throws IOException, NoSuchUserException
{
try {
FederatedClient client = federatedClientManager.getClient(relay);
client.sendMessage(destination, outgoingMessage);
} catch (NoSuchPeerException e) {
logger.info("No such peer", e);
throw new NoSuchUserException(e);
}
}
private List<OutgoingMessageSignal> getOutgoingMessageSignals(String number,
List<IncomingMessage> incomingMessages)
{
List<OutgoingMessageSignal> outgoingMessages = new LinkedList<>();
for (IncomingMessage incoming : incomingMessages) {
OutgoingMessageSignal.Builder outgoingMessage = OutgoingMessageSignal.newBuilder();
outgoingMessage.setType(incoming.getType());
outgoingMessage.setSource(number);
byte[] messageBody = getMessageBody(incoming);
if (messageBody != null) {
outgoingMessage.setMessage(ByteString.copyFrom(messageBody));
}
outgoingMessage.setTimestamp(System.currentTimeMillis());
int index = 0;
for (IncomingMessage sub : incomingMessages) {
if (sub != incoming) {
outgoingMessage.setDestinations(index++, sub.getDestination());
}
}
outgoingMessages.add(outgoingMessage.build());
}
return outgoingMessages;
}
private byte[] getMessageBody(IncomingMessage message) {
try {
return Base64.decode(message.getBody());
} catch (IOException ioe) {
ioe.printStackTrace();
return null;
}
}
private byte[] serializeResponse(MessageResponse response) throws IOException {
try {
return objectMapper.writeValueAsBytes(response);
} catch (JsonProcessingException e) {
throw new IOException(e);
}
}
private IncomingMessageList parseIncomingMessages(HttpServletRequest request)
throws IOException, ValidationException
{
BufferedReader reader = request.getReader();
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line);
}
IncomingMessageList messages = objectMapper.readValue(content.toString(),
IncomingMessageList.class);
if (messages.getMessages() == null) {
throw new ValidationException();
}
for (IncomingMessage message : messages.getMessages()) {
if (message.getBody() == null) throw new ValidationException();
if (message.getDestination() == null) throw new ValidationException();
}
return messages;
}
private Account authenticate(HttpServletRequest request) throws AuthenticationException {
try {
AuthorizationHeader authorizationHeader = new AuthorizationHeader(request.getHeader("Authorization"));
BasicCredentials credentials = new BasicCredentials(authorizationHeader.getUserName(),
authorizationHeader.getPassword() );
Optional<Account> account = accountAuthenticator.authenticate(credentials);
if (account.isPresent()) return account.get();
else throw new AuthenticationException("Bad credentials");
} catch (InvalidAuthorizationHeaderException e) {
throw new AuthenticationException(e);
}
}
// @Timed
// @POST
// @Consumes(MediaType.APPLICATION_JSON)
// @Produces(MediaType.APPLICATION_JSON)
// public MessageResponse sendMessage(@Auth Account sender, IncomingMessageList messages)
// throws IOException
// {
// List<String> success = new LinkedList<>();
// List<String> failure = new LinkedList<>();
// List<IncomingMessage> incomingMessages = messages.getMessages();
// List<OutgoingMessageSignal> outgoingMessages = getOutgoingMessageSignals(sender.getNumber(), incomingMessages);
//
// IterablePair<IncomingMessage, OutgoingMessageSignal> listPair = new IterablePair<>(incomingMessages, outgoingMessages);
//
// for (Pair<IncomingMessage, OutgoingMessageSignal> messagePair : listPair) {
// String destination = messagePair.first().getDestination();
// String relay = messagePair.first().getRelay();
//
// try {
// if (Util.isEmpty(relay)) sendLocalMessage(destination, messagePair.second());
// else sendRelayMessage(relay, destination, messagePair.second());
// success.add(destination);
// } catch (NoSuchUserException e) {
// logger.debug("No such user", e);
// failure.add(destination);
// }
// }
//
// return new MessageResponse(success, failure);
// }
}

View File

@ -0,0 +1,45 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.controllers;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import java.util.LinkedList;
import java.util.List;
public class NoSuchUserException extends Exception {
private List<String> missing;
public NoSuchUserException(String user) {
super(user);
missing = new LinkedList<String>();
missing.add(user);
}
public NoSuchUserException(List<String> missing) {
this.missing = missing;
}
public NoSuchUserException(Exception e) {
super(e);
}
public List<String> getMissing() {
return missing;
}
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.controllers;
public class RateLimitExceededException extends Exception {
public RateLimitExceededException(String number) {
super(number);
}
}

View File

@ -0,0 +1,21 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.controllers;
public class ValidationException extends Exception {
}

View File

@ -0,0 +1,46 @@
/**
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
public class AccountAttributes {
@JsonProperty
@NotEmpty
private String signalingKey;
@JsonProperty
private boolean supportsSms;
public AccountAttributes() {}
public AccountAttributes(String signalingKey, boolean supportsSms) {
this.signalingKey = signalingKey;
this.supportsSms = supportsSms;
}
public String getSignalingKey() {
return signalingKey;
}
public boolean getSupportsSms() {
return supportsSms;
}
}

View File

@ -0,0 +1,35 @@
/**
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
public class AccountCount {
@JsonProperty
private int count;
public AccountCount(int count) {
this.count = count;
}
public AccountCount() {}
public int getCount() {
return count;
}
}

View File

@ -0,0 +1,31 @@
/**
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
public class ApnRegistrationId {
@JsonProperty
@NotEmpty
private String apnRegistrationId;
public String getApnRegistrationId() {
return apnRegistrationId;
}
}

View File

@ -0,0 +1,43 @@
/**
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
public class AttachmentDescriptor {
@JsonProperty
private long id;
@JsonProperty
private String location;
public AttachmentDescriptor(long id, String location) {
this.id = id;
this.location = location;
}
public AttachmentDescriptor() {}
public long getId() {
return id;
}
public String getLocation() {
return location;
}
}

View File

@ -0,0 +1,39 @@
/**
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
public class AttachmentUri {
@JsonProperty
private String location;
public AttachmentUri(URL uri) {
this.location = uri.toString();
}
public AttachmentUri() {}
public URL getLocation() throws MalformedURLException {
return URI.create(location).toURL();
}
}

View File

@ -0,0 +1,98 @@
/**
* 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.entities;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.gson.Gson;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.util.Arrays;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class ClientContact {
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@JsonProperty
private byte[] token;
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() {}
public byte[] getToken() {
return token;
}
public String getRelay() {
return relay;
}
public void setRelay(String relay) {
this.relay = relay;
}
public boolean isSupportsSms() {
return supportsSms;
}
public boolean isInactive() {
return inactive;
}
public void setInactive(boolean inactive) {
this.inactive = inactive;
}
public String toString() {
return new Gson().toJson(this);
}
@Override
public boolean equals(Object other) {
if (other == null) return false;
if (!(other instanceof ClientContact)) return false;
ClientContact that = (ClientContact)other;
return
Arrays.equals(this.token, that.token) &&
this.supportsSms == that.supportsSms &&
this.inactive == that.inactive &&
(this.relay == null ? (that.relay == null) : this.relay.equals(that.relay));
}
public int hashCode() {
return Arrays.hashCode(this.token);
}
}

View File

@ -0,0 +1,33 @@
/**
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotNull;
import java.util.List;
public class ClientContactTokens {
@NotNull
@JsonProperty
private List<String> contacts;
public List<String> getContacts() {
return contacts;
}
}

View File

@ -0,0 +1,41 @@
/**
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.LinkedList;
import java.util.List;
public class ClientContacts {
@JsonProperty
private List<ClientContact> contacts;
public ClientContacts(List<ClientContact> contacts) {
if (contacts != null) this.contacts = contacts;
else this.contacts = new LinkedList<>();
}
public ClientContacts() {
this.contacts = new LinkedList<>();
}
public List<ClientContact> getContacts() {
return contacts;
}
}

View File

@ -0,0 +1,115 @@
/**
* 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.entities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.Util;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class EncryptedOutgoingMessage {
private final Logger logger = LoggerFactory.getLogger(EncryptedOutgoingMessage.class);
private static final byte[] VERSION = new byte[]{0x01};
private static final int CIPHER_KEY_SIZE = 32;
private static final int MAC_KEY_SIZE = 20;
private static final int MAC_SIZE = 10;
private final OutgoingMessageSignal outgoingMessage;
private final String signalingKey;
public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage,
String signalingKey)
{
this.outgoingMessage = outgoingMessage;
this.signalingKey = signalingKey;
}
public String serialize() throws IOException {
byte[] plaintext = outgoingMessage.toByteArray();
SecretKeySpec cipherKey = getCipherKey (signalingKey);
SecretKeySpec macKey = getMacKey(signalingKey);
byte[] ciphertext = getCiphertext(plaintext, cipherKey, macKey);
return Base64.encodeBytes(ciphertext);
}
private byte[] getCiphertext(byte[] plaintext, SecretKeySpec cipherKey, SecretKeySpec macKey)
throws IOException
{
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, cipherKey);
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(macKey);
hmac.update(VERSION);
byte[] ivBytes = cipher.getIV();
hmac.update(ivBytes);
byte[] ciphertext = cipher.doFinal(plaintext);
byte[] mac = hmac.doFinal(ciphertext);
byte[] truncatedMac = new byte[MAC_SIZE];
System.arraycopy(mac, 0, truncatedMac, 0, truncatedMac.length);
return Util.combine(VERSION, ivBytes, ciphertext, truncatedMac);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {
logger.warn("Invalid Key", e);
throw new IOException("Invalid key!");
}
}
private SecretKeySpec getCipherKey(String signalingKey) throws IOException {
byte[] signalingKeyBytes = Base64.decode(signalingKey);
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
if (signalingKeyBytes.length < CIPHER_KEY_SIZE)
throw new IOException("Signaling key too short!");
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
return new SecretKeySpec(cipherKey, "AES");
}
private SecretKeySpec getMacKey(String signalingKey) throws IOException {
byte[] signalingKeyBytes = Base64.decode(signalingKey);
byte[] macKey = new byte[MAC_KEY_SIZE];
if (signalingKeyBytes.length < CIPHER_KEY_SIZE + MAC_KEY_SIZE)
throw new IOException(("Signaling key too short!"));
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
return new SecretKeySpec(macKey, "HmacSHA256");
}
}

View File

@ -0,0 +1,33 @@
/**
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
public class GcmRegistrationId {
@JsonProperty
@NotEmpty
private String gcmRegistrationId;
public String getGcmRegistrationId() {
return gcmRegistrationId;
}
}

View File

@ -0,0 +1,56 @@
/**
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
public class IncomingMessage {
@JsonProperty
private int type;
@JsonProperty
@NotEmpty
private String destination;
@JsonProperty
@NotEmpty
private String body;
@JsonProperty
private String relay;
@JsonProperty
private long timestamp;
public String getDestination() {
return destination;
}
public String getBody() {
return body;
}
public int getType() {
return type;
}
public String getRelay() {
return relay;
}
}

View File

@ -0,0 +1,37 @@
/**
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.List;
public class IncomingMessageList {
@JsonProperty
@NotNull
@Valid
private List<IncomingMessage> messages;
public IncomingMessageList() {}
public List<IncomingMessage> getMessages() {
return messages;
}
}

View File

@ -0,0 +1,809 @@
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: OutgoingMessageSignal.proto
package org.whispersystems.textsecuregcm.entities;
public final class MessageProtos {
private MessageProtos() {}
public static void registerAllExtensions(
com.google.protobuf.ExtensionRegistry registry) {
}
public interface OutgoingMessageSignalOrBuilder
extends com.google.protobuf.MessageOrBuilder {
// optional uint32 type = 1;
boolean hasType();
int getType();
// optional string source = 2;
boolean hasSource();
String getSource();
// optional string relay = 3;
boolean hasRelay();
String getRelay();
// repeated string destinations = 4;
java.util.List<String> getDestinationsList();
int getDestinationsCount();
String getDestinations(int index);
// optional uint64 timestamp = 5;
boolean hasTimestamp();
long getTimestamp();
// optional bytes message = 6;
boolean hasMessage();
com.google.protobuf.ByteString getMessage();
}
public static final class OutgoingMessageSignal extends
com.google.protobuf.GeneratedMessage
implements OutgoingMessageSignalOrBuilder {
// Use OutgoingMessageSignal.newBuilder() to construct.
private OutgoingMessageSignal(Builder builder) {
super(builder);
}
private OutgoingMessageSignal(boolean noInit) {}
private static final OutgoingMessageSignal defaultInstance;
public static OutgoingMessageSignal getDefaultInstance() {
return defaultInstance;
}
public OutgoingMessageSignal getDefaultInstanceForType() {
return defaultInstance;
}
public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_descriptor;
}
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
internalGetFieldAccessorTable() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable;
}
private int bitField0_;
// optional uint32 type = 1;
public static final int TYPE_FIELD_NUMBER = 1;
private int type_;
public boolean hasType() {
return ((bitField0_ & 0x00000001) == 0x00000001);
}
public int getType() {
return type_;
}
// optional string source = 2;
public static final int SOURCE_FIELD_NUMBER = 2;
private java.lang.Object source_;
public boolean hasSource() {
return ((bitField0_ & 0x00000002) == 0x00000002);
}
public String getSource() {
java.lang.Object ref = source_;
if (ref instanceof String) {
return (String) ref;
} else {
com.google.protobuf.ByteString bs =
(com.google.protobuf.ByteString) ref;
String s = bs.toStringUtf8();
if (com.google.protobuf.Internal.isValidUtf8(bs)) {
source_ = s;
}
return s;
}
}
private com.google.protobuf.ByteString getSourceBytes() {
java.lang.Object ref = source_;
if (ref instanceof String) {
com.google.protobuf.ByteString b =
com.google.protobuf.ByteString.copyFromUtf8((String) ref);
source_ = b;
return b;
} else {
return (com.google.protobuf.ByteString) ref;
}
}
// optional string relay = 3;
public static final int RELAY_FIELD_NUMBER = 3;
private java.lang.Object relay_;
public boolean hasRelay() {
return ((bitField0_ & 0x00000004) == 0x00000004);
}
public String getRelay() {
java.lang.Object ref = relay_;
if (ref instanceof String) {
return (String) ref;
} else {
com.google.protobuf.ByteString bs =
(com.google.protobuf.ByteString) ref;
String s = bs.toStringUtf8();
if (com.google.protobuf.Internal.isValidUtf8(bs)) {
relay_ = s;
}
return s;
}
}
private com.google.protobuf.ByteString getRelayBytes() {
java.lang.Object ref = relay_;
if (ref instanceof String) {
com.google.protobuf.ByteString b =
com.google.protobuf.ByteString.copyFromUtf8((String) ref);
relay_ = b;
return b;
} else {
return (com.google.protobuf.ByteString) ref;
}
}
// repeated string destinations = 4;
public static final int DESTINATIONS_FIELD_NUMBER = 4;
private com.google.protobuf.LazyStringList destinations_;
public java.util.List<String>
getDestinationsList() {
return destinations_;
}
public int getDestinationsCount() {
return destinations_.size();
}
public String getDestinations(int index) {
return destinations_.get(index);
}
// optional uint64 timestamp = 5;
public static final int TIMESTAMP_FIELD_NUMBER = 5;
private long timestamp_;
public boolean hasTimestamp() {
return ((bitField0_ & 0x00000008) == 0x00000008);
}
public long getTimestamp() {
return timestamp_;
}
// optional bytes message = 6;
public static final int MESSAGE_FIELD_NUMBER = 6;
private com.google.protobuf.ByteString message_;
public boolean hasMessage() {
return ((bitField0_ & 0x00000010) == 0x00000010);
}
public com.google.protobuf.ByteString getMessage() {
return message_;
}
private void initFields() {
type_ = 0;
source_ = "";
relay_ = "";
destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY;
timestamp_ = 0L;
message_ = 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.writeUInt32(1, type_);
}
if (((bitField0_ & 0x00000002) == 0x00000002)) {
output.writeBytes(2, getSourceBytes());
}
if (((bitField0_ & 0x00000004) == 0x00000004)) {
output.writeBytes(3, getRelayBytes());
}
for (int i = 0; i < destinations_.size(); i++) {
output.writeBytes(4, destinations_.getByteString(i));
}
if (((bitField0_ & 0x00000008) == 0x00000008)) {
output.writeUInt64(5, timestamp_);
}
if (((bitField0_ & 0x00000010) == 0x00000010)) {
output.writeBytes(6, message_);
}
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
.computeUInt32Size(1, type_);
}
if (((bitField0_ & 0x00000002) == 0x00000002)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(2, getSourceBytes());
}
if (((bitField0_ & 0x00000004) == 0x00000004)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(3, getRelayBytes());
}
{
int dataSize = 0;
for (int i = 0; i < destinations_.size(); i++) {
dataSize += com.google.protobuf.CodedOutputStream
.computeBytesSizeNoTag(destinations_.getByteString(i));
}
size += dataSize;
size += 1 * getDestinationsList().size();
}
if (((bitField0_ & 0x00000008) == 0x00000008)) {
size += com.google.protobuf.CodedOutputStream
.computeUInt64Size(5, timestamp_);
}
if (((bitField0_ & 0x00000010) == 0x00000010)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(6, message_);
}
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.entities.MessageProtos.OutgoingMessageSignal parseFrom(
com.google.protobuf.ByteString data)
throws com.google.protobuf.InvalidProtocolBufferException {
return newBuilder().mergeFrom(data).buildParsed();
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
com.google.protobuf.ByteString data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return newBuilder().mergeFrom(data, extensionRegistry)
.buildParsed();
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(byte[] data)
throws com.google.protobuf.InvalidProtocolBufferException {
return newBuilder().mergeFrom(data).buildParsed();
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
byte[] data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return newBuilder().mergeFrom(data, extensionRegistry)
.buildParsed();
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(java.io.InputStream input)
throws java.io.IOException {
return newBuilder().mergeFrom(input).buildParsed();
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return newBuilder().mergeFrom(input, extensionRegistry)
.buildParsed();
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseDelimitedFrom(java.io.InputStream input)
throws java.io.IOException {
Builder builder = newBuilder();
if (builder.mergeDelimitedFrom(input)) {
return builder.buildParsed();
} else {
return null;
}
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseDelimitedFrom(
java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
Builder builder = newBuilder();
if (builder.mergeDelimitedFrom(input, extensionRegistry)) {
return builder.buildParsed();
} else {
return null;
}
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
com.google.protobuf.CodedInputStream input)
throws java.io.IOException {
return newBuilder().mergeFrom(input).buildParsed();
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return newBuilder().mergeFrom(input, extensionRegistry)
.buildParsed();
}
public static Builder newBuilder() { return Builder.create(); }
public Builder newBuilderForType() { return newBuilder(); }
public static Builder newBuilder(org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal 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;
}
public static final class Builder extends
com.google.protobuf.GeneratedMessage.Builder<Builder>
implements org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignalOrBuilder {
public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_descriptor;
}
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
internalGetFieldAccessorTable() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable;
}
// Construct using org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.newBuilder()
private Builder() {
maybeForceBuilderInitialization();
}
private Builder(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_ = 0;
bitField0_ = (bitField0_ & ~0x00000001);
source_ = "";
bitField0_ = (bitField0_ & ~0x00000002);
relay_ = "";
bitField0_ = (bitField0_ & ~0x00000004);
destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY;
bitField0_ = (bitField0_ & ~0x00000008);
timestamp_ = 0L;
bitField0_ = (bitField0_ & ~0x00000010);
message_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000020);
return this;
}
public Builder clone() {
return create().mergeFrom(buildPartial());
}
public com.google.protobuf.Descriptors.Descriptor
getDescriptorForType() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDescriptor();
}
public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal getDefaultInstanceForType() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDefaultInstance();
}
public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal build() {
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal result = buildPartial();
if (!result.isInitialized()) {
throw newUninitializedMessageException(result);
}
return result;
}
private org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal buildParsed()
throws com.google.protobuf.InvalidProtocolBufferException {
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal result = buildPartial();
if (!result.isInitialized()) {
throw newUninitializedMessageException(
result).asInvalidProtocolBufferException();
}
return result;
}
public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal buildPartial() {
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal result = new org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal(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.source_ = source_;
if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
to_bitField0_ |= 0x00000004;
}
result.relay_ = relay_;
if (((bitField0_ & 0x00000008) == 0x00000008)) {
destinations_ = new com.google.protobuf.UnmodifiableLazyStringList(
destinations_);
bitField0_ = (bitField0_ & ~0x00000008);
}
result.destinations_ = destinations_;
if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
to_bitField0_ |= 0x00000008;
}
result.timestamp_ = timestamp_;
if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
to_bitField0_ |= 0x00000010;
}
result.message_ = message_;
result.bitField0_ = to_bitField0_;
onBuilt();
return result;
}
public Builder mergeFrom(com.google.protobuf.Message other) {
if (other instanceof org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal) {
return mergeFrom((org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal)other);
} else {
super.mergeFrom(other);
return this;
}
}
public Builder mergeFrom(org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal other) {
if (other == org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDefaultInstance()) return this;
if (other.hasType()) {
setType(other.getType());
}
if (other.hasSource()) {
setSource(other.getSource());
}
if (other.hasRelay()) {
setRelay(other.getRelay());
}
if (!other.destinations_.isEmpty()) {
if (destinations_.isEmpty()) {
destinations_ = other.destinations_;
bitField0_ = (bitField0_ & ~0x00000008);
} else {
ensureDestinationsIsMutable();
destinations_.addAll(other.destinations_);
}
onChanged();
}
if (other.hasTimestamp()) {
setTimestamp(other.getTimestamp());
}
if (other.hasMessage()) {
setMessage(other.getMessage());
}
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 {
com.google.protobuf.UnknownFieldSet.Builder unknownFields =
com.google.protobuf.UnknownFieldSet.newBuilder(
this.getUnknownFields());
while (true) {
int tag = input.readTag();
switch (tag) {
case 0:
this.setUnknownFields(unknownFields.build());
onChanged();
return this;
default: {
if (!parseUnknownField(input, unknownFields,
extensionRegistry, tag)) {
this.setUnknownFields(unknownFields.build());
onChanged();
return this;
}
break;
}
case 8: {
bitField0_ |= 0x00000001;
type_ = input.readUInt32();
break;
}
case 18: {
bitField0_ |= 0x00000002;
source_ = input.readBytes();
break;
}
case 26: {
bitField0_ |= 0x00000004;
relay_ = input.readBytes();
break;
}
case 34: {
ensureDestinationsIsMutable();
destinations_.add(input.readBytes());
break;
}
case 40: {
bitField0_ |= 0x00000010;
timestamp_ = input.readUInt64();
break;
}
case 50: {
bitField0_ |= 0x00000020;
message_ = input.readBytes();
break;
}
}
}
}
private int bitField0_;
// optional uint32 type = 1;
private int type_ ;
public boolean hasType() {
return ((bitField0_ & 0x00000001) == 0x00000001);
}
public int getType() {
return type_;
}
public Builder setType(int value) {
bitField0_ |= 0x00000001;
type_ = value;
onChanged();
return this;
}
public Builder clearType() {
bitField0_ = (bitField0_ & ~0x00000001);
type_ = 0;
onChanged();
return this;
}
// optional string source = 2;
private java.lang.Object source_ = "";
public boolean hasSource() {
return ((bitField0_ & 0x00000002) == 0x00000002);
}
public String getSource() {
java.lang.Object ref = source_;
if (!(ref instanceof String)) {
String s = ((com.google.protobuf.ByteString) ref).toStringUtf8();
source_ = s;
return s;
} else {
return (String) ref;
}
}
public Builder setSource(String value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000002;
source_ = value;
onChanged();
return this;
}
public Builder clearSource() {
bitField0_ = (bitField0_ & ~0x00000002);
source_ = getDefaultInstance().getSource();
onChanged();
return this;
}
void setSource(com.google.protobuf.ByteString value) {
bitField0_ |= 0x00000002;
source_ = value;
onChanged();
}
// optional string relay = 3;
private java.lang.Object relay_ = "";
public boolean hasRelay() {
return ((bitField0_ & 0x00000004) == 0x00000004);
}
public String getRelay() {
java.lang.Object ref = relay_;
if (!(ref instanceof String)) {
String s = ((com.google.protobuf.ByteString) ref).toStringUtf8();
relay_ = s;
return s;
} else {
return (String) ref;
}
}
public Builder setRelay(String value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000004;
relay_ = value;
onChanged();
return this;
}
public Builder clearRelay() {
bitField0_ = (bitField0_ & ~0x00000004);
relay_ = getDefaultInstance().getRelay();
onChanged();
return this;
}
void setRelay(com.google.protobuf.ByteString value) {
bitField0_ |= 0x00000004;
relay_ = value;
onChanged();
}
// repeated string destinations = 4;
private com.google.protobuf.LazyStringList destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY;
private void ensureDestinationsIsMutable() {
if (!((bitField0_ & 0x00000008) == 0x00000008)) {
destinations_ = new com.google.protobuf.LazyStringArrayList(destinations_);
bitField0_ |= 0x00000008;
}
}
public java.util.List<String>
getDestinationsList() {
return java.util.Collections.unmodifiableList(destinations_);
}
public int getDestinationsCount() {
return destinations_.size();
}
public String getDestinations(int index) {
return destinations_.get(index);
}
public Builder setDestinations(
int index, String value) {
if (value == null) {
throw new NullPointerException();
}
ensureDestinationsIsMutable();
destinations_.set(index, value);
onChanged();
return this;
}
public Builder addDestinations(String value) {
if (value == null) {
throw new NullPointerException();
}
ensureDestinationsIsMutable();
destinations_.add(value);
onChanged();
return this;
}
public Builder addAllDestinations(
java.lang.Iterable<String> values) {
ensureDestinationsIsMutable();
super.addAll(values, destinations_);
onChanged();
return this;
}
public Builder clearDestinations() {
destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY;
bitField0_ = (bitField0_ & ~0x00000008);
onChanged();
return this;
}
void addDestinations(com.google.protobuf.ByteString value) {
ensureDestinationsIsMutable();
destinations_.add(value);
onChanged();
}
// optional uint64 timestamp = 5;
private long timestamp_ ;
public boolean hasTimestamp() {
return ((bitField0_ & 0x00000010) == 0x00000010);
}
public long getTimestamp() {
return timestamp_;
}
public Builder setTimestamp(long value) {
bitField0_ |= 0x00000010;
timestamp_ = value;
onChanged();
return this;
}
public Builder clearTimestamp() {
bitField0_ = (bitField0_ & ~0x00000010);
timestamp_ = 0L;
onChanged();
return this;
}
// optional bytes message = 6;
private com.google.protobuf.ByteString message_ = com.google.protobuf.ByteString.EMPTY;
public boolean hasMessage() {
return ((bitField0_ & 0x00000020) == 0x00000020);
}
public com.google.protobuf.ByteString getMessage() {
return message_;
}
public Builder setMessage(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000020;
message_ = value;
onChanged();
return this;
}
public Builder clearMessage() {
bitField0_ = (bitField0_ & ~0x00000020);
message_ = getDefaultInstance().getMessage();
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:textsecure.OutgoingMessageSignal)
}
static {
defaultInstance = new OutgoingMessageSignal(true);
defaultInstance.initFields();
}
// @@protoc_insertion_point(class_scope:textsecure.OutgoingMessageSignal)
}
private static com.google.protobuf.Descriptors.Descriptor
internal_static_textsecure_OutgoingMessageSignal_descriptor;
private static
com.google.protobuf.GeneratedMessage.FieldAccessorTable
internal_static_textsecure_OutgoingMessageSignal_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\033OutgoingMessageSignal.proto\022\ntextsecur" +
"e\"~\n\025OutgoingMessageSignal\022\014\n\004type\030\001 \001(\r" +
"\022\016\n\006source\030\002 \001(\t\022\r\n\005relay\030\003 \001(\t\022\024\n\014desti" +
"nations\030\004 \003(\t\022\021\n\ttimestamp\030\005 \001(\004\022\017\n\007mess" +
"age\030\006 \001(\014B:\n)org.whispersystems.textsecu" +
"regcm.entitiesB\rMessageProtos"
};
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_OutgoingMessageSignal_descriptor =
getDescriptor().getMessageTypes().get(0);
internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_textsecure_OutgoingMessageSignal_descriptor,
new java.lang.String[] { "Type", "Source", "Relay", "Destinations", "Timestamp", "Message", },
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.class,
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.Builder.class);
return null;
}
};
com.google.protobuf.Descriptors.FileDescriptor
.internalBuildGeneratedFileFrom(descriptorData,
new com.google.protobuf.Descriptors.FileDescriptor[] {
}, assigner);
}
// @@protoc_insertion_point(outer_class_scope)
}

View File

@ -0,0 +1,40 @@
/**
* 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.entities;
import java.util.List;
public class MessageResponse {
private List<String> success;
private List<String> failure;
public MessageResponse(List<String> success, List<String> failure) {
this.success = success;
this.failure = failure;
}
public MessageResponse() {}
public List<String> getSuccess() {
return success;
}
public List<String> getFailure() {
return failure;
}
}

View File

@ -0,0 +1,116 @@
/**
* 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.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotNull;
import javax.xml.bind.annotation.XmlTransient;
import java.io.Serializable;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class PreKey {
@JsonIgnore
private long id;
@JsonIgnore
private String number;
@JsonProperty
@NotNull
private long keyId;
@JsonProperty
@NotNull
private String publicKey;
@JsonProperty
@NotNull
private String identityKey;
@JsonProperty
private boolean lastResort;
public PreKey() {}
public PreKey(long id, String number, long keyId,
String publicKey, String identityKey,
boolean lastResort)
{
this.id = id;
this.number = number;
this.keyId = keyId;
this.publicKey = publicKey;
this.identityKey = identityKey;
this.lastResort = lastResort;
}
@XmlTransient
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
@XmlTransient
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public long getKeyId() {
return keyId;
}
public void setKeyId(long keyId) {
this.keyId = keyId;
}
public String getIdentityKey() {
return identityKey;
}
public void setIdentityKey(String identityKey) {
this.identityKey = identityKey;
}
@XmlTransient
public boolean isLastResort() {
return lastResort;
}
public void setLastResort(boolean lastResort) {
this.lastResort = lastResort;
}
}

View File

@ -0,0 +1,44 @@
/**
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.List;
public class PreKeyList {
@JsonProperty
@NotNull
private PreKey lastResortKey;
@JsonProperty
@NotNull
@Valid
private List<PreKey> keys;
public List<PreKey> getKeys() {
return keys;
}
public PreKey getLastResortKey() {
return lastResortKey;
}
}

View File

@ -0,0 +1,55 @@
/**
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.hibernate.validator.constraints.NotEmpty;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import javax.validation.constraints.NotNull;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
public class RelayMessage {
@JsonProperty
@NotEmpty
private String destination;
@JsonProperty
@NotNull
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
private byte[] outgoingMessageSignal;
public RelayMessage() {}
public RelayMessage(String destination, byte[] outgoingMessageSignal) {
this.destination = destination;
this.outgoingMessageSignal = outgoingMessageSignal;
}
public String getDestination() {
return destination;
}
public byte[] getOutgoingMessageSignal() {
return outgoingMessageSignal;
}
}

View File

@ -0,0 +1,212 @@
/**
* 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.federation;
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.bouncycastle.openssl.PEMReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
import org.whispersystems.textsecuregcm.entities.AccountCount;
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.RelayMessage;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.ws.rs.core.MediaType;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
public class FederatedClient {
private final Logger logger = LoggerFactory.getLogger(FederatedClient.class);
private static final String USER_COUNT_PATH = "/v1/federation/user_count";
private static final String USER_TOKENS_PATH = "/v1/federation/user_tokens/%d";
private static final String RELAY_MESSAGE_PATH = "/v1/federation/message";
private static final String PREKEY_PATH = "/v1/federation/key/%s";
private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d";
private final FederatedPeer peer;
private final Client client;
private final String authorizationHeader;
public FederatedClient(String federationName, FederatedPeer peer)
throws IOException
{
try {
this.client = Client.create(getClientConfig(peer));
this.peer = peer;
this.authorizationHeader = getAuthorizationHeader(federationName, peer);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (KeyStoreException | KeyManagementException | CertificateException e) {
throw new IOException(e);
}
}
public URL getSignedAttachmentUri(long attachmentId) throws IOException {
try {
WebResource resource = client.resource(peer.getUrl())
.path(String.format(ATTACHMENT_URI_PATH, attachmentId));
return resource.accept(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
.get(AttachmentUri.class)
.getLocation();
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("Bad URI", e);
throw new IOException(e);
}
}
public PreKey getKey(String destination) {
try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH, destination));
return resource.accept(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
.get(PreKey.class);
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("PreKey", e);
return null;
}
}
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);
return count.getCount();
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("User Count", e);
return 0;
}
}
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);
return contacts.getContacts();
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("User Tokens", e);
return null;
}
}
public void sendMessage(String destination, OutgoingMessageSignal message)
throws IOException, NoSuchUserException
{
try {
WebResource resource = client.resource(peer.getUrl()).path(RELAY_MESSAGE_PATH);
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
.entity(new RelayMessage(destination, message.toByteArray()))
.put(ClientResponse.class);
if (response.getStatus() == 404) {
throw new NoSuchUserException("No remote user: " + destination);
}
if (response.getStatus() != 200 && response.getStatus() != 204) {
throw new IOException("Bad response: " + response.getStatus());
}
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("sendMessage", e);
throw new IOException(e);
}
}
private String getAuthorizationHeader(String federationName, FederatedPeer peer) {
return "Basic " + Base64.encodeBytes((federationName + ":" + peer.getAuthenticationToken()).getBytes());
}
private ClientConfig getClientConfig(FederatedPeer peer)
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, CertificateException
{
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
trustManagerFactory.init(initializeTrustStore(peer.getName(), peer.getCertificate()));
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);
return config;
}
private KeyStore initializeTrustStore(String name, String pemCertificate)
throws CertificateException
{
try {
PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemCertificate.getBytes())));
X509Certificate certificate = (X509Certificate) reader.readObject();
if (certificate == null) {
throw new CertificateException("No certificate found in parsing!");
}
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
keyStore.setCertificateEntry(name, certificate);
return keyStore;
} catch (IOException | KeyStoreException e) {
throw new CertificateException(e);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public String getPeerName() {
return peer.getName();
}
}

View File

@ -0,0 +1,62 @@
/**
* 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.federation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
public class FederatedClientManager {
private final Logger logger = LoggerFactory.getLogger(FederatedClientManager.class);
private final HashMap<String, FederatedClient> clients = new HashMap<>();
public FederatedClientManager(FederationConfiguration federationConfig)
throws IOException
{
List<FederatedPeer> peers = federationConfig.getPeers();
String identity = federationConfig.getName();
if (peers != null) {
for (FederatedPeer peer : peers) {
logger.info("Adding peer: " + peer.getName());
clients.put(peer.getName(), new FederatedClient(identity, peer));
}
}
}
public FederatedClient getClient(String name) throws NoSuchPeerException {
FederatedClient client = clients.get(name);
if (client == null) {
throw new NoSuchPeerException(name);
}
return client;
}
public List<FederatedClient> getClients() {
return new LinkedList<>(clients.values());
}
}

View File

@ -0,0 +1,66 @@
/**
* 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.federation;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.URL;
public class FederatedPeer {
@NotEmpty
@JsonProperty
private String name;
@NotEmpty
@URL
@JsonProperty
private String url;
@NotEmpty
@JsonProperty
private String authenticationToken;
@NotEmpty
@JsonProperty
private String certificate;
public FederatedPeer() {}
public FederatedPeer(String name, String url, String authenticationToken, String certificate) {
this.name = name;
this.url = url;
this.authenticationToken = authenticationToken;
this.certificate = certificate;
}
public String getUrl() {
return url;
}
public String getName() {
return name;
}
public String getAuthenticationToken() {
return authenticationToken;
}
public String getCertificate() {
return certificate;
}
}

View File

@ -0,0 +1,24 @@
/**
* 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.federation;
public class NoSuchPeerException extends Exception {
public NoSuchPeerException(String name) {
super(name);
}
}

View File

@ -0,0 +1,53 @@
/**
* 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.limits;
import java.io.Serializable;
public class LeakyBucket implements Serializable {
private final int bucketSize;
private final double leakRatePerMillis;
private int spaceRemaining;
private long lastUpdateTimeMillis;
public LeakyBucket(int bucketSize, double leakRatePerMillis) {
this.bucketSize = bucketSize;
this.leakRatePerMillis = leakRatePerMillis;
this.spaceRemaining = bucketSize;
this.lastUpdateTimeMillis = System.currentTimeMillis();
}
public boolean add(int amount) {
this.spaceRemaining = getUpdatedSpaceRemaining();
if (this.spaceRemaining >= amount) {
this.spaceRemaining -= amount;
return true;
} else {
return false;
}
}
private int getUpdatedSpaceRemaining() {
long elapsedTime = System.currentTimeMillis() - this.lastUpdateTimeMillis;
return Math.min(this.bucketSize,
(int)Math.floor(this.spaceRemaining + (elapsedTime * this.leakRatePerMillis)));
}
}

View File

@ -0,0 +1,78 @@
/**
* 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.limits;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import net.spy.memcached.MemcachedClient;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import java.util.concurrent.TimeUnit;
public class RateLimiter {
private final Meter meter;
private final MemcachedClient memcachedClient;
private final String name;
private final int bucketSize;
private final double leakRatePerMillis;
public RateLimiter(MemcachedClient memcachedClient, String name,
int bucketSize, double leakRatePerMinute)
{
this.meter = Metrics.newMeter(RateLimiter.class, name, "exceeded", TimeUnit.MINUTES);
this.memcachedClient = memcachedClient;
this.name = name;
this.bucketSize = bucketSize;
this.leakRatePerMillis = leakRatePerMinute / (60.0 * 1000.0);
}
public void validate(String key, int amount) throws RateLimitExceededException {
LeakyBucket bucket = getBucket(key);
if (bucket.add(amount)) {
setBucket(key, bucket);
} else {
meter.mark();
throw new RateLimitExceededException(key + " , " + amount);
}
}
public void validate(String key) throws RateLimitExceededException {
validate(key, 1);
}
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;
}
}
private String getBucketName(String key) {
return LeakyBucket.class.getSimpleName() + name + key;
}
}

View File

@ -0,0 +1,92 @@
/**
* 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.limits;
import net.spy.memcached.MemcachedClient;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
public class RateLimiters {
private final RateLimiter smsDestinationLimiter;
private final RateLimiter voiceDestinationLimiter;
private final RateLimiter verifyLimiter;
private final RateLimiter attachmentLimiter;
private final RateLimiter contactsLimiter;
private final RateLimiter preKeysLimiter;
private final RateLimiter messagesLimiter;
public RateLimiters(RateLimitsConfiguration config, MemcachedClient memcachedClient) {
this.smsDestinationLimiter = new RateLimiter(memcachedClient, "smsDestination",
config.getSmsDestination().getBucketSize(),
config.getSmsDestination().getLeakRatePerMinute());
this.voiceDestinationLimiter = new RateLimiter(memcachedClient, "voxDestination",
config.getVoiceDestination().getBucketSize(),
config.getVoiceDestination().getLeakRatePerMinute());
this.verifyLimiter = new RateLimiter(memcachedClient, "verify",
config.getVerifyNumber().getBucketSize(),
config.getVerifyNumber().getLeakRatePerMinute());
this.attachmentLimiter = new RateLimiter(memcachedClient, "attachmentCreate",
config.getAttachments().getBucketSize(),
config.getAttachments().getLeakRatePerMinute());
this.contactsLimiter = new RateLimiter(memcachedClient, "contactsQuery",
config.getContactQueries().getBucketSize(),
config.getContactQueries().getLeakRatePerMinute());
this.preKeysLimiter = new RateLimiter(memcachedClient, "prekeys",
config.getPreKeys().getBucketSize(),
config.getPreKeys().getLeakRatePerMinute());
this.messagesLimiter = new RateLimiter(memcachedClient, "messages",
config.getMessages().getBucketSize(),
config.getMessages().getLeakRatePerMinute());
}
public RateLimiter getMessagesLimiter() {
return messagesLimiter;
}
public RateLimiter getPreKeysLimiter() {
return preKeysLimiter;
}
public RateLimiter getContactsLimiter() {
return contactsLimiter;
}
public RateLimiter getAttachmentLimiter() {
return this.attachmentLimiter;
}
public RateLimiter getSmsDestinationLimiter() {
return smsDestinationLimiter;
}
public RateLimiter getVoiceDestinationLimiter() {
return voiceDestinationLimiter;
}
public RateLimiter getVerifyLimiter() {
return verifyLimiter;
}
}

View File

@ -0,0 +1,37 @@
/**
* 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.mappers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
@Provider
public class IOExceptionMapper implements ExceptionMapper<IOException> {
private final Logger logger = LoggerFactory.getLogger(IOExceptionMapper.class);
@Override
public Response toResponse(IOException e) {
logger.warn("IOExceptionMapper", e);
return Response.status(503).build();
}
}

View File

@ -0,0 +1,31 @@
/**
* 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.mappers;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class RateLimitExceededExceptionMapper implements ExceptionMapper<RateLimitExceededException> {
@Override
public Response toResponse(RateLimitExceededException e) {
return Response.status(413).build();
}
}

View File

@ -0,0 +1,53 @@
/**
* 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.yammer.metrics.core.HealthCheck;
import com.yammer.metrics.core.HealthCheck.Result;
import net.spy.memcached.MemcachedClient;
import java.security.SecureRandom;
public class MemcacheHealthCheck extends HealthCheck {
private final MemcachedClient client;
public MemcacheHealthCheck(MemcachedClient client) {
super("memcached");
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");
}
return Result.healthy();
}
}

View File

@ -0,0 +1,54 @@
/**
* 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

@ -0,0 +1,54 @@
/**
* 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 org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
import org.whispersystems.textsecuregcm.util.Util;
import java.net.URI;
import java.net.URISyntaxException;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;
public class RedisClientFactory {
private final JedisPool jedisPool;
public RedisClientFactory(RedisConfiguration redisConfig) throws URISyntaxException {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setTestOnBorrow(true);
URI redisURI = new URI(redisConfig.getUrl());
String redisHost = redisURI.getHost();
int redisPort = redisURI.getPort();
String redisPassword = null;
if (!Util.isEmpty(redisURI.getUserInfo())) {
redisPassword = redisURI.getUserInfo().split(":",2)[1];
}
this.jedisPool = new JedisPool(poolConfig, redisHost, redisPort,
Protocol.DEFAULT_TIMEOUT, redisPassword);
}
public JedisPool getRedisClientPool() {
return jedisPool;
}
}

View File

@ -0,0 +1,49 @@
/**
* 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.yammer.metrics.core.HealthCheck;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class RedisHealthCheck extends HealthCheck {
private final JedisPool clientPool;
public RedisHealthCheck(JedisPool clientPool) {
super("redis");
this.clientPool = clientPool;
}
@Override
protected Result check() throws Exception {
Jedis client = clientPool.getResource();
try {
client.set("HEALTH", "test");
if (!"test".equals(client.get("HEALTH"))) {
return Result.unhealthy("fetch failed");
}
return Result.healthy();
} finally {
clientPool.returnResource(client);
}
}
}

View File

@ -0,0 +1,116 @@
/**
* 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.push;
import com.google.common.base.Optional;
import com.notnoop.apns.APNS;
import com.notnoop.apns.ApnsService;
import com.notnoop.exceptions.NetworkIOException;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import org.bouncycastle.openssl.PEMReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
public class APNSender {
private final Meter success = Metrics.newMeter(APNSender.class, "sent", "success", TimeUnit.MINUTES);
private final Meter failure = Metrics.newMeter(APNSender.class, "sent", "failure", TimeUnit.MINUTES);
private final Logger logger = LoggerFactory.getLogger(APNSender.class);
private static final String MESSAGE_BODY = "m";
private final Optional<ApnsService> apnService;
public APNSender(String apnCertificate, String apnKey)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
{
if (!Util.isEmpty(apnCertificate) && !Util.isEmpty(apnKey)) {
byte[] keyStore = initializeKeyStore(apnCertificate, apnKey);
this.apnService = Optional.of(APNS.newService()
.withCert(new ByteArrayInputStream(keyStore), "insecure")
.withSandboxDestination().build());
} else {
this.apnService = Optional.absent();
}
}
public void sendMessage(String registrationId, EncryptedOutgoingMessage message)
throws IOException
{
try {
if (!apnService.isPresent()) {
failure.mark();
throw new IOException("APN access not configured!");
}
String payload = APNS.newPayload()
.alertBody("Message!")
.customField(MESSAGE_BODY, message.serialize())
.build();
logger.debug("APN Payload: " + payload);
apnService.get().push(registrationId, payload);
success.mark();
} catch (MalformedURLException mue) {
throw new AssertionError(mue);
} catch (NetworkIOException nioe) {
logger.warn("Network Error", nioe);
failure.mark();
throw new IOException("Error sending APN");
}
}
private static byte[] initializeKeyStore(String pemCertificate, String pemKey)
throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException
{
PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemCertificate.getBytes())));
X509Certificate certificate = (X509Certificate) reader.readObject();
Certificate[] certificateChain = {certificate};
reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemKey.getBytes())));
KeyPair keyPair = (KeyPair) reader.readObject();
KeyStore keyStore = KeyStore.getInstance("pkcs12");
keyStore.load(null);
keyStore.setEntry("apn",
new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), certificateChain),
new KeyStore.PasswordProtection("insecure".toCharArray()));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
keyStore.store(baos, "insecure".toCharArray());
return baos.toByteArray();
}
}

View File

@ -0,0 +1,63 @@
/**
* 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.push;
import com.google.android.gcm.server.Constants;
import com.google.android.gcm.server.Message;
import com.google.android.gcm.server.Result;
import com.google.android.gcm.server.Sender;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class GCMSender {
private final Meter success = Metrics.newMeter(GCMSender.class, "sent", "success", TimeUnit.MINUTES);
private final Meter failure = Metrics.newMeter(GCMSender.class, "sent", "failure", TimeUnit.MINUTES);
private final Sender sender;
public GCMSender(String apiKey) {
this.sender = new Sender(apiKey);
}
public String sendMessage(String gcmRegistrationId, EncryptedOutgoingMessage outgoingMessage)
throws IOException, NoSuchUserException
{
Message gcmMessage = new Message.Builder().addData("type", "message")
.addData("message", outgoingMessage.serialize())
.build();
Result result = sender.send(gcmMessage, gcmRegistrationId, 5);
if (result.getMessageId() != null) {
success.mark();
return result.getCanonicalRegistrationId();
} else {
failure.mark();
if (result.getErrorCodeName().equals(Constants.ERROR_NOT_REGISTERED)) {
throw new NoSuchUserException("User no longer registered with GCM.");
} else {
throw new IOException("GCM Failed: " + result.getErrorCodeName());
}
}
}
}

View File

@ -0,0 +1,103 @@
/**
* 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.push;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import java.io.IOException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
public class PushSender {
private final Logger logger = LoggerFactory.getLogger(PushSender.class);
private final AccountsManager accounts;
private final DirectoryManager directory;
private final GCMSender gcmSender;
private final APNSender apnSender;
public PushSender(GcmConfiguration gcmConfiguration,
ApnConfiguration apnConfiguration,
AccountsManager accounts,
DirectoryManager directory)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
{
this.accounts = accounts;
this.directory = directory;
this.gcmSender = new GCMSender(gcmConfiguration.getApiKey());
this.apnSender = new APNSender(apnConfiguration.getCertificate(), apnConfiguration.getKey());
}
public void sendMessage(String destination, MessageProtos.OutgoingMessageSignal outgoingMessage)
throws IOException, NoSuchUserException
{
Optional<Account> account = accounts.get(destination);
if (!account.isPresent()) {
directory.remove(destination);
throw new NoSuchUserException("No such local destination: " + destination);
}
String signalingKey = account.get().getSignalingKey();
EncryptedOutgoingMessage message = new EncryptedOutgoingMessage(outgoingMessage, signalingKey);
if (account.get().getGcmRegistrationId() != null) sendGcmMessage(account.get(), message);
else if (account.get().getApnRegistrationId() != null) sendApnMessage(account.get(), message);
else throw new NoSuchUserException("No push identifier!");
}
private void sendGcmMessage(Account account, EncryptedOutgoingMessage outgoingMessage)
throws IOException, NoSuchUserException
{
try {
String canonicalId = gcmSender.sendMessage(account.getGcmRegistrationId(),
outgoingMessage);
if (canonicalId != null) {
account.setGcmRegistrationId(canonicalId);
accounts.update(account);
}
} catch (NoSuchUserException e) {
logger.debug("No Such User", e);
account.setGcmRegistrationId(null);
accounts.update(account);
throw new NoSuchUserException("User no longer exists in GCM.");
}
}
private void sendApnMessage(Account account, EncryptedOutgoingMessage outgoingMessage)
throws IOException
{
apnSender.sendMessage(account.getApnRegistrationId(), outgoingMessage);
}
}

View File

@ -0,0 +1,92 @@
/**
* 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.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
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 java.util.concurrent.TimeUnit;
import org.whispersystems.textsecuregcm.sms.SenderFactory.VoxSender;
import org.whispersystems.textsecuregcm.sms.SenderFactory.SmsSender;
public class NexmoSmsSender implements SmsSender, VoxSender {
private final Meter smsMeter = Metrics.newMeter(NexmoSmsSender.class, "sms", "delivered", TimeUnit.MINUTES);
private final Meter voxMeter = Metrics.newMeter(NexmoSmsSender.class, "vox", "delivered", TimeUnit.MINUTES);
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();
}
@Override
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.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();
}
@Override
public void deliverVoxVerification(String destination, String message) throws IOException {
URL url = new URL(String.format(NEXMO_VOX_URL, apiKey, apiSecret, destination,
URLEncoder.encode(VoxSender.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

@ -0,0 +1,70 @@
/**
* 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.google.common.base.Optional;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import java.io.IOException;
public class SenderFactory {
private final TwilioSmsSender twilioSender;
private final Optional<NexmoSmsSender> nexmoSender;
public SenderFactory(TwilioConfiguration twilioConfig, NexmoConfiguration nexmoConfig) {
this.twilioSender = new TwilioSmsSender(twilioConfig);
if (nexmoConfig != null) {
this.nexmoSender = Optional.of(new NexmoSmsSender(nexmoConfig));
} else {
this.nexmoSender = Optional.absent();
}
}
public SmsSender getSmsSender(String number) {
if (nexmoSender.isPresent() && !isTwilioDestination(number)) {
return nexmoSender.get();
} else {
return twilioSender;
}
}
public VoxSender getVoxSender(String number) {
if (nexmoSender.isPresent()) {
return nexmoSender.get();
}
throw new AssertionError("FIX ME!");
}
private boolean isTwilioDestination(String number) {
return number.length() == 12 && number.startsWith("+1");
}
public interface SmsSender {
public static final String VERIFICATION_TEXT = "Your TextSecure verification code: ";
public void deliverSmsVerification(String destination, String verificationCode) throws IOException;
}
public interface VoxSender {
public static final String VERIFICATION_TEXT = "Your TextSecure verification code is: ";
public void deliverVoxVerification(String destination, String verificationCode) throws IOException;
}
}

View File

@ -0,0 +1,81 @@
/**
* 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.sun.jersey.core.util.Base64;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class TwilioSmsSender implements SenderFactory.SmsSender {
private final Meter smsMeter = Metrics.newMeter(TwilioSmsSender.class, "sms", "delivered", TimeUnit.MINUTES);
private static final String TWILIO_URL = "https://api.twilio.com/2010-04-01/Accounts/%s/SMS/Messages";
private final String accountId;
private final String accountToken;
private final String number;
public TwilioSmsSender(TwilioConfiguration config) {
this.accountId = config.getAccountId();
this.accountToken = config.getAccountToken();
this.number = config.getNumber();
}
@Override
public void deliverSmsVerification(String destination, String verificationCode) throws IOException {
URL url = new URL(String.format(TWILIO_URL, accountId));
URLConnection connection = url.openConnection();
connection.setDoOutput(true);
connection.setRequestProperty("Authorization", getTwilioAuthorizationHeader());
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
Map<String, String> formData = new HashMap<>();
formData.put("From", number);
formData.put("To", destination);
formData.put("Body", VERIFICATION_TEXT + verificationCode);
OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
writer.write(Util.encodeFormParams(formData));
writer.flush();
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
while (reader.readLine() != null) {}
writer.close();
reader.close();
smsMeter.mark();
}
private String getTwilioAuthorizationHeader() {
String encoded = new String(Base64.encode(String.format("%s:%s", accountId, accountToken)));
return "Basic " + encoded.replace("\n", "");
}
}

View File

@ -0,0 +1,109 @@
/**
* 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.storage;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import java.io.Serializable;
public class Account implements Serializable {
public static final int MEMCACHE_VERION = 1;
private long id;
private String number;
private String hashedAuthenticationToken;
private String salt;
private String signalingKey;
private String gcmRegistrationId;
private String apnRegistrationId;
private boolean supportsSms;
public Account() {}
public Account(long id, String number, String hashedAuthenticationToken, String salt,
String signalingKey, String gcmRegistrationId, String apnRegistrationId,
boolean supportsSms)
{
this.id = id;
this.number = number;
this.hashedAuthenticationToken = hashedAuthenticationToken;
this.salt = salt;
this.signalingKey = signalingKey;
this.gcmRegistrationId = gcmRegistrationId;
this.apnRegistrationId = apnRegistrationId;
this.supportsSms = supportsSms;
}
public String getApnRegistrationId() {
return apnRegistrationId;
}
public void setApnRegistrationId(String apnRegistrationId) {
this.apnRegistrationId = apnRegistrationId;
}
public String getGcmRegistrationId() {
return gcmRegistrationId;
}
public void setGcmRegistrationId(String gcmRegistrationId) {
this.gcmRegistrationId = gcmRegistrationId;
}
public void setNumber(String number) {
this.number = number;
}
public String getNumber() {
return number;
}
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
this.hashedAuthenticationToken = credentials.getHashedAuthenticationToken();
this.salt = credentials.getSalt();
}
public AuthenticationCredentials getAuthenticationCredentials() {
return new AuthenticationCredentials(hashedAuthenticationToken, salt);
}
public String getSignalingKey() {
return signalingKey;
}
public void setSignalingKey(String signalingKey) {
this.signalingKey = signalingKey;
}
public boolean getSupportsSms() {
return supportsSms;
}
public void setSupportsSms(boolean supportsSms) {
this.supportsSms = supportsSms;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
}

View File

@ -0,0 +1,133 @@
/**
* 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.storage;
import org.skife.jdbi.v2.SQLStatement;
import org.skife.jdbi.v2.StatementContext;
import org.skife.jdbi.v2.TransactionIsolationLevel;
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.GetGeneratedKeys;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
import org.skife.jdbi.v2.sqlobject.Transaction;
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
import org.skife.jdbi.v2.tweak.ResultSetMapper;
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.Iterator;
import java.util.List;
public abstract class Accounts {
public static final String ID = "id";
public static final String NUMBER = "number";
public static final String AUTH_TOKEN = "auth_token";
public static final String SALT = "salt";
public static final String SIGNALING_KEY = "signaling_key";
public static final String GCM_ID = "gcm_id";
public static final String APN_ID = "apn_id";
public static final String SUPPORTS_SMS = "supports_sms";
@SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + AUTH_TOKEN + ", " +
SALT + ", " + SIGNALING_KEY + ", " + GCM_ID + ", " +
APN_ID + ", " + SUPPORTS_SMS + ") " +
"VALUES (:number, :auth_token, :salt, :signaling_key, :gcm_id, :apn_id, :supports_sms)")
@GetGeneratedKeys
abstract long createStep(@AccountBinder Account account);
@SqlUpdate("DELETE FROM accounts WHERE number = :number")
abstract void removeStep(@Bind("number") String number);
@SqlUpdate("UPDATE accounts SET " + AUTH_TOKEN + " = :auth_token, " + SALT + " = :salt, " +
SIGNALING_KEY + " = :signaling_key, " + GCM_ID + " = :gcm_id, " +
APN_ID + " = :apn_id, " + SUPPORTS_SMS + " = :supports_sms " +
"WHERE " + NUMBER + " = :number")
abstract void update(@AccountBinder Account account);
@Mapper(AccountMapper.class)
@SqlQuery("SELECT * FROM accounts WHERE " + NUMBER + " = :number")
abstract Account get(@Bind("number") String number);
@SqlQuery("SELECT COUNT(*) from accounts")
abstract long getCount();
@Mapper(AccountMapper.class)
@SqlQuery("SELECT * FROM accounts OFFSET :offset LIMIT :limit")
abstract List<Account> getAll(@Bind("offset") int offset, @Bind("limit") int length);
@Mapper(AccountMapper.class)
@SqlQuery("SELECT * FROM accounts")
abstract Iterator<Account> getAll();
@Transaction(TransactionIsolationLevel.REPEATABLE_READ)
public long create(Account account) {
removeStep(account.getNumber());
return createStep(account);
}
public static class AccountMapper implements ResultSetMapper<Account> {
@Override
public Account map(int i, ResultSet resultSet, StatementContext statementContext)
throws SQLException
{
return new Account(resultSet.getLong(ID), resultSet.getString(NUMBER),
resultSet.getString(AUTH_TOKEN), resultSet.getString(SALT),
resultSet.getString(SIGNALING_KEY), resultSet.getString(GCM_ID),
resultSet.getString(APN_ID),
resultSet.getInt(SUPPORTS_SMS) == 1);
}
}
@BindingAnnotation(AccountBinder.AccountBinderFactory.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface AccountBinder {
public static class AccountBinderFactory implements BinderFactory {
@Override
public Binder build(Annotation annotation) {
return new Binder<AccountBinder, Account>() {
@Override
public void bind(SQLStatement<?> sql,
AccountBinder accountBinder,
Account account)
{
sql.bind(ID, account.getId());
sql.bind(NUMBER, account.getNumber());
sql.bind(AUTH_TOKEN, account.getAuthenticationCredentials()
.getHashedAuthenticationToken());
sql.bind(SALT, account.getAuthenticationCredentials().getSalt());
sql.bind(SIGNALING_KEY, account.getSignalingKey());
sql.bind(GCM_ID, account.getGcmRegistrationId());
sql.bind(APN_ID, account.getApnRegistrationId());
sql.bind(SUPPORTS_SMS, account.getSupportsSms() ? 1 : 0);
}
};
}
}
}
}

View File

@ -0,0 +1,108 @@
/**
* 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.storage;
import com.google.common.base.Optional;
import net.spy.memcached.MemcachedClient;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Iterator;
import java.util.List;
public class AccountsManager {
private final Accounts accounts;
private final MemcachedClient memcachedClient;
private final DirectoryManager directory;
public AccountsManager(Accounts accounts,
DirectoryManager directory,
MemcachedClient memcachedClient)
{
this.accounts = accounts;
this.directory = directory;
this.memcachedClient = memcachedClient;
}
public long getCount() {
return accounts.getCount();
}
public List<Account> getAll(int offset, int length) {
return accounts.getAll(offset, length);
}
public Iterator<Account> getAll() {
return accounts.getAll();
}
public void create(Account account) {
long id = accounts.create(account);
account.setId(id);
if (memcachedClient != null) {
memcachedClient.set(getKey(account.getNumber()), 0, account);
}
updateDirectory(account);
}
public void update(Account account) {
if (memcachedClient != null) {
memcachedClient.set(getKey(account.getNumber()), 0, account);
}
accounts.update(account);
updateDirectory(account);
}
public Optional<Account> get(String number) {
Account account = null;
if (memcachedClient != null) {
account = (Account)memcachedClient.get(getKey(number));
}
if (account == null) {
account = accounts.get(number);
if (account != null && memcachedClient != null) {
memcachedClient.set(getKey(number), 0, account);
}
}
if (account != null) return Optional.of(account);
else return Optional.absent();
}
private void updateDirectory(Account account) {
if (account.getGcmRegistrationId() != null || account.getApnRegistrationId() != null) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
directory.add(clientContact);
} else {
directory.remove(account.getNumber());
}
}
private String getKey(String number) {
return Account.class.getSimpleName() + Account.MEMCACHE_VERION + number;
}
}

View File

@ -0,0 +1,164 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.storage;
import com.google.common.base.Optional;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.util.IterablePair;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.LinkedList;
import java.util.List;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;
public class DirectoryManager {
private static final byte[] DIRECTORY_KEY = {'d', 'i', 'r', 'e', 'c', 't', 'o', 'r', 'y'};
private final JedisPool redisPool;
public DirectoryManager(JedisPool redisPool) {
this.redisPool = redisPool;
}
public void remove(String number) {
remove(Util.getContactToken(number));
}
public void remove(BatchOperationHandle handle, String number) {
remove(handle, Util.getContactToken(number));
}
public void remove(byte[] token) {
Jedis jedis = redisPool.getResource();
jedis.hdel(DIRECTORY_KEY, token);
redisPool.returnResource(jedis);
}
public void remove(BatchOperationHandle handle, byte[] token) {
Pipeline pipeline = handle.pipeline;
pipeline.hdel(DIRECTORY_KEY, token);
}
public void add(ClientContact contact) {
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms());
Jedis jedis = redisPool.getResource();
jedis.hset(DIRECTORY_KEY, contact.getToken(), new Gson().toJson(tokenValue).getBytes());
redisPool.returnResource(jedis);
}
public void add(BatchOperationHandle handle, ClientContact contact) {
Pipeline pipeline = handle.pipeline;
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms());
pipeline.hset(DIRECTORY_KEY, contact.getToken(), new Gson().toJson(tokenValue).getBytes());
}
public Optional<ClientContact> get(byte[] token) {
Jedis jedis = redisPool.getResource();
try {
byte[] result = jedis.hget(DIRECTORY_KEY, token);
if (result == null) {
return Optional.absent();
}
TokenValue tokenValue = new Gson().fromJson(new String(result), TokenValue.class);
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms));
} finally {
redisPool.returnResource(jedis);
}
}
public List<ClientContact> get(List<byte[]> tokens) {
Jedis jedis = redisPool.getResource();
try {
Pipeline pipeline = jedis.pipelined();
List<Response<byte[]>> futures = new LinkedList<>();
List<ClientContact> results = new LinkedList<>();
try {
for (byte[] token : tokens) {
futures.add(pipeline.hget(DIRECTORY_KEY, token));
}
} finally {
pipeline.sync();
}
IterablePair<byte[], Response<byte[]>> lists = new IterablePair<>(tokens, futures);
for (IterablePair.Pair<byte[], Response<byte[]>> pair : lists) {
if (pair.second().get() != null) {
TokenValue tokenValue = new Gson().fromJson(new String(pair.second().get()), TokenValue.class);
ClientContact clientContact = new ClientContact(pair.first(), tokenValue.relay, tokenValue.supportsSms);
results.add(clientContact);
}
}
return results;
} finally {
redisPool.returnResource(jedis);
}
}
public BatchOperationHandle startBatchOperation() {
Jedis jedis = redisPool.getResource();
return new BatchOperationHandle(jedis, jedis.pipelined());
}
public void stopBatchOperation(BatchOperationHandle handle) {
Pipeline pipeline = handle.pipeline;
Jedis jedis = handle.jedis;
pipeline.sync();
redisPool.returnResource(jedis);
}
public static class BatchOperationHandle {
public final Pipeline pipeline;
public final Jedis jedis;
public BatchOperationHandle(Jedis jedis, Pipeline pipeline) {
this.pipeline = pipeline;
this.jedis = jedis;
}
}
private static class TokenValue {
@SerializedName("r")
private String relay;
@SerializedName("s")
private boolean supportsSms;
public TokenValue(String relay, boolean supportsSms) {
this.relay = relay;
this.supportsSms = supportsSms;
}
}
}

View File

@ -0,0 +1,121 @@
/**
* 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.storage;
import org.skife.jdbi.v2.SQLStatement;
import org.skife.jdbi.v2.StatementContext;
import org.skife.jdbi.v2.TransactionIsolationLevel;
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.SqlBatch;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
import org.skife.jdbi.v2.sqlobject.Transaction;
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
import org.skife.jdbi.v2.tweak.ResultSetMapper;
import org.whispersystems.textsecuregcm.entities.PreKey;
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 Keys {
@SqlUpdate("DELETE FROM keys WHERE number = :number")
abstract void removeKeys(@Bind("number") String number);
@SqlUpdate("DELETE FROM keys WHERE id = :id")
abstract void removeKey(@Bind("id") long id);
@SqlBatch("INSERT INTO keys (number, key_id, public_key, identity_key, last_resort) VALUES (:number, :key_id, :public_key, :identity_key, :last_resort)")
abstract void append(@PreKeyBinder List<PreKey> preKeys);
@SqlUpdate("INSERT INTO keys (number, key_id, public_key, identity_key, last_resort) VALUES (:number, :key_id, :public_key, :identity_key, :last_resort)")
abstract void append(@PreKeyBinder PreKey preKey);
@SqlQuery("SELECT * FROM keys WHERE number = :number ORDER BY id LIMIT 1 FOR UPDATE")
@Mapper(PreKeyMapper.class)
abstract PreKey retrieveFirst(@Bind("number") String number);
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
public void store(String number, PreKey lastResortKey, List<PreKey> keys) {
for (PreKey key : keys) {
key.setNumber(number);
}
lastResortKey.setNumber(number);
removeKeys(number);
append(keys);
append(lastResortKey);
}
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
public PreKey get(String number) {
PreKey preKey = retrieveFirst(number);
if (preKey != null && !preKey.isLastResort()) {
removeKey(preKey.getId());
}
return preKey;
}
@BindingAnnotation(PreKeyBinder.PreKeyBinderFactory.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface PreKeyBinder {
public static class PreKeyBinderFactory implements BinderFactory {
@Override
public Binder build(Annotation annotation) {
return new Binder<PreKeyBinder, PreKey>() {
@Override
public void bind(SQLStatement<?> sql, PreKeyBinder accountBinder, PreKey preKey)
{
sql.bind("id", preKey.getId());
sql.bind("number", preKey.getNumber());
sql.bind("key_id", preKey.getKeyId());
sql.bind("public_key", preKey.getPublicKey());
sql.bind("identity_key", preKey.getIdentityKey());
sql.bind("last_resort", preKey.isLastResort() ? 1 : 0);
}
};
}
}
}
public static class PreKeyMapper implements ResultSetMapper<PreKey> {
@Override
public PreKey map(int i, ResultSet resultSet, StatementContext statementContext)
throws SQLException
{
return new PreKey(resultSet.getLong("id"), resultSet.getString("number"),
resultSet.getLong("key_id"), resultSet.getString("public_key"),
resultSet.getString("identity_key"),
resultSet.getInt("last_resort") == 1);
}
}
}

View File

@ -0,0 +1,32 @@
/**
* 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.storage;
import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
public interface PendingAccounts {
@SqlUpdate("WITH upsert AS (UPDATE pending_accounts SET verification_code = :verification_code WHERE number = :number RETURNING *) " +
"INSERT INTO pending_accounts (number, verification_code) SELECT :number, :verification_code WHERE NOT EXISTS (SELECT * FROM upsert)")
void insert(@Bind("number") String number, @Bind("verification_code") String verificationCode);
@SqlQuery("SELECT verification_code FROM pending_accounts WHERE number = :number")
String getCodeForNumber(@Bind("number") String number);
}

View File

@ -0,0 +1,62 @@
/**
* 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.storage;
import com.google.common.base.Optional;
import net.spy.memcached.MemcachedClient;
public class PendingAccountsManager {
private static final String MEMCACHE_PREFIX = "pending_account";
private final PendingAccounts pendingAccounts;
private final MemcachedClient memcachedClient;
public PendingAccountsManager(PendingAccounts pendingAccounts,
MemcachedClient memcachedClient)
{
this.pendingAccounts = pendingAccounts;
this.memcachedClient = memcachedClient;
}
public void store(String number, String code) {
if (memcachedClient != null) {
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
}
pendingAccounts.insert(number, code);
}
public Optional<String> getCodeForNumber(String number) {
String code = null;
if (memcachedClient != null) {
code = (String)memcachedClient.get(MEMCACHE_PREFIX + number);
}
if (code == null) {
code = pendingAccounts.getCodeForNumber(number);
if (code != null && memcachedClient != null) {
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
}
}
if (code != null) return Optional.of(code);
else return Optional.absent();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,50 @@
/**
* 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.util;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
public class ByteArrayAdapter {
public static class Serializing extends JsonSerializer<byte[]> {
@Override
public void serialize(byte[] bytes, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException, JsonProcessingException
{
jsonGenerator.writeString(Base64.encodeBytesWithoutPadding(bytes));
}
}
public static class Deserializing extends JsonDeserializer<byte[]> {
@Override
public byte[] deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JsonProcessingException
{
return Base64.decodeWithoutPadding(jsonParser.getValueAsString());
}
}
}

View File

@ -0,0 +1,180 @@
/**
* 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.util;
public class Conversions {
public static byte intsToByteHighAndLow(int highValue, int lowValue) {
return (byte)((highValue << 4 | lowValue) & 0xFF);
}
public static int highBitsToInt(byte value) {
return (value & 0xFF) >> 4;
}
public static int lowBitsToInt(byte value) {
return (value & 0xF);
}
public static int highBitsToMedium(int value) {
return (value >> 12);
}
public static int lowBitsToMedium(int value) {
return (value & 0xFFF);
}
public static byte[] shortToByteArray(int value) {
byte[] bytes = new byte[2];
shortToByteArray(bytes, 0, value);
return bytes;
}
public static int shortToByteArray(byte[] bytes, int offset, int value) {
bytes[offset+1] = (byte)value;
bytes[offset] = (byte)(value >> 8);
return 2;
}
public static int shortToLittleEndianByteArray(byte[] bytes, int offset, int value) {
bytes[offset] = (byte)value;
bytes[offset+1] = (byte)(value >> 8);
return 2;
}
public static byte[] mediumToByteArray(int value) {
byte[] bytes = new byte[3];
mediumToByteArray(bytes, 0, value);
return bytes;
}
public static int mediumToByteArray(byte[] bytes, int offset, int value) {
bytes[offset + 2] = (byte)value;
bytes[offset + 1] = (byte)(value >> 8);
bytes[offset] = (byte)(value >> 16);
return 3;
}
public static byte[] intToByteArray(int value) {
byte[] bytes = new byte[4];
intToByteArray(bytes, 0, value);
return bytes;
}
public static int intToByteArray(byte[] bytes, int offset, int value) {
bytes[offset + 3] = (byte)value;
bytes[offset + 2] = (byte)(value >> 8);
bytes[offset + 1] = (byte)(value >> 16);
bytes[offset] = (byte)(value >> 24);
return 4;
}
public static int intToLittleEndianByteArray(byte[] bytes, int offset, int value) {
bytes[offset] = (byte)value;
bytes[offset+1] = (byte)(value >> 8);
bytes[offset+2] = (byte)(value >> 16);
bytes[offset+3] = (byte)(value >> 24);
return 4;
}
public static byte[] longToByteArray(long l) {
byte[] bytes = new byte[8];
longToByteArray(bytes, 0, l);
return bytes;
}
public static int longToByteArray(byte[] bytes, int offset, long value) {
bytes[offset + 7] = (byte)value;
bytes[offset + 6] = (byte)(value >> 8);
bytes[offset + 5] = (byte)(value >> 16);
bytes[offset + 4] = (byte)(value >> 24);
bytes[offset + 3] = (byte)(value >> 32);
bytes[offset + 2] = (byte)(value >> 40);
bytes[offset + 1] = (byte)(value >> 48);
bytes[offset] = (byte)(value >> 56);
return 8;
}
public static int longTo4ByteArray(byte[] bytes, int offset, long value) {
bytes[offset + 3] = (byte)value;
bytes[offset + 2] = (byte)(value >> 8);
bytes[offset + 1] = (byte)(value >> 16);
bytes[offset + 0] = (byte)(value >> 24);
return 4;
}
public static int byteArrayToShort(byte[] bytes) {
return byteArrayToShort(bytes, 0);
}
public static int byteArrayToShort(byte[] bytes, int offset) {
return
(bytes[offset] & 0xff) << 8 | (bytes[offset + 1] & 0xff);
}
// The SSL patented 3-byte Value.
public static int byteArrayToMedium(byte[] bytes, int offset) {
return
(bytes[offset] & 0xff) << 16 |
(bytes[offset + 1] & 0xff) << 8 |
(bytes[offset + 2] & 0xff);
}
public static int byteArrayToInt(byte[] bytes) {
return byteArrayToInt(bytes, 0);
}
public static int byteArrayToInt(byte[] bytes, int offset) {
return
(bytes[offset] & 0xff) << 24 |
(bytes[offset + 1] & 0xff) << 16 |
(bytes[offset + 2] & 0xff) << 8 |
(bytes[offset + 3] & 0xff);
}
public static int byteArrayToIntLittleEndian(byte[] bytes, int offset) {
return
(bytes[offset + 3] & 0xff) << 24 |
(bytes[offset + 2] & 0xff) << 16 |
(bytes[offset + 1] & 0xff) << 8 |
(bytes[offset] & 0xff);
}
public static long byteArrayToLong(byte[] bytes) {
return byteArrayToLong(bytes, 0);
}
public static long byteArray4ToLong(byte[] bytes, int offset) {
return
((bytes[offset + 0] & 0xffL) << 24) |
((bytes[offset + 1] & 0xffL) << 16) |
((bytes[offset + 2] & 0xffL) << 8) |
((bytes[offset + 3] & 0xffL));
}
public static long byteArrayToLong(byte[] bytes, int offset) {
return
((bytes[offset] & 0xffL) << 56) |
((bytes[offset + 1] & 0xffL) << 48) |
((bytes[offset + 2] & 0xffL) << 40) |
((bytes[offset + 3] & 0xffL) << 32) |
((bytes[offset + 4] & 0xffL) << 24) |
((bytes[offset + 5] & 0xffL) << 16) |
((bytes[offset + 6] & 0xffL) << 8) |
((bytes[offset + 7] & 0xffL));
}
}

View File

@ -0,0 +1,109 @@
/**
* 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.util;
/**
* Utility for generating hex dumps a la hexl-mode in emacs.
*/
public class Hex {
private final static int HEX_DIGITS_START = 10;
private final static int ASCII_TEXT_START = HEX_DIGITS_START + (16*2 + (16/2));
final static String EOL = System.getProperty("line.separator");
private final static char[] HEX_DIGITS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
public static String toString(byte[] bytes) {
return toString(bytes, 0, bytes.length);
}
public static String toString(byte[] bytes, int offset, int length) {
StringBuffer buf = new StringBuffer();
for (int i = 0; i < length; i++) {
appendHexChar(buf, bytes[offset + i]);
buf.append(' ');
}
return buf.toString();
}
public static String dump(byte[] bytes) {
return dump(bytes, 0, bytes.length);
}
public static String dump(byte[] bytes, int offset, int length) {
StringBuffer buf = new StringBuffer();
int lines = ((length - 1) / 16) + 1;
int lineOffset;
int lineLength;
for (int i = 0; i < lines; i++) {
lineOffset = (i * 16) + offset;
lineLength = Math.min(16, (length - (i * 16)));
appendDumpLine(buf, i, bytes, lineOffset, lineLength);
buf.append(EOL);
}
return buf.toString();
}
private static void appendDumpLine(StringBuffer buf, int line,
byte[] bytes, int lineOffset,
int lineLength)
{
buf.append(HEX_DIGITS[(line >> 28) & 0xf]);
buf.append(HEX_DIGITS[(line >> 24) & 0xf]);
buf.append(HEX_DIGITS[(line >> 20) & 0xf]);
buf.append(HEX_DIGITS[(line >> 16) & 0xf]);
buf.append(HEX_DIGITS[(line >> 12) & 0xf]);
buf.append(HEX_DIGITS[(line >> 8) & 0xf]);
buf.append(HEX_DIGITS[(line >> 4) & 0xf]);
buf.append(HEX_DIGITS[(line ) & 0xf]);
buf.append(": ");
for (int i = 0; i < 16; i++) {
int idx = i + lineOffset;
if (i < lineLength) {
int b = bytes[idx];
appendHexChar(buf, b);
} else {
buf.append(" ");
}
if ((i % 2) == 1) {
buf.append(' ');
}
}
for (int i = 0; i < 16 && i < lineLength; i++) {
int idx = i + lineOffset;
int b = bytes[idx];
if (b >= 0x20 && b <= 0x7e) {
buf.append((char)b);
} else {
buf.append('.');
}
}
}
private static void appendHexChar(StringBuffer buf, int b) {
buf.append(HEX_DIGITS[(b >> 4) & 0xf]);
buf.append(HEX_DIGITS[b & 0xf]);
}
}

View File

@ -0,0 +1,77 @@
/**
* 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.util;
import java.util.Iterator;
import java.util.List;
public class IterablePair <T1, T2> implements Iterable<IterablePair.Pair<T1,T2>> {
private final List<T1> first;
private final List<T2> second;
public IterablePair(List<T1> first, List<T2> second) {
this.first = first;
this.second = second;
}
@Override
public Iterator<Pair<T1, T2>> iterator(){
return new ParallelIterator<>( first.iterator(), second.iterator() );
}
public static class Pair<T1, T2> {
private final T1 v1;
private final T2 v2;
Pair(T1 v1, T2 v2) {
this.v1 = v1;
this.v2 = v2;
}
public T1 first(){
return v1;
}
public T2 second(){
return v2;
}
}
public static class ParallelIterator <T1, T2> implements Iterator<Pair<T1, T2>> {
private final Iterator<T1> it1;
private final Iterator<T2> it2;
public ParallelIterator(Iterator<T1> it1, Iterator<T2> it2) {
this.it1 = it1; this.it2 = it2;
}
@Override
public boolean hasNext() { return it1.hasNext() && it2.hasNext(); }
@Override
public Pair<T1, T2> next() {
return new Pair<>(it1.next(), it2.next());
}
@Override
public void remove(){
it1.remove();
it2.remove();
}
}
}

View File

@ -0,0 +1,52 @@
/**
* 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.util;
import com.amazonaws.HttpMethod;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
import java.net.URL;
import java.util.Date;
public class UrlSigner {
private static final long DURATION = 60 * 60 * 1000;
private final AWSCredentials credentials;
private final String bucket;
public UrlSigner(S3Configuration config) {
this.credentials = new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret());
this.bucket = config.getAttachmentsBucket();
}
public URL getPreSignedUrl(long attachmentId, HttpMethod method) {
AmazonS3 client = new AmazonS3Client(credentials);
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, String.valueOf(attachmentId), method);
request.setExpiration(new Date(System.currentTimeMillis() + DURATION));
request.setContentType("application/octet-stream");
return client.generatePresignedUrl(request);
}
}

View File

@ -0,0 +1,86 @@
/**
* 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.util;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
public class Util {
public static byte[] getContactToken(String number) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA1");
byte[] result = digest.digest(number.getBytes());
byte[] truncated = Util.truncate(result, 10);
return truncated;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public static String getEncodedContactToken(String number) {
return Base64.encodeBytesWithoutPadding(getContactToken(number));
}
public static boolean isValidNumber(String number) {
return number.matches("^\\+[0-9]{10,}");
}
public static String encodeFormParams(Map<String, String> params) {
try {
StringBuffer buffer = new StringBuffer();
for (String key : params.keySet()) {
buffer.append(String.format("%s=%s",
URLEncoder.encode(key, "UTF-8"),
URLEncoder.encode(params.get(key), "UTF-8")));
buffer.append("&");
}
buffer.deleteCharAt(buffer.length()-1);
return buffer.toString();
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
public static boolean isEmpty(String param) {
return param == null || param.length() == 0;
}
public static byte[] combine(byte[] one, byte[] two, byte[] three, byte[] four) {
byte[] combined = new byte[one.length + two.length + three.length + four.length];
System.arraycopy(one, 0, combined, 0, one.length);
System.arraycopy(two, 0, combined, one.length, two.length);
System.arraycopy(three, 0, combined, one.length + two.length, three.length);
System.arraycopy(four, 0, combined, one.length + two.length + three.length, four.length);
return combined;
}
public static byte[] truncate(byte[] element, int length) {
byte[] result = new byte[length];
System.arraycopy(element, 0, result, 0, result.length);
return result;
}
}

View File

@ -0,0 +1,57 @@
/**
* 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.util;
public class VerificationCode {
private String verificationCode;
private String verificationCodeDisplay;
private String verificationCodeSpeech;
public VerificationCode(int verificationCode) {
this.verificationCode = verificationCode + "";
this.verificationCodeDisplay = this.verificationCode.substring(0, 3) + "-" +
this.verificationCode.substring(3, 6);
this.verificationCodeSpeech = delimit(verificationCode + "");
}
public String getVerificationCode() {
return verificationCode;
}
public String getVerificationCodeDisplay() {
return verificationCodeDisplay;
}
public String getVerificationCodeSpeech() {
return verificationCodeSpeech;
}
private String delimit(String code) {
String delimited = "";
for (int i=0;i<code.length();i++) {
delimited += code.charAt(i);
if (i != code.length() - 1)
delimited += ',';
}
return delimited;
}
}

View File

@ -0,0 +1,76 @@
/**
* 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.workers;
import com.yammer.dropwizard.cli.ConfiguredCommand;
import com.yammer.dropwizard.config.Bootstrap;
import com.yammer.dropwizard.db.DatabaseConfiguration;
import com.yammer.dropwizard.jdbi.ImmutableListContainerFactory;
import com.yammer.dropwizard.jdbi.ImmutableSetContainerFactory;
import com.yammer.dropwizard.jdbi.OptionalContainerFactory;
import com.yammer.dropwizard.jdbi.args.OptionalArgumentFactory;
import net.sourceforge.argparse4j.inf.Namespace;
import net.spy.memcached.MemcachedClient;
import org.skife.jdbi.v2.DBI;
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory;
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import redis.clients.jedis.JedisPool;
public class DirectoryCommand extends ConfiguredCommand<WhisperServerConfiguration> {
public DirectoryCommand() {
super("directory", "Update directory from DB and peers.");
}
@Override
protected void run(Bootstrap<WhisperServerConfiguration> bootstrap,
Namespace namespace,
WhisperServerConfiguration config)
throws Exception
{
try {
DatabaseConfiguration dbConfig = config.getDatabaseConfiguration();
DBI dbi = new DBI(dbConfig.getUrl(), dbConfig.getUser(), dbConfig.getPassword());
dbi.registerArgumentFactory(new OptionalArgumentFactory(dbConfig.getDriverClass()));
dbi.registerContainerFactory(new ImmutableListContainerFactory());
dbi.registerContainerFactory(new ImmutableSetContainerFactory());
dbi.registerContainerFactory(new OptionalContainerFactory());
Accounts accounts = dbi.onDemand(Accounts.class);
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool();
DirectoryManager directory = new DirectoryManager(redisClient);
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
DirectoryUpdater update = new DirectoryUpdater(accountsManager, federatedClientManager, directory);
update.updateFromLocalDatabase();
update.updateFromPeers();
} finally {
Thread.sleep(3000);
System.exit(0);
}
}
}

View File

@ -0,0 +1,126 @@
/**
* 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.workers;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.federation.FederatedClient;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Iterator;
import java.util.List;
public class DirectoryUpdater {
private final Logger logger = LoggerFactory.getLogger(DirectoryUpdater.class);
private final AccountsManager accountsManager;
private final FederatedClientManager federatedClientManager;
private final DirectoryManager directory;
public DirectoryUpdater(AccountsManager accountsManager,
FederatedClientManager federatedClientManager,
DirectoryManager directory)
{
this.accountsManager = accountsManager;
this.federatedClientManager = federatedClientManager;
this.directory = directory;
}
public void updateFromLocalDatabase() {
BatchOperationHandle batchOperation = directory.startBatchOperation();
try {
Iterator<Account> accounts = accountsManager.getAll();
if (accounts == null)
return;
while (accounts.hasNext()) {
Account account = accounts.next();
if (account.getApnRegistrationId() != null || account.getGcmRegistrationId() != null) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
directory.add(batchOperation, clientContact);
logger.debug("Adding local token: " + Base64.encodeBytesWithoutPadding(token));
} else {
directory.remove(batchOperation, account.getNumber());
}
}
} finally {
directory.stopBatchOperation(batchOperation);
}
logger.info("Local directory is updated.");
}
public void updateFromPeers() {
logger.info("Updating peer directories.");
List<FederatedClient> clients = federatedClientManager.getClients();
for (FederatedClient client : clients) {
logger.info("Updating directory from peer: " + client.getPeerName());
BatchOperationHandle handle = directory.startBatchOperation();
try {
int userCount = client.getUserCount();
int retrieved = 0;
logger.info("Remote peer user count: " + userCount);
while (retrieved < userCount) {
List<ClientContact> clientContacts = client.getUserTokens(retrieved);
if (clientContacts == null)
break;
for (ClientContact clientContact : clientContacts) {
clientContact.setRelay(client.getPeerName());
Optional<ClientContact> existing = directory.get(clientContact.getToken());
if (!clientContact.isInactive() && (!existing.isPresent() || existing.get().getRelay().equals(client.getPeerName()))) {
directory.add(handle, clientContact);
} else {
if (existing != null && client.getPeerName().equals(existing.get().getRelay())) {
directory.remove(clientContact.getToken());
}
}
}
retrieved += clientContacts.size();
}
logger.info("Update from peer complete.");
} finally {
directory.stopBatchOperation(handle);
}
}
logger.info("Update from peer directories complete.");
}
}

View File

@ -0,0 +1,8 @@
_ _ _ _ _____
| | | | | (_) / ___|
| | | | |__ _ ___ _ __ ___ _ __\ `--. ___ _ ____ _____ _ __
| |/\| | '_ \| / __| '_ \ / _ \ '__|`--. \/ _ \ '__\ \ / / _ \ '__|
\ /\ / | | | \__ \ |_) | __/ | /\__/ / __/ | \ V / __/ |
\/ \/|_| |_|_|___/ .__/ \___|_| \____/ \___|_| \_/ \___|_|
| |
|_|

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd">
<changeSet id="1" author="moxie">
<createTable tableName="accounts">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="number" type="varchar(255)">
<constraints unique="true" nullable="false"/>
</column>
<column name="auth_token" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="salt" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="signaling_key" type="varchar(255)"/>
<column name="gcm_id" type="text"/>
<column name="apn_id" type="text"/>
<column name="supports_sms" type="smallint" defaultValue="0"/>
</createTable>
<createTable tableName="pending_accounts">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="number" type="varchar(255)">
<constraints unique="true" nullable="false"/>
</column>
<column name="verification_code" type="varchar(255)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="keys">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="number" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="key_id" type="bigint">
<constraints nullable="false"/>
</column>
<column name="public_key" type="text">
<constraints nullable="false"/>
</column>
<column name="identity_key" type="text">
<constraints nullable="false"/>
</column>
<column name="last_resort" type="smallint" defaultValue="0"/>
</createTable>
<createIndex tableName="keys" indexName="keys_number_index">
<column name="number"/>
</createIndex>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,11 @@
package org.whispersystems.textsecuregcm.tests;
/**
* Created with IntelliJ IDEA.
* User: moxie
* Date: 10/28/13
* Time: 12:53 PM
* To change this template use File | Settings | File Templates.
*/
public class BaseTest {
}

View File

@ -0,0 +1,91 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.base.Optional;
import com.sun.jersey.api.client.ClientResponse;
import com.yammer.dropwizard.testing.ResourceTest;
import org.junit.Test;
import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.sms.SenderFactory;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import javax.ws.rs.core.MediaType;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*;
public class AccountControllerTest extends ResourceTest {
private static final String SENDER = "+14152222222";
private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class);
private AccountsManager accountsManager = mock(AccountsManager.class );
private RateLimiters rateLimiters = mock(RateLimiters.class );
private RateLimiter rateLimiter = mock(RateLimiter.class );
private TwilioSmsSender twilioSmsSender = mock(TwilioSmsSender.class );
private SenderFactory senderFactory = mock(SenderFactory.class );
@Override
protected void setUpResources() throws Exception {
addProvider(AuthHelper.getAuthenticator());
when(rateLimiters.getSmsDestinationLimiter()).thenReturn(rateLimiter);
when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter);
when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter);
when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of("1234"));
when(senderFactory.getSmsSender(SENDER)).thenReturn(twilioSmsSender);
addResource(new AccountController(pendingAccountsManager,
accountsManager,
rateLimiters,
senderFactory));
}
@Test
public void testSendCode() throws Exception {
ClientResponse response =
client().resource(String.format("/v1/accounts/sms/code/%s", SENDER))
.get(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(200);
verify(twilioSmsSender).deliverSmsVerification(eq(SENDER), anyString());
}
@Test
public void testVerifyCode() throws Exception {
ClientResponse response =
client().resource(String.format("/v1/accounts/code/%s", "1234"))
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.entity(new AccountAttributes("keykeykeykey", false))
.type(MediaType.APPLICATION_JSON_TYPE)
.put(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(204);
verify(accountsManager).create(isA(Account.class));
}
@Test
public void testVerifyBadCode() throws Exception {
ClientResponse response =
client().resource(String.format("/v1/accounts/code/%s", "1111"))
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.entity(new AccountAttributes("keykeykeykey", false))
.type(MediaType.APPLICATION_JSON_TYPE)
.put(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(403);
verifyNoMoreInteractions(accountsManager);
}
}

View File

@ -0,0 +1,82 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.sun.jersey.api.client.ClientResponse;
import com.yammer.dropwizard.testing.ResourceTest;
import org.junit.Test;
import org.whispersystems.textsecuregcm.controllers.KeysController;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
public class KeyControllerTest extends ResourceTest {
private final String EXISTS_NUMBER = "+14152222222";
private final String NOT_EXISTS_NUMBER = "+14152222220";
private final PreKey SAMPLE_KEY = new PreKey(1, EXISTS_NUMBER, 1234, "test1", "test2", false);
private final Keys keys = mock(Keys.class);
@Override
protected void setUpResources() {
addProvider(AuthHelper.getAuthenticator());
RateLimiters rateLimiters = mock(RateLimiters.class);
RateLimiter rateLimiter = mock(RateLimiter.class );
when(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter);
when(keys.get(EXISTS_NUMBER)).thenReturn(SAMPLE_KEY);
when(keys.get(NOT_EXISTS_NUMBER)).thenReturn(null);
addResource(new KeysController(rateLimiters, keys, null));
}
@Test
public void validRequestTest() throws Exception {
PreKey result = client().resource(String.format("/v1/keys/%s", EXISTS_NUMBER))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(PreKey.class);
assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
assertThat(result.getIdentityKey()).isEqualTo(SAMPLE_KEY.getIdentityKey());
assertThat(result.getId() == 0);
assertThat(result.getNumber() == null);
verify(keys).get(EXISTS_NUMBER);
}
@Test
public void invalidRequestTest() throws Exception {
ClientResponse response = client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(ClientResponse.class);
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(404);
verify(keys).get(NOT_EXISTS_NUMBER);
}
@Test
public void unauthorizedRequestTest() throws Exception {
ClientResponse response =
client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD))
.get(ClientResponse.class);
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(401);
response =
client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER))
.get(ClientResponse.class);
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(401);
}
}

View File

@ -0,0 +1,44 @@
package org.whispersystems.textsecuregcm.tests.entities;
import org.junit.Test;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.util.Util;
import static com.yammer.dropwizard.testing.JsonHelpers.*;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
public class ClientContactTest {
@Test
public void serializeToJSON() throws Exception {
byte[] token = Util.getContactToken("+14152222222");
ClientContact contact = new ClientContact(token, null, false);
ClientContact contactWithRelay = new ClientContact(token, "whisper", false);
ClientContact contactWithRelaySms = new ClientContact(token, "whisper", true );
assertThat("Basic Contact Serialization works",
asJson(contact),
is(equalTo(jsonFixture("fixtures/contact.json"))));
assertThat("Contact Relay Serialization works",
asJson(contactWithRelay),
is(equalTo(jsonFixture("fixtures/contact.relay.json"))));
assertThat("Contact Relay+SMS Serialization works",
asJson(contactWithRelaySms),
is(equalTo(jsonFixture("fixtures/contact.relay.sms.json"))));
}
@Test
public void deserializeFromJSON() throws Exception {
ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"),
"whisper", true);
assertThat("a ClientContact can be deserialized from JSON",
fromJson(jsonFixture("fixtures/contact.relay.sms.json"), ClientContact.class),
is(contact));
}
}

View File

@ -0,0 +1,34 @@
package org.whispersystems.textsecuregcm.tests.entities;
import org.junit.Test;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.util.Util;
import static com.yammer.dropwizard.testing.JsonHelpers.*;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
public class PreKeyTest {
@Test
public void serializeToJSON() throws Exception {
PreKey preKey = new PreKey(1, "+14152222222", 1234, "test", "identityTest", false);
assertThat("Basic Contact Serialization works",
asJson(preKey),
is(equalTo(jsonFixture("fixtures/prekey.json"))));
}
@Test
public void deserializeFromJSON() throws Exception {
ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"),
"whisper", true);
assertThat("a ClientContact can be deserialized from JSON",
fromJson(jsonFixture("fixtures/contact.relay.sms.json"), ClientContact.class),
is(contact));
}
}

View File

@ -0,0 +1,44 @@
package org.whispersystems.textsecuregcm.tests.util;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
import org.whispersystems.textsecuregcm.auth.MultiBasicAuthProvider;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.util.Base64;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class AuthHelper {
public static final String VALID_NUMBER = "+14150000000";
public static final String VALID_PASSWORD = "foo";
public static final String INVVALID_NUMBER = "+14151111111";
public static final String INVALID_PASSWORD = "bar";
public static MultiBasicAuthProvider<FederatedPeer, Account> getAuthenticator() {
AccountsManager accounts = mock(AccountsManager.class);
Account account = mock(Account.class);
AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
when(credentials.verify("foo")).thenReturn(true);
when(account.getAuthenticationCredentials()).thenReturn(credentials);
when(accounts.get(VALID_NUMBER)).thenReturn(Optional.of(account));
return new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(new FederationConfiguration()),
FederatedPeer.class,
new AccountAuthenticator(accounts),
Account.class, "WhisperServer");
}
public static String getAuthHeader(String number, String password) {
return "Basic " + Base64.encodeBytes((number + ":" + password).getBytes());
}
}

View File

@ -0,0 +1,3 @@
{
"token" : "BQVVHxMt5zAFXA"
}

View File

@ -0,0 +1,4 @@
{
"token" : "BQVVHxMt5zAFXA",
"relay" : "whisper"
}

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