Track voice support on TS server.

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-08-13 11:25:05 -07:00
parent 4c3aae63d3
commit 62d8f635b0
24 changed files with 108 additions and 64 deletions

View File

@ -9,7 +9,7 @@
<groupId>org.whispersystems.textsecure</groupId>
<artifactId>TextSecureServer</artifactId>
<version>0.71</version>
<version>0.75</version>
<properties>
<dropwizard.version>0.9.0-rc3</dropwizard.version>

View File

@ -18,6 +18,8 @@ package org.whispersystems.textsecuregcm;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.graphite.GraphiteReporter;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.google.common.base.Optional;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
@ -148,6 +150,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
{
SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics());
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
environment.getObjectMapper().setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
environment.getObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
DBIFactory dbiFactory = new DBIFactory();
DBI database = dbiFactory.build(environment, config.getDataSourceFactory(), "accountdb");

View File

@ -250,19 +250,17 @@ public class AccountController {
@Timed
@PUT
@Path("/wsc/")
public void setWebSocketChannelSupported(@Auth Account account) {
@Path("/attributes/")
public void setAccountAttributes(@Auth Account account, @Valid AccountAttributes attributes) {
Device device = account.getAuthenticatedDevice().get();
device.setFetchesMessages(true);
accounts.update(account);
}
@Timed
@DELETE
@Path("/wsc/")
public void deleteWebSocketChannel(@Auth Account account) {
Device device = account.getAuthenticatedDevice().get();
device.setFetchesMessages(false);
device.setFetchesMessages(attributes.getFetchesMessages());
device.setName(attributes.getName());
device.setLastSeen(Util.todayInMillis());
device.setVoiceSupported(attributes.getVoice());
device.setRegistrationId(attributes.getRegistrationId());
device.setSignalingKey(attributes.getSignalingKey());
accounts.update(account);
}
@ -283,6 +281,7 @@ public class AccountController {
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
device.setName(accountAttributes.getName());
device.setVoiceSupported(accountAttributes.getVoice());
device.setCreated(System.currentTimeMillis());
device.setLastSeen(Util.todayInMillis());
@ -293,8 +292,6 @@ public class AccountController {
accounts.create(account);
messagesManager.clear(number);
pendingAccounts.remove(number);
logger.debug("Stored device...");
}
@VisibleForTesting protected VerificationCode generateVerificationCode(String number) {

View File

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

View File

@ -21,8 +21,6 @@ import com.google.common.annotations.VisibleForTesting;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Max;
public class AccountAttributes {
@JsonProperty
@ -39,19 +37,23 @@ public class AccountAttributes {
@Length(max = 50, message = "This field must be less than 50 characters")
private String name;
@JsonProperty
private boolean voice;
public AccountAttributes() {}
@VisibleForTesting
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId) {
this(signalingKey, fetchesMessages, registrationId, null);
this(signalingKey, fetchesMessages, registrationId, null, false);
}
@VisibleForTesting
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name) {
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name, boolean voice) {
this.signalingKey = signalingKey;
this.fetchesMessages = fetchesMessages;
this.registrationId = registrationId;
this.name = name;
this.voice = voice;
}
public String getSignalingKey() {
@ -69,4 +71,8 @@ public class AccountAttributes {
public String getName() {
return name;
}
public boolean getVoice() {
return voice;
}
}

View File

@ -32,12 +32,16 @@ public class ClientContact {
@JsonProperty
private byte[] token;
@JsonProperty
private boolean voice;
private String relay;
private boolean inactive;
public ClientContact(byte[] token, String relay) {
public ClientContact(byte[] token, String relay, boolean voice) {
this.token = token;
this.relay = relay;
this.voice = voice;
}
public ClientContact() {}
@ -62,9 +66,13 @@ public class ClientContact {
this.inactive = inactive;
}
// public String toString() {
// return new Gson().toJson(this);
// }
public boolean isVoice() {
return voice;
}
public void setVoice(boolean voice) {
this.voice = voice;
}
@Override
public boolean equals(Object other) {
@ -76,6 +84,7 @@ public class ClientContact {
return
Arrays.equals(this.token, that.token) &&
this.inactive == that.inactive &&
this.voice == that.voice &&
(this.relay == null ? (that.relay == null) : this.relay.equals(that.relay));
}

View File

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

View File

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

View File

@ -82,6 +82,7 @@ public class FeedbackHandler implements Managed, Runnable {
device.get().setGcmId(event.getCanonicalId());
} else {
device.get().setGcmId(null);
device.get().setFetchesMessages(false);
}
accountsManager.update(account.get());
}

View File

@ -71,7 +71,7 @@ public class Account {
}
public void removeDevice(long deviceId) {
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0));
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, false));
}
public Set<Device> getDevices() {
@ -92,6 +92,16 @@ public class Account {
return Optional.absent();
}
public boolean isVoiceSupported() {
for (Device device : devices) {
if (device.isActive() && device.isVoiceSupported()) {
return true;
}
}
return false;
}
public boolean isActive() {
return
getMasterDevice().isPresent() &&

View File

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

View File

@ -100,7 +100,7 @@ public class AccountsManager {
private void updateDirectory(Account account) {
if (account.isActive()) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null);
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported());
directory.add(clientContact);
} else {
directory.remove(account.getNumber());

View File

@ -70,13 +70,16 @@ public class Device {
@JsonProperty
private long created;
@JsonProperty
private boolean voice;
public Device() {}
public Device(long id, String name, String authToken, String salt,
String signalingKey, String gcmId, String apnId,
String voipApnId, boolean fetchesMessages,
int registrationId, SignedPreKey signedPreKey,
long lastSeen, long created)
long lastSeen, long created, boolean voice)
{
this.id = id;
this.name = name;
@ -91,6 +94,7 @@ public class Device {
this.signedPreKey = signedPreKey;
this.lastSeen = lastSeen;
this.created = created;
this.voice = voice;
}
public String getApnId() {
@ -157,6 +161,14 @@ public class Device {
this.name = name;
}
public boolean isVoiceSupported() {
return voice;
}
public void setVoiceSupported(boolean voice) {
this.voice = voice;
}
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
this.authToken = credentials.getHashedAuthenticationToken();
this.salt = credentials.getSalt();

View File

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

View File

@ -26,7 +26,6 @@ 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.io.IOException;
@ -73,7 +72,7 @@ public class DirectoryUpdater {
for (Account account : accounts) {
if (account.isActive()) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null);
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported());
directory.add(batchOperation, clientContact);
contactsAdded++;

View File

@ -5,10 +5,7 @@ import org.skife.jdbi.v2.DBI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.Messages;
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
import java.util.concurrent.TimeUnit;

View File

@ -19,6 +19,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
@ -47,6 +48,7 @@ public class AccountControllerTest {
public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.setMapper(SystemMapper.getMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new AccountController(pendingAccountsManager,
accountsManager,

View File

@ -135,7 +135,7 @@ public class DeviceControllerTest {
.target("/v1/devices/5678901")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, "this is a really long name that is longer than 80 characters"),
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, "this is a really long name that is longer than 80 characters", true),
MediaType.APPLICATION_JSON_TYPE));
assertEquals(response.getStatus(), 422);

View File

@ -79,12 +79,12 @@ public class FederatedControllerTest {
@Before
public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis()));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
}};
Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis(), System.currentTimeMillis()));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis(), System.currentTimeMillis()));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList);

View File

@ -74,13 +74,13 @@ public class MessageControllerTest {
@Before
public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis()));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
}};
Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis()));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis()));
add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis()));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), false));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), false));
add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), false));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList);

View File

@ -53,12 +53,12 @@ public class ReceiptControllerTest {
@Before
public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis()));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
}};
Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis(), System.currentTimeMillis()));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis(), System.currentTimeMillis()));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList);

View File

@ -14,9 +14,9 @@ public class ClientContactTest {
@Test
public void serializeToJSON() throws Exception {
byte[] token = Util.getContactToken("+14152222222");
ClientContact contact = new ClientContact(token, null);
ClientContact contactWithRelay = new ClientContact(token, "whisper");
ClientContact contactWithRelaySms = new ClientContact(token, "whisper");
ClientContact contact = new ClientContact(token, null, false);
ClientContact contactWithRelay = new ClientContact(token, "whisper", false);
ClientContact contactWithRelayVox = new ClientContact(token, "whisper", true);
assertThat("Basic Contact Serialization works",
asJson(contact),
@ -25,12 +25,16 @@ public class ClientContactTest {
assertThat("Contact Relay Serialization works",
asJson(contactWithRelay),
is(equalTo(jsonFixture("fixtures/contact.relay.json"))));
assertThat("Contact Relay Vox Serializaton works",
asJson(contactWithRelayVox),
is(equalTo(jsonFixture("fixtures/contact.relay.voice.json"))));
}
@Test
public void deserializeFromJSON() throws Exception {
ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"),
"whisper");
"whisper", false);
assertThat("a ClientContact can be deserialized from JSON",
fromJson(jsonFixture("fixtures/contact.relay.json"), ClientContact.class),

View File

@ -26,7 +26,7 @@ public class PreKeyTest {
@Test
public void deserializeFromJSONV() throws Exception {
ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"),
"whisper");
"whisper", false);
assertThat("a ClientContact can be deserialized from JSON",
fromJson(jsonFixture("fixtures/contact.relay.json"), ClientContact.class),

View File

@ -0,0 +1,5 @@
{
"token" : "BQVVHxMt5zAFXA",
"voice" : true,
"relay" : "whisper"
}