Mark accounts as inactive if no device has been seen for a year.

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2016-03-11 16:02:55 -08:00
parent c410348278
commit d95ca5f9e4
7 changed files with 194 additions and 109 deletions

View File

@ -225,7 +225,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
if (config.getWebsocketConfiguration().isEnabled()) { if (config.getWebsocketConfiguration().isEnabled()) {
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config, 90000); WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config, 90000);
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator)); webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator));
webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(accountsManager, pushSender, receiptSender, messagesManager, pubSubManager, apnFallbackManager)); webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(accountsManager, pushSender, receiptSender, messagesManager, pubSubManager));
webSocketEnvironment.jersey().register(new KeepAliveController(pubSubManager)); webSocketEnvironment.jersey().register(new KeepAliveController(pubSubManager));
WebSocketEnvironment provisioningEnvironment = new WebSocketEnvironment(environment, config); WebSocketEnvironment provisioningEnvironment = new WebSocketEnvironment(environment, config);

View File

@ -24,6 +24,7 @@ import com.google.common.base.Optional;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
public class Account { public class Account {
@ -105,7 +106,8 @@ public class Account {
public boolean isActive() { public boolean isActive() {
return return
getMasterDevice().isPresent() && getMasterDevice().isPresent() &&
getMasterDevice().get().isActive(); getMasterDevice().get().isActive() &&
getLastSeen() > (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(365));
} }
public long getNextDeviceId() { public long getNextDeviceId() {
@ -147,4 +149,16 @@ public class Account {
public String getIdentityKey() { public String getIdentityKey() {
return identityKey; return identityKey;
} }
public long getLastSeen() {
long lastSeen = 0;
for (Device device : devices) {
if (device.getLastSeen() > lastSeen) {
lastSeen = device.getLastSeen();
}
}
return lastSeen;
}
} }

View File

@ -1,11 +1,10 @@
package org.whispersystems.textsecuregcm.websocket; package org.whispersystems.textsecuregcm.websocket;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender; import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
@ -13,7 +12,6 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PubSubManager; import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubProtos;
import org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage; import org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
@ -24,11 +22,10 @@ import static com.codahale.metrics.MetricRegistry.name;
public class AuthenticatedConnectListener implements WebSocketConnectListener { public class AuthenticatedConnectListener implements WebSocketConnectListener {
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class); private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private static final Histogram durationHistogram = metricRegistry.histogram(name(WebSocketConnection.class, "connected_duration")); private static final Timer durationTimer = metricRegistry.timer(name(WebSocketConnection.class, "connected_duration"));
private final ApnFallbackManager apnFallbackManager;
private final AccountsManager accountsManager; private final AccountsManager accountsManager;
private final PushSender pushSender; private final PushSender pushSender;
private final ReceiptSender receiptSender; private final ReceiptSender receiptSender;
@ -37,26 +34,25 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
public AuthenticatedConnectListener(AccountsManager accountsManager, PushSender pushSender, public AuthenticatedConnectListener(AccountsManager accountsManager, PushSender pushSender,
ReceiptSender receiptSender, MessagesManager messagesManager, ReceiptSender receiptSender, MessagesManager messagesManager,
PubSubManager pubSubManager, ApnFallbackManager apnFallbackManager) PubSubManager pubSubManager)
{ {
this.accountsManager = accountsManager; this.accountsManager = accountsManager;
this.pushSender = pushSender; this.pushSender = pushSender;
this.receiptSender = receiptSender; this.receiptSender = receiptSender;
this.messagesManager = messagesManager; this.messagesManager = messagesManager;
this.pubSubManager = pubSubManager; this.pubSubManager = pubSubManager;
this.apnFallbackManager = apnFallbackManager;
} }
@Override @Override
public void onWebSocketConnect(WebSocketSessionContext context) { public void onWebSocketConnect(WebSocketSessionContext context) {
final Account account = context.getAuthenticated(Account.class); final Account account = context.getAuthenticated(Account.class);
final Device device = account.getAuthenticatedDevice().get(); final Device device = account.getAuthenticatedDevice().get();
final long connectTime = System.currentTimeMillis(); final Timer.Context timer = durationTimer.time();
final WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId()); final WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
final WebSocketConnectionInfo info = new WebSocketConnectionInfo(address); final WebSocketConnectionInfo info = new WebSocketConnectionInfo(address);
final WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender, final WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender,
messagesManager, account, device, messagesManager, account, device,
context.getClient()); context.getClient());
pubSubManager.publish(info, PubSubMessage.newBuilder().setType(PubSubMessage.Type.CONNECTED).build()); pubSubManager.publish(info, PubSubMessage.newBuilder().setType(PubSubMessage.Type.CONNECTED).build());
updateLastSeen(account, device); updateLastSeen(account, device);
@ -66,7 +62,7 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
@Override @Override
public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason) { public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason) {
pubSubManager.unsubscribe(address, connection); pubSubManager.unsubscribe(address, connection);
durationHistogram.update(System.currentTimeMillis() - connectTime); timer.stop();
} }
}); });
} }

View File

@ -76,14 +76,14 @@ public class DirectoryCommand extends EnvironmentCommand<WhisperServerConfigurat
JedisPool redisClient = new RedisClientFactory(configuration.getDirectoryConfiguration().getUrl()).getRedisClientPool(); JedisPool redisClient = new RedisClientFactory(configuration.getDirectoryConfiguration().getUrl()).getRedisClientPool();
DirectoryManager directory = new DirectoryManager(redisClient); DirectoryManager directory = new DirectoryManager(redisClient);
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient); AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
FederatedClientManager federatedClientManager = new FederatedClientManager(environment, // FederatedClientManager federatedClientManager = new FederatedClientManager(environment,
configuration.getJerseyClientConfiguration(), // configuration.getJerseyClientConfiguration(),
configuration.getFederationConfiguration()); // configuration.getFederationConfiguration());
DirectoryUpdater update = new DirectoryUpdater(accountsManager, federatedClientManager, directory); DirectoryUpdater update = new DirectoryUpdater(accountsManager, directory);
update.updateFromLocalDatabase(); update.updateFromLocalDatabase();
update.updateFromPeers(); // update.updateFromPeers();
} catch (Exception ex) { } catch (Exception ex) {
logger.warn("Directory Exception", ex); logger.warn("Directory Exception", ex);
throw new RuntimeException(ex); throw new RuntimeException(ex);

View File

@ -42,15 +42,11 @@ public class DirectoryUpdater {
private final Logger logger = LoggerFactory.getLogger(DirectoryUpdater.class); private final Logger logger = LoggerFactory.getLogger(DirectoryUpdater.class);
private final AccountsManager accountsManager; private final AccountsManager accountsManager;
private final FederatedClientManager federatedClientManager;
private final DirectoryManager directory; private final DirectoryManager directory;
public DirectoryUpdater(AccountsManager accountsManager, public DirectoryUpdater(AccountsManager accountsManager, DirectoryManager directory)
FederatedClientManager federatedClientManager,
DirectoryManager directory)
{ {
this.accountsManager = accountsManager; this.accountsManager = accountsManager;
this.federatedClientManager = federatedClientManager;
this.directory = directory; this.directory = directory;
} }
@ -91,75 +87,75 @@ public class DirectoryUpdater {
logger.info(String.format("Local directory is updated (%d added, %d removed).", contactsAdded, contactsRemoved)); logger.info(String.format("Local directory is updated (%d added, %d removed).", contactsAdded, contactsRemoved));
} }
public void updateFromPeers() { // public void updateFromPeers() {
logger.info("Updating peer directories."); // logger.info("Updating peer directories.");
//
int contactsAdded = 0; // int contactsAdded = 0;
int contactsRemoved = 0; // int contactsRemoved = 0;
List<FederatedClient> clients = federatedClientManager.getClients(); // List<FederatedClient> clients = federatedClientManager.getClients();
//
for (FederatedClient client : clients) { // for (FederatedClient client : clients) {
logger.info("Updating directory from peer: " + client.getPeerName()); // logger.info("Updating directory from peer: " + client.getPeerName());
//
int userCount = client.getUserCount(); // int userCount = client.getUserCount();
int retrieved = 0; // int retrieved = 0;
//
logger.info("Remote peer user count: " + userCount); // logger.info("Remote peer user count: " + userCount);
//
while (retrieved < userCount) { // while (retrieved < userCount) {
logger.info("Retrieving remote tokens..."); // logger.info("Retrieving remote tokens...");
List<ClientContact> remoteContacts = client.getUserTokens(retrieved); // List<ClientContact> remoteContacts = client.getUserTokens(retrieved);
List<PendingClientContact> localContacts = new LinkedList<>(); // List<PendingClientContact> localContacts = new LinkedList<>();
BatchOperationHandle handle = directory.startBatchOperation(); // BatchOperationHandle handle = directory.startBatchOperation();
//
if (remoteContacts == null) { // if (remoteContacts == null) {
logger.info("Remote tokens empty, ending..."); // logger.info("Remote tokens empty, ending...");
break; // break;
} else { // } else {
logger.info("Retrieved " + remoteContacts.size() + " remote tokens..."); // logger.info("Retrieved " + remoteContacts.size() + " remote tokens...");
} // }
//
for (ClientContact remoteContact : remoteContacts) { // for (ClientContact remoteContact : remoteContacts) {
localContacts.add(directory.get(handle, remoteContact.getToken())); // localContacts.add(directory.get(handle, remoteContact.getToken()));
} // }
//
directory.stopBatchOperation(handle); // directory.stopBatchOperation(handle);
//
handle = directory.startBatchOperation(); // handle = directory.startBatchOperation();
Iterator<ClientContact> remoteContactIterator = remoteContacts.iterator(); // Iterator<ClientContact> remoteContactIterator = remoteContacts.iterator();
Iterator<PendingClientContact> localContactIterator = localContacts.iterator(); // Iterator<PendingClientContact> localContactIterator = localContacts.iterator();
//
while (remoteContactIterator.hasNext() && localContactIterator.hasNext()) { // while (remoteContactIterator.hasNext() && localContactIterator.hasNext()) {
try { // try {
ClientContact remoteContact = remoteContactIterator.next(); // ClientContact remoteContact = remoteContactIterator.next();
Optional<ClientContact> localContact = localContactIterator.next().get(); // Optional<ClientContact> localContact = localContactIterator.next().get();
//
remoteContact.setRelay(client.getPeerName()); // remoteContact.setRelay(client.getPeerName());
//
if (!remoteContact.isInactive() && (!localContact.isPresent() || client.getPeerName().equals(localContact.get().getRelay()))) { // if (!remoteContact.isInactive() && (!localContact.isPresent() || client.getPeerName().equals(localContact.get().getRelay()))) {
contactsAdded++; // contactsAdded++;
directory.add(handle, remoteContact); // directory.add(handle, remoteContact);
} else { // } else {
if (localContact.isPresent() && client.getPeerName().equals(localContact.get().getRelay())) { // if (localContact.isPresent() && client.getPeerName().equals(localContact.get().getRelay())) {
contactsRemoved++; // contactsRemoved++;
directory.remove(handle, remoteContact.getToken()); // directory.remove(handle, remoteContact.getToken());
} // }
} // }
} catch (IOException e) { // } catch (IOException e) {
logger.warn("JSON Serialization Failed: ", e); // logger.warn("JSON Serialization Failed: ", e);
} // }
} // }
//
directory.stopBatchOperation(handle); // directory.stopBatchOperation(handle);
//
retrieved += remoteContacts.size(); // retrieved += remoteContacts.size();
logger.info("Processed: " + retrieved + " remote tokens."); // logger.info("Processed: " + retrieved + " remote tokens.");
} // }
//
logger.info("Update from peer complete."); // logger.info("Update from peer complete.");
} // }
//
logger.info("Update from peer directories complete."); // logger.info("Update from peer directories complete.");
logger.info(String.format("Added %d and removed %d remove contacts.", contactsAdded, contactsRemoved)); // logger.info(String.format("Added %d and removed %d remove contacts.", contactsAdded, contactsRemoved));
} // }
} }

View File

@ -0,0 +1,81 @@
package org.whispersystems.textsecuregcm.tests.storage;
import org.junit.Before;
import org.junit.Test;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import java.util.HashSet;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class AccountTest {
private final Device oldMasterDevice = mock(Device.class);
private final Device recentMasterDevice = mock(Device.class);
private final Device agingSecondaryDevice = mock(Device.class);
private final Device recentSecondaryDevice = mock(Device.class);
private final Device oldSecondaryDevice = mock(Device.class);
@Before
public void setup() {
when(oldMasterDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(366));
when(oldMasterDevice.isActive()).thenReturn(true);
when(oldMasterDevice.getId()).thenReturn(Device.MASTER_ID);
when(recentMasterDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1));
when(recentMasterDevice.isActive()).thenReturn(true);
when(recentMasterDevice.getId()).thenReturn(Device.MASTER_ID);
when(agingSecondaryDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31));
when(agingSecondaryDevice.isActive()).thenReturn(false);
when(agingSecondaryDevice.getId()).thenReturn(2L);
when(recentSecondaryDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1));
when(recentSecondaryDevice.isActive()).thenReturn(true);
when(recentSecondaryDevice.getId()).thenReturn(2L);
when(oldSecondaryDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(366));
when(oldSecondaryDevice.isActive()).thenReturn(false);
when(oldSecondaryDevice.getId()).thenReturn(2L);
}
@Test
public void testAccountActive() {
Account recentAccount = new Account("+14152222222", new HashSet<Device>() {{
add(recentMasterDevice);
add(recentSecondaryDevice);
}});
assertTrue(recentAccount.isActive());
Account oldSecondaryAccount = new Account("+14152222222", new HashSet<Device>() {{
add(recentMasterDevice);
add(agingSecondaryDevice);
}});
assertTrue(oldSecondaryAccount.isActive());
Account agingPrimaryAccount = new Account("+14152222222", new HashSet<Device>() {{
add(oldMasterDevice);
add(agingSecondaryDevice);
}});
assertTrue(agingPrimaryAccount.isActive());
}
@Test
public void testAccountInactive() {
Account oldPrimaryAccount = new Account("+14152222222", new HashSet<Device>() {{
add(oldMasterDevice);
add(oldSecondaryDevice);
}});
assertFalse(oldPrimaryAccount.isActive());
}
}

View File

@ -10,7 +10,6 @@ import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender; import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.push.WebsocketSender; import org.whispersystems.textsecuregcm.push.WebsocketSender;
@ -58,13 +57,12 @@ public class WebSocketConnectionTest {
private static final UpgradeRequest upgradeRequest = mock(UpgradeRequest.class ); private static final UpgradeRequest upgradeRequest = mock(UpgradeRequest.class );
private static final PushSender pushSender = mock(PushSender.class); private static final PushSender pushSender = mock(PushSender.class);
private static final ReceiptSender receiptSender = mock(ReceiptSender.class); private static final ReceiptSender receiptSender = mock(ReceiptSender.class);
private static final ApnFallbackManager apnFallbackManager = mock(ApnFallbackManager.class);
@Test @Test
public void testCredentials() throws Exception { public void testCredentials() throws Exception {
MessagesManager storedMessages = mock(MessagesManager.class); MessagesManager storedMessages = mock(MessagesManager.class);
WebSocketAccountAuthenticator webSocketAuthenticator = new WebSocketAccountAuthenticator(accountAuthenticator); WebSocketAccountAuthenticator webSocketAuthenticator = new WebSocketAccountAuthenticator(accountAuthenticator);
AuthenticatedConnectListener connectListener = new AuthenticatedConnectListener(accountsManager, pushSender, receiptSender, storedMessages, pubSubManager, apnFallbackManager); AuthenticatedConnectListener connectListener = new AuthenticatedConnectListener(accountsManager, pushSender, receiptSender, storedMessages, pubSubManager);
WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class); WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class);
when(accountAuthenticator.authenticate(eq(new BasicCredentials(VALID_USER, VALID_PASSWORD)))) when(accountAuthenticator.authenticate(eq(new BasicCredentials(VALID_USER, VALID_PASSWORD))))