Secret sender
This commit is contained in:
parent
8513b6fbd5
commit
7e026a7072
6
pom.xml
6
pom.xml
|
@ -102,12 +102,12 @@
|
|||
<dependency>
|
||||
<groupId>org.whispersystems</groupId>
|
||||
<artifactId>websocket-resources</artifactId>
|
||||
<version>0.5.4</version>
|
||||
<version>0.5.8</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems</groupId>
|
||||
<artifactId>dropwizard-simpleauth</artifactId>
|
||||
<version>0.3.0</version>
|
||||
<artifactId>curve25519-java</artifactId>
|
||||
<version>0.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
|
|
@ -21,22 +21,48 @@ option java_outer_classname = "MessageProtos";
|
|||
|
||||
message Envelope {
|
||||
enum Type {
|
||||
UNKNOWN = 0;
|
||||
CIPHERTEXT = 1;
|
||||
KEY_EXCHANGE = 2;
|
||||
PREKEY_BUNDLE = 3;
|
||||
RECEIPT = 5;
|
||||
UNKNOWN = 0;
|
||||
CIPHERTEXT = 1;
|
||||
KEY_EXCHANGE = 2;
|
||||
PREKEY_BUNDLE = 3;
|
||||
RECEIPT = 5;
|
||||
UNIDENTIFIED_SENDER = 6;
|
||||
}
|
||||
|
||||
optional Type type = 1;
|
||||
optional string source = 2;
|
||||
optional uint32 sourceDevice = 7;
|
||||
optional string relay = 3;
|
||||
optional uint64 timestamp = 5;
|
||||
optional bytes legacyMessage = 6; // Contains an encrypted DataMessage XXX -- Remove after 10/01/15
|
||||
optional bytes content = 8; // Contains an encrypted Content
|
||||
optional Type type = 1;
|
||||
optional string source = 2;
|
||||
optional uint32 sourceDevice = 7;
|
||||
optional string relay = 3;
|
||||
optional uint64 timestamp = 5;
|
||||
optional bytes legacyMessage = 6; // Contains an encrypted DataMessage XXX -- Remove after 10/01/15
|
||||
optional bytes content = 8; // Contains an encrypted Content
|
||||
optional string serverGuid = 9;
|
||||
optional uint64 server_timestamp = 10;
|
||||
}
|
||||
|
||||
message ProvisioningUuid {
|
||||
optional string uuid = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message ServerCertificate {
|
||||
message Certificate {
|
||||
optional uint32 id = 1;
|
||||
optional bytes key = 2;
|
||||
}
|
||||
|
||||
optional bytes certificate = 1;
|
||||
optional bytes signature = 2;
|
||||
}
|
||||
|
||||
message SenderCertificate {
|
||||
message Certificate {
|
||||
optional string sender = 1;
|
||||
optional uint32 senderDevice = 2;
|
||||
optional fixed64 expires = 3;
|
||||
optional bytes identityKey = 4;
|
||||
optional ServerCertificate signer = 5;
|
||||
}
|
||||
|
||||
optional bytes certificate = 1;
|
||||
optional bytes signature = 2;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.whispersystems.dispatch;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dispatch.io.RedisPubSubConnectionFactory;
|
||||
|
@ -9,10 +8,12 @@ import org.whispersystems.dispatch.redis.PubSubReply;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class DispatchManager extends Thread {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DispatchManager.class);
|
||||
|
@ -45,7 +46,7 @@ public class DispatchManager extends Thread {
|
|||
}
|
||||
|
||||
public synchronized void subscribe(String name, DispatchChannel dispatchChannel) {
|
||||
Optional<DispatchChannel> previous = Optional.fromNullable(subscriptions.get(name));
|
||||
Optional<DispatchChannel> previous = Optional.ofNullable(subscriptions.get(name));
|
||||
subscriptions.put(name, dispatchChannel);
|
||||
|
||||
try {
|
||||
|
@ -60,7 +61,7 @@ public class DispatchManager extends Thread {
|
|||
}
|
||||
|
||||
public synchronized void unsubscribe(String name, DispatchChannel channel) {
|
||||
Optional<DispatchChannel> subscription = Optional.fromNullable(subscriptions.get(name));
|
||||
Optional<DispatchChannel> subscription = Optional.ofNullable(subscriptions.get(name));
|
||||
|
||||
if (subscription.isPresent() && subscription.get() == channel) {
|
||||
subscriptions.remove(name);
|
||||
|
@ -105,7 +106,7 @@ public class DispatchManager extends Thread {
|
|||
}
|
||||
|
||||
private void dispatchSubscribe(final PubSubReply reply) {
|
||||
Optional<DispatchChannel> subscription = Optional.fromNullable(subscriptions.get(reply.getChannel()));
|
||||
Optional<DispatchChannel> subscription = Optional.ofNullable(subscriptions.get(reply.getChannel()));
|
||||
|
||||
if (subscription.isPresent()) {
|
||||
dispatchSubscription(reply.getChannel(), subscription.get());
|
||||
|
@ -115,7 +116,7 @@ public class DispatchManager extends Thread {
|
|||
}
|
||||
|
||||
private void dispatchMessage(PubSubReply reply) {
|
||||
Optional<DispatchChannel> subscription = Optional.fromNullable(subscriptions.get(reply.getChannel()));
|
||||
Optional<DispatchChannel> subscription = Optional.ofNullable(subscriptions.get(reply.getChannel()));
|
||||
|
||||
if (subscription.isPresent()) {
|
||||
dispatchMessage(reply.getChannel(), subscription.get(), reply.getContent().get());
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.whispersystems.dispatch.redis;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dispatch.io.RedisInputStream;
|
||||
|
@ -14,6 +13,7 @@ import java.io.IOException;
|
|||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class PubSubConnection {
|
||||
|
@ -98,12 +98,12 @@ public class PubSubConnection {
|
|||
|
||||
private PubSubReply readUnsubscribeReply() throws IOException {
|
||||
String channelName = readSubscriptionReply();
|
||||
return new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, channelName, Optional.<byte[]>absent());
|
||||
return new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, channelName, Optional.empty());
|
||||
}
|
||||
|
||||
private PubSubReply readSubscribeReply() throws IOException {
|
||||
String channelName = readSubscriptionReply();
|
||||
return new PubSubReply(PubSubReply.Type.SUBSCRIBE, channelName, Optional.<byte[]>absent());
|
||||
return new PubSubReply(PubSubReply.Type.SUBSCRIBE, channelName, Optional.empty());
|
||||
}
|
||||
|
||||
private String readSubscriptionReply() throws IOException {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package org.whispersystems.dispatch.redis;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class PubSubReply {
|
||||
|
||||
public enum Type {
|
||||
|
@ -11,7 +13,7 @@ public class PubSubReply {
|
|||
}
|
||||
|
||||
private final Type type;
|
||||
private final String channel;
|
||||
private final String channel;
|
||||
private final Optional<byte[]> content;
|
||||
|
||||
public PubSubReply(Type type, String channel, Optional<byte[]> content) {
|
||||
|
|
|
@ -17,20 +17,7 @@
|
|||
package org.whispersystems.textsecuregcm;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AttachmentsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MessageCacheConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ProfilesConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.*;
|
||||
import org.whispersystems.websocket.configuration.WebSocketConfiguration;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
@ -101,10 +88,6 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
@JsonProperty
|
||||
private List<MaxDeviceConfiguration> maxDevices = new LinkedList<>();
|
||||
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private FederationConfiguration federation = new FederationConfiguration();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
|
@ -143,6 +126,11 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
@JsonProperty
|
||||
private ApnConfiguration apn;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private UnidentifiedDeliveryConfiguration unidentifiedDelivery;
|
||||
|
||||
public WebSocketConfiguration getWebSocketConfiguration() {
|
||||
return webSocket;
|
||||
}
|
||||
|
@ -195,10 +183,6 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
return limits;
|
||||
}
|
||||
|
||||
public FederationConfiguration getFederationConfiguration() {
|
||||
return federation;
|
||||
}
|
||||
|
||||
public TurnConfiguration getTurnConfiguration() {
|
||||
return turn;
|
||||
}
|
||||
|
@ -215,6 +199,10 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
return profiles;
|
||||
}
|
||||
|
||||
public UnidentifiedDeliveryConfiguration getDeliveryCertificate() {
|
||||
return unidentifiedDelivery;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getTestDevices() {
|
||||
Map<String, Integer> results = new HashMap<>();
|
||||
|
||||
|
|
|
@ -20,31 +20,24 @@ import com.codahale.metrics.SharedMetricRegistries;
|
|||
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;
|
||||
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||
import org.skife.jdbi.v2.DBI;
|
||||
import org.whispersystems.dispatch.DispatchManager;
|
||||
import org.whispersystems.dropwizard.simpleauth.AuthDynamicFeature;
|
||||
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
|
||||
import org.whispersystems.dropwizard.simpleauth.BasicCredentialAuthFilter;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.DirectoryCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentController;
|
||||
import org.whispersystems.textsecuregcm.controllers.CertificateController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
|
||||
import org.whispersystems.textsecuregcm.controllers.FederationControllerV1;
|
||||
import org.whispersystems.textsecuregcm.controllers.FederationControllerV2;
|
||||
import org.whispersystems.textsecuregcm.controllers.KeepAliveController;
|
||||
import org.whispersystems.textsecuregcm.controllers.KeysController;
|
||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||
import org.whispersystems.textsecuregcm.controllers.ProfileController;
|
||||
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle;
|
||||
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
|
||||
|
@ -75,6 +68,7 @@ import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
|
|||
import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler;
|
||||
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.workers.CertificateCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.PeriodicStatsCommand;
|
||||
|
@ -88,9 +82,13 @@ import javax.servlet.FilterRegistration;
|
|||
import javax.servlet.ServletRegistration;
|
||||
import java.security.Security;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import io.dropwizard.Application;
|
||||
import io.dropwizard.auth.AuthDynamicFeature;
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
import io.dropwizard.jdbi.DBIFactory;
|
||||
import io.dropwizard.setup.Bootstrap;
|
||||
|
@ -109,6 +107,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
bootstrap.addCommand(new TrimMessagesCommand());
|
||||
bootstrap.addCommand(new PeriodicStatsCommand());
|
||||
bootstrap.addCommand(new DeleteUserCommand());
|
||||
bootstrap.addCommand(new CertificateCommand());
|
||||
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") {
|
||||
@Override
|
||||
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
|
||||
|
@ -163,7 +162,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, cacheClient);
|
||||
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, cacheClient );
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
|
||||
FederatedClientManager federatedClientManager = new FederatedClientManager(environment, config.getJerseyClientConfiguration(), config.getFederationConfiguration());
|
||||
MessagesCache messagesCache = new MessagesCache(messagesClient, messages, accountsManager, config.getMessageCacheConfiguration().getPersistDelayMinutes());
|
||||
MessagesManager messagesManager = new MessagesManager(messages, messagesCache);
|
||||
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(messagesManager);
|
||||
|
@ -173,7 +171,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
GCMSender gcmSender = new GCMSender(accountsManager, config.getGcmConfiguration().getApiKey(), directoryQueue);
|
||||
WebsocketSender websocketSender = new WebsocketSender(messagesManager, pubSubManager);
|
||||
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager );
|
||||
FederatedPeerAuthenticator federatedPeerAuthenticator = new FederatedPeerAuthenticator(config.getFederationConfiguration());
|
||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheClient);
|
||||
|
||||
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushSchedulerClient, apnSender, accountsManager);
|
||||
|
@ -181,7 +178,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
SmsSender smsSender = new SmsSender(twilioSmsSender);
|
||||
UrlSigner urlSigner = new UrlSigner(config.getAttachmentsConfiguration());
|
||||
PushSender pushSender = new PushSender(apnFallbackManager, gcmSender, apnSender, websocketSender, config.getPushConfiguration().getQueueSize());
|
||||
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
|
||||
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender);
|
||||
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration());
|
||||
|
||||
DirectoryCredentialsGenerator directoryCredentialsGenerator = new DirectoryCredentialsGenerator(config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(),
|
||||
|
@ -201,27 +198,21 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
environment.lifecycle().manage(messagesCache);
|
||||
environment.lifecycle().manage(directoryReconciler);
|
||||
|
||||
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
|
||||
KeysController keysController = new KeysController(rateLimiters, keys, accountsManager, federatedClientManager);
|
||||
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, federatedClientManager, apnFallbackManager);
|
||||
AttachmentController attachmentController = new AttachmentController(rateLimiters, urlSigner);
|
||||
KeysController keysController = new KeysController(rateLimiters, keys, accountsManager);
|
||||
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, apnFallbackManager);
|
||||
ProfileController profileController = new ProfileController(rateLimiters , accountsManager, config.getProfilesConfiguration());
|
||||
|
||||
environment.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<Account>()
|
||||
.setAuthenticator(deviceAuthenticator)
|
||||
.setPrincipal(Account.class)
|
||||
.buildAuthFilter(),
|
||||
new BasicCredentialAuthFilter.Builder<FederatedPeer>()
|
||||
.setAuthenticator(federatedPeerAuthenticator)
|
||||
.setPrincipal(FederatedPeer.class)
|
||||
.buildAuthFilter()));
|
||||
environment.jersey().register(new AuthValueFactoryProvider.Binder());
|
||||
environment.jersey().register(new AuthValueFactoryProvider.Binder<>(Account.class));
|
||||
|
||||
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, directoryQueue, messagesManager, turnTokenGenerator, config.getTestDevices()));
|
||||
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, directoryQueue, rateLimiters, config.getMaxDevices()));
|
||||
environment.jersey().register(new DirectoryController(rateLimiters, directory, directoryCredentialsGenerator));
|
||||
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController));
|
||||
environment.jersey().register(new FederationControllerV2(accountsManager, attachmentController, messageController, keysController));
|
||||
environment.jersey().register(new ProvisioningController(rateLimiters, pushSender));
|
||||
environment.jersey().register(new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays())));
|
||||
environment.jersey().register(attachmentController);
|
||||
environment.jersey().register(keysController);
|
||||
environment.jersey().register(messageController);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -19,18 +19,19 @@ package org.whispersystems.textsecuregcm.auth;
|
|||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dropwizard.simpleauth.Authenticator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import io.dropwizard.auth.AuthenticationException;
|
||||
import io.dropwizard.auth.Authenticator;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
|
||||
public class AccountAuthenticator implements Authenticator<BasicCredentials, Account> {
|
||||
|
@ -56,13 +57,13 @@ public class AccountAuthenticator implements Authenticator<BasicCredentials, Acc
|
|||
Optional<Account> account = accountsManager.get(authorizationHeader.getNumber());
|
||||
|
||||
if (!account.isPresent()) {
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Optional<Device> device = account.get().getDevice(authorizationHeader.getDeviceId());
|
||||
|
||||
if (!device.isPresent()) {
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
|
||||
|
@ -73,9 +74,9 @@ public class AccountAuthenticator implements Authenticator<BasicCredentials, Acc
|
|||
}
|
||||
|
||||
authenticationFailedMeter.mark();
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
} catch (InvalidAuthorizationHeaderException iahe) {
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
|
||||
public class Anonymous {
|
||||
|
||||
private final byte[] unidentifiedSenderAccessKey;
|
||||
|
||||
public Anonymous(String header) {
|
||||
try {
|
||||
this.unidentifiedSenderAccessKey = Base64.decode(header);
|
||||
} catch (IOException e) {
|
||||
throw new WebApplicationException(e, Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getAccessKey() {
|
||||
return unidentifiedSenderAccessKey;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import org.whispersystems.textsecuregcm.crypto.Curve;
|
||||
import org.whispersystems.textsecuregcm.crypto.ECPrivateKey;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.SenderCertificate;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.ServerCertificate;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class CertificateGenerator {
|
||||
|
||||
private final ECPrivateKey privateKey;
|
||||
private final int expiresDays;
|
||||
private final ServerCertificate serverCertificate;
|
||||
|
||||
public CertificateGenerator(byte[] serverCertificate, ECPrivateKey privateKey, int expiresDays)
|
||||
throws InvalidProtocolBufferException
|
||||
{
|
||||
this.privateKey = privateKey;
|
||||
this.expiresDays = expiresDays;
|
||||
this.serverCertificate = ServerCertificate.parseFrom(serverCertificate);
|
||||
}
|
||||
|
||||
public byte[] createFor(Account account, Device device) throws IOException, InvalidKeyException {
|
||||
byte[] certificate = SenderCertificate.Certificate.newBuilder()
|
||||
.setSender(account.getNumber())
|
||||
.setSenderDevice(Math.toIntExact(device.getId()))
|
||||
.setExpires(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresDays))
|
||||
.setIdentityKey(ByteString.copyFrom(Base64.decode(account.getIdentityKey())))
|
||||
.setSigner(serverCertificate)
|
||||
.build()
|
||||
.toByteArray();
|
||||
|
||||
byte[] signature = Curve.calculateSignature(privateKey, certificate);
|
||||
|
||||
return SenderCertificate.newBuilder()
|
||||
.setCertificate(ByteString.copyFrom(certificate))
|
||||
.setSignature(ByteString.copyFrom(signature))
|
||||
.build()
|
||||
.toByteArray();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dropwizard.simpleauth.Authenticator;
|
||||
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import io.dropwizard.auth.AuthenticationException;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
|
||||
|
||||
public class FederatedPeerAuthenticator implements Authenticator<BasicCredentials, FederatedPeer> {
|
||||
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
|
||||
private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(),
|
||||
"authentication",
|
||||
"failed"));
|
||||
|
||||
private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(),
|
||||
"authentication",
|
||||
"succeeded"));
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.Hex;
|
||||
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class OptionalAccess {
|
||||
|
||||
public static final String UNIDENTIFIED = "Unidentified-Access-Key";
|
||||
|
||||
public static void verify(Optional<Account> requestAccount,
|
||||
Optional<Anonymous> accessKey,
|
||||
Optional<Account> targetAccount,
|
||||
String deviceSelector)
|
||||
{
|
||||
try {
|
||||
verify(requestAccount, accessKey, targetAccount);
|
||||
|
||||
if (!deviceSelector.equals("*")) {
|
||||
long deviceId = Long.parseLong(deviceSelector);
|
||||
|
||||
Optional<Device> targetDevice = targetAccount.get().getDevice(deviceId);
|
||||
|
||||
if (targetDevice.isPresent() && targetDevice.get().isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestAccount.isPresent()) {
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
} else {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
}
|
||||
}
|
||||
|
||||
public static void verify(Optional<Account> requestAccount,
|
||||
Optional<Anonymous> accessKey,
|
||||
Optional<Account> targetAccount)
|
||||
{
|
||||
if (requestAccount.isPresent() && targetAccount.isPresent() && targetAccount.get().isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//noinspection ConstantConditions
|
||||
if (requestAccount.isPresent() && (!targetAccount.isPresent() || (targetAccount.isPresent() && !targetAccount.get().isActive()))) {
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
}
|
||||
|
||||
if (accessKey.isPresent() && targetAccount.isPresent() && targetAccount.get().isActive() && targetAccount.get().isUnrestrictedUnidentifiedAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (accessKey.isPresent() &&
|
||||
targetAccount.isPresent() &&
|
||||
targetAccount.get().getUnidentifiedAccessKey().isPresent() &&
|
||||
targetAccount.get().isActive() &&
|
||||
MessageDigest.isEqual(accessKey.get().getAccessKey(), targetAccount.get().getUnidentifiedAccessKey().get()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class UnidentifiedAccessChecksum {
|
||||
|
||||
public static String generateFor(Optional<byte[]> unidentifiedAccessKey) {
|
||||
try {
|
||||
if (!unidentifiedAccessKey.isPresent()|| unidentifiedAccessKey.get().length != 16) return null;
|
||||
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(unidentifiedAccessKey.get(), "HmacSHA256"));
|
||||
|
||||
return Base64.encodeBytes(mac.doFinal(new byte[32]));
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FederationConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private List<FederatedPeer> peers;
|
||||
|
||||
@JsonProperty
|
||||
private String name;
|
||||
|
||||
public List<FederatedPeer> getPeers() {
|
||||
return peers;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.base.Optional;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
|
||||
public class RedPhoneConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private String authKey;
|
||||
|
||||
public Optional<byte[]> getAuthorizationKey() throws DecoderException {
|
||||
if (authKey == null || authKey.trim().length() == 0) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
return Optional.of(Hex.decodeHex(authKey.toCharArray()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.whispersystems.textsecuregcm.crypto.Curve;
|
||||
import org.whispersystems.textsecuregcm.crypto.ECPrivateKey;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
public class UnidentifiedDeliveryConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
@NotNull
|
||||
private byte[] certificate;
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
@NotNull
|
||||
@Size(min = 32, max = 32)
|
||||
private byte[] privateKey;
|
||||
|
||||
@NotNull
|
||||
private int expiresDays;
|
||||
|
||||
public byte[] getCertificate() {
|
||||
return certificate;
|
||||
}
|
||||
|
||||
public ECPrivateKey getPrivateKey() {
|
||||
return Curve.decodePrivatePoint(privateKey);
|
||||
}
|
||||
|
||||
public int getExpiresDays() {
|
||||
return expiresDays;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -21,7 +21,6 @@ import com.codahale.metrics.MetricRegistry;
|
|||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
|
@ -66,11 +65,13 @@ import java.io.IOException;
|
|||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@Path("/v1/accounts")
|
||||
public class AccountController {
|
||||
|
||||
|
@ -311,13 +312,14 @@ public class AccountController {
|
|||
device.setFetchesMessages(attributes.getFetchesMessages());
|
||||
device.setName(attributes.getName());
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setVoiceSupported(attributes.getVoice());
|
||||
device.setVideoSupported(attributes.getVideo());
|
||||
device.setUnauthenticatedDeliverySupported(attributes.getUnidentifiedAccessKey() != null);
|
||||
device.setRegistrationId(attributes.getRegistrationId());
|
||||
device.setSignalingKey(attributes.getSignalingKey());
|
||||
device.setUserAgent(userAgent);
|
||||
|
||||
account.setPin(attributes.getPin());
|
||||
account.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey());
|
||||
account.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess());
|
||||
|
||||
accounts.update(account);
|
||||
}
|
||||
|
@ -339,8 +341,7 @@ public class AccountController {
|
|||
device.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||
device.setRegistrationId(accountAttributes.getRegistrationId());
|
||||
device.setName(accountAttributes.getName());
|
||||
device.setVoiceSupported(accountAttributes.getVoice());
|
||||
device.setVideoSupported(accountAttributes.getVideo());
|
||||
device.setUnauthenticatedDeliverySupported(accountAttributes.getUnidentifiedAccessKey() != null);
|
||||
device.setCreated(System.currentTimeMillis());
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setUserAgent(userAgent);
|
||||
|
@ -349,6 +350,8 @@ public class AccountController {
|
|||
account.setNumber(number);
|
||||
account.addDevice(device);
|
||||
account.setPin(accountAttributes.getPin());
|
||||
account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
|
||||
account.setUnrestrictedUnidentifiedAccess(accountAttributes.isUnrestrictedUnidentifiedAccess());
|
||||
|
||||
if (accounts.create(account)) {
|
||||
newUserMeter.mark();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -18,26 +18,20 @@ package org.whispersystems.textsecuregcm.controllers;
|
|||
|
||||
import com.amazonaws.HttpMethod;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.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.s3.UrlSigner;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.Conversions;
|
||||
import org.whispersystems.textsecuregcm.s3.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.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.security.SecureRandom;
|
||||
|
@ -49,21 +43,18 @@ import io.dropwizard.auth.Auth;
|
|||
@Path("/v1/attachments")
|
||||
public class AttachmentController {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private final Logger logger = LoggerFactory.getLogger(AttachmentController.class);
|
||||
|
||||
private static final String[] UNACCELERATED_REGIONS = {"+20", "+971", "+968", "+974"};
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final FederatedClientManager federatedClientManager;
|
||||
private final UrlSigner urlSigner;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final UrlSigner urlSigner;
|
||||
|
||||
public AttachmentController(RateLimiters rateLimiters,
|
||||
FederatedClientManager federatedClientManager,
|
||||
UrlSigner urlSigner)
|
||||
public AttachmentController(RateLimiters rateLimiters, UrlSigner urlSigner)
|
||||
{
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.federatedClientManager = federatedClientManager;
|
||||
this.urlSigner = urlSigner;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.urlSigner = urlSigner;
|
||||
}
|
||||
|
||||
@Timed
|
||||
|
@ -88,20 +79,10 @@ public class AttachmentController {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{attachmentId}")
|
||||
public AttachmentUri redirectToAttachment(@Auth Account account,
|
||||
@PathParam("attachmentId") long attachmentId,
|
||||
@QueryParam("relay") Optional<String> relay)
|
||||
@PathParam("attachmentId") long attachmentId)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
if (!relay.isPresent()) {
|
||||
return new AttachmentUri(urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET, Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> account.getNumber().startsWith(region))));
|
||||
} else {
|
||||
return new AttachmentUri(federatedClientManager.getClient(relay.get()).getSignedAttachmentUri(attachmentId));
|
||||
}
|
||||
} catch (NoSuchPeerException e) {
|
||||
logger.info("No such peer: " + relay);
|
||||
throw new WebApplicationException(Response.status(404).build());
|
||||
}
|
||||
return new AttachmentUri(urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET, Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> account.getNumber().startsWith(region))));
|
||||
}
|
||||
|
||||
private long generateAttachmentId() {
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v1/certificate")
|
||||
public class CertificateController {
|
||||
|
||||
private final CertificateGenerator certificateGenerator;
|
||||
|
||||
public CertificateController(CertificateGenerator certificateGenerator) {
|
||||
this.certificateGenerator = certificateGenerator;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/delivery")
|
||||
public DeliveryCertificate getDeliveryCertificate(@Auth Account account) throws IOException, InvalidKeyException {
|
||||
if (!account.getAuthenticatedDevice().isPresent()) throw new AssertionError();
|
||||
return new DeliveryCertificate(certificateGenerator.createFor(account, account.getAuthenticatedDevice().get()));
|
||||
}
|
||||
|
||||
}
|
|
@ -18,7 +18,6 @@ package org.whispersystems.textsecuregcm.controllers;
|
|||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
|
@ -55,6 +54,7 @@ import java.security.SecureRandom;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
|
@ -213,6 +213,15 @@ public class DeviceController {
|
|||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/unauthenticated_delivery")
|
||||
public void setUnauthenticatedDelivery(@Auth Account account) {
|
||||
assert(account.getAuthenticatedDevice().isPresent());
|
||||
account.getAuthenticatedDevice().get().setUnauthenticatedDeliverySupported(true);
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
@VisibleForTesting protected VerificationCode generateVerificationCode() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
int randomInt = 100000 + random.nextInt(900000);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -20,13 +20,9 @@ import com.codahale.metrics.Histogram;
|
|||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.base.Optional;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.DirectoryCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||
|
@ -49,6 +45,7 @@ import javax.ws.rs.core.Response;
|
|||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
|
||||
public class FederationController {
|
||||
|
||||
protected final AccountsManager accounts;
|
||||
protected final AttachmentController attachmentController;
|
||||
protected final MessageController messageController;
|
||||
|
||||
public FederationController(AccountsManager accounts,
|
||||
AttachmentController attachmentController,
|
||||
MessageController messageController)
|
||||
{
|
||||
this.accounts = accounts;
|
||||
this.attachmentController = attachmentController;
|
||||
this.messageController = messageController;
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountCount;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||
import org.whispersystems.textsecuregcm.federation.NonLimitedAccount;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v1/federation")
|
||||
public class FederationControllerV1 extends FederationController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FederationControllerV1.class);
|
||||
|
||||
private static final int ACCOUNT_CHUNK_SIZE = 10000;
|
||||
|
||||
public FederationControllerV1(AccountsManager accounts,
|
||||
AttachmentController attachmentController,
|
||||
MessageController messageController)
|
||||
{
|
||||
super(accounts, attachmentController, messageController);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/attachment/{attachmentId}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer,
|
||||
@PathParam("attachmentId") long attachmentId)
|
||||
throws IOException
|
||||
{
|
||||
return attachmentController.redirectToAttachment(new NonLimitedAccount("Unknown", -1, peer.getName()),
|
||||
attachmentId, Optional.<String>absent());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/messages/{source}/{sourceDeviceId}/{destination}")
|
||||
public void sendMessages(@Auth FederatedPeer peer,
|
||||
@PathParam("source") String source,
|
||||
@PathParam("sourceDeviceId") long sourceDeviceId,
|
||||
@PathParam("destination") String destination,
|
||||
@Valid IncomingMessageList messages)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
messages.setRelay(null);
|
||||
messageController.sendMessage(new NonLimitedAccount(source, sourceDeviceId, peer.getName()), destination, messages);
|
||||
} catch (RateLimitExceededException e) {
|
||||
logger.warn("Rate limiting on federated channel", e);
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/user_count")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public AccountCount getUserCount(@Auth FederatedPeer peer) {
|
||||
return new AccountCount((int)accounts.getCount());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/user_tokens/{offset}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
|
||||
@PathParam("offset") int offset)
|
||||
{
|
||||
List<Account> accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE);
|
||||
List<ClientContact> clientContacts = new LinkedList<>();
|
||||
|
||||
for (Account account : accountList) {
|
||||
byte[] token = Util.getContactToken(account.getNumber());
|
||||
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported(), account.isVideoSupported());
|
||||
|
||||
if (!account.isActive()) {
|
||||
clientContact.setInactive(true);
|
||||
}
|
||||
|
||||
clientContacts.add(clientContact);
|
||||
}
|
||||
|
||||
return new ClientContacts(clientContacts);
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||
import org.whispersystems.textsecuregcm.federation.NonLimitedAccount;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v2/federation")
|
||||
public class FederationControllerV2 extends FederationController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FederationControllerV2.class);
|
||||
|
||||
private final KeysController keysController;
|
||||
|
||||
public FederationControllerV2(AccountsManager accounts, AttachmentController attachmentController, MessageController messageController, KeysController keysController) {
|
||||
super(accounts, attachmentController, messageController);
|
||||
this.keysController = keysController;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/key/{number}/{device}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Optional<PreKeyResponse> getKeysV2(@Auth FederatedPeer peer,
|
||||
@PathParam("number") String number,
|
||||
@PathParam("device") String device)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
return keysController.getDeviceKeys(new NonLimitedAccount("Unknown", -1, peer.getName()),
|
||||
number, device, Optional.<String>absent());
|
||||
} catch (RateLimitExceededException e) {
|
||||
logger.warn("Rate limiting on federated channel", e);
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -17,18 +17,17 @@
|
|||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.base.Optional;
|
||||
import org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItem;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyState;
|
||||
import org.whispersystems.textsecuregcm.auth.Anonymous;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItem;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyState;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
|
@ -39,36 +38,34 @@ import org.whispersystems.textsecuregcm.storage.Keys;
|
|||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
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.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@Path("/v2/keys")
|
||||
public class KeysController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(KeysController.class);
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final Keys keys;
|
||||
private final AccountsManager accounts;
|
||||
private final FederatedClientManager federatedClientManager;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final Keys keys;
|
||||
private final AccountsManager accounts;
|
||||
|
||||
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
|
||||
FederatedClientManager federatedClientManager)
|
||||
{
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.keys = keys;
|
||||
this.accounts = accounts;
|
||||
this.federatedClientManager = federatedClientManager;
|
||||
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.keys = keys;
|
||||
this.accounts = accounts;
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -111,50 +108,49 @@ public class KeysController {
|
|||
@GET
|
||||
@Path("/{number}/{device_id}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Optional<PreKeyResponse> getDeviceKeys(@Auth Account account,
|
||||
@PathParam("number") String number,
|
||||
@PathParam("device_id") String deviceId,
|
||||
@QueryParam("relay") Optional<String> relay)
|
||||
public Optional<PreKeyResponse> getDeviceKeys(@Auth Optional<Account> account,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@PathParam("number") String number,
|
||||
@PathParam("device_id") String deviceId)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
try {
|
||||
if (relay.isPresent()) {
|
||||
return federatedClientManager.getClient(relay.get()).getKeysV2(number, deviceId);
|
||||
}
|
||||
if (!account.isPresent() && !accessKey.isPresent()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
Account target = getAccount(number, deviceId);
|
||||
if (account.isPresent()) {
|
||||
rateLimiters.getPreKeysLimiter().validate(account.get().getNumber() + "__" + number + "." + deviceId);
|
||||
}
|
||||
|
||||
if (account.isRateLimited()) {
|
||||
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number + "." + deviceId);
|
||||
}
|
||||
Optional<Account> target = accounts.get(number);
|
||||
OptionalAccess.verify(account, accessKey, target, deviceId);
|
||||
|
||||
Optional<List<KeyRecord>> targetKeys = getLocalKeys(target, deviceId);
|
||||
List<PreKeyResponseItem> devices = new LinkedList<>();
|
||||
assert(target.isPresent());
|
||||
|
||||
for (Device device : target.getDevices()) {
|
||||
if (device.isActive() && (deviceId.equals("*") || device.getId() == Long.parseLong(deviceId))) {
|
||||
SignedPreKey signedPreKey = device.getSignedPreKey();
|
||||
PreKey preKey = null;
|
||||
Optional<List<KeyRecord>> targetKeys = getLocalKeys(target.get(), deviceId);
|
||||
List<PreKeyResponseItem> devices = new LinkedList<>();
|
||||
|
||||
if (targetKeys.isPresent()) {
|
||||
for (KeyRecord keyRecord : targetKeys.get()) {
|
||||
if (!keyRecord.isLastResort() && keyRecord.getDeviceId() == device.getId()) {
|
||||
preKey = new PreKey(keyRecord.getKeyId(), keyRecord.getPublicKey());
|
||||
}
|
||||
for (Device device : target.get().getDevices()) {
|
||||
if (device.isActive() && (deviceId.equals("*") || device.getId() == Long.parseLong(deviceId))) {
|
||||
SignedPreKey signedPreKey = device.getSignedPreKey();
|
||||
PreKey preKey = null;
|
||||
|
||||
if (targetKeys.isPresent()) {
|
||||
for (KeyRecord keyRecord : targetKeys.get()) {
|
||||
if (!keyRecord.isLastResort() && keyRecord.getDeviceId() == device.getId()) {
|
||||
preKey = new PreKey(keyRecord.getKeyId(), keyRecord.getPublicKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (signedPreKey != null || preKey != null) {
|
||||
devices.add(new PreKeyResponseItem(device.getId(), device.getRegistrationId(), signedPreKey, preKey));
|
||||
}
|
||||
if (signedPreKey != null || preKey != null) {
|
||||
devices.add(new PreKeyResponseItem(device.getId(), device.getRegistrationId(), signedPreKey, preKey));
|
||||
}
|
||||
}
|
||||
|
||||
if (devices.isEmpty()) return Optional.absent();
|
||||
else return Optional.of(new PreKeyResponse(target.getIdentityKey(), devices));
|
||||
} catch (NoSuchPeerException | NoSuchUserException e) {
|
||||
throw new WebApplicationException(Response.status(404).build());
|
||||
}
|
||||
|
||||
if (devices.isEmpty()) return Optional.empty();
|
||||
else return Optional.of(new PreKeyResponse(target.get().getIdentityKey(), devices));
|
||||
}
|
||||
|
||||
@Timed
|
||||
|
@ -176,12 +172,10 @@ public class KeysController {
|
|||
SignedPreKey signedPreKey = device.getSignedPreKey();
|
||||
|
||||
if (signedPreKey != null) return Optional.of(signedPreKey);
|
||||
else return Optional.absent();
|
||||
else return Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<List<KeyRecord>> getLocalKeys(Account destination, String deviceIdSelector)
|
||||
throws NoSuchUserException
|
||||
{
|
||||
private Optional<List<KeyRecord>> getLocalKeys(Account destination, String deviceIdSelector) {
|
||||
try {
|
||||
if (deviceIdSelector.equals("*")) {
|
||||
return keys.get(destination.getNumber());
|
||||
|
@ -202,30 +196,4 @@ public class KeysController {
|
|||
throw new WebApplicationException(Response.status(422).build());
|
||||
}
|
||||
}
|
||||
|
||||
private Account getAccount(String number, String deviceSelector)
|
||||
throws NoSuchUserException
|
||||
{
|
||||
try {
|
||||
Optional<Account> account = accounts.get(number);
|
||||
|
||||
if (!account.isPresent() || !account.get().isActive()) {
|
||||
throw new NoSuchUserException("No active account");
|
||||
}
|
||||
|
||||
if (!deviceSelector.equals("*")) {
|
||||
long deviceId = Long.parseLong(deviceSelector);
|
||||
|
||||
Optional<Device> targetDevice = account.get().getDevice(deviceId);
|
||||
|
||||
if (!targetDevice.isPresent() || !targetDevice.get().isActive()) {
|
||||
throw new NoSuchUserException("No active device");
|
||||
}
|
||||
}
|
||||
|
||||
return account.get();
|
||||
} catch (NumberFormatException e) {
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,10 +17,11 @@
|
|||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.protobuf.ByteString;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.Anonymous;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
|
@ -29,15 +30,11 @@ import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
|||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||
import org.whispersystems.textsecuregcm.entities.SendMessageResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.StaleDevices;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClient;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
|
||||
import org.whispersystems.textsecuregcm.redis.RedisOperation;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
|
@ -51,6 +48,7 @@ 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;
|
||||
|
@ -62,10 +60,13 @@ import java.io.IOException;
|
|||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@Path("/v1/messages")
|
||||
public class MessageController {
|
||||
|
||||
|
@ -74,7 +75,6 @@ public class MessageController {
|
|||
private final RateLimiters rateLimiters;
|
||||
private final PushSender pushSender;
|
||||
private final ReceiptSender receiptSender;
|
||||
private final FederatedClientManager federatedClientManager;
|
||||
private final AccountsManager accountsManager;
|
||||
private final MessagesManager messagesManager;
|
||||
private final ApnFallbackManager apnFallbackManager;
|
||||
|
@ -84,7 +84,6 @@ public class MessageController {
|
|||
ReceiptSender receiptSender,
|
||||
AccountsManager accountsManager,
|
||||
MessagesManager messagesManager,
|
||||
FederatedClientManager federatedClientManager,
|
||||
ApnFallbackManager apnFallbackManager)
|
||||
{
|
||||
this.rateLimiters = rateLimiters;
|
||||
|
@ -92,7 +91,6 @@ public class MessageController {
|
|||
this.receiptSender = receiptSender;
|
||||
this.accountsManager = accountsManager;
|
||||
this.messagesManager = messagesManager;
|
||||
this.federatedClientManager = federatedClientManager;
|
||||
this.apnFallbackManager = apnFallbackManager;
|
||||
}
|
||||
|
||||
|
@ -101,22 +99,41 @@ public class MessageController {
|
|||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public SendMessageResponse sendMessage(@Auth Account source,
|
||||
@PathParam("destination") String destinationName,
|
||||
@Valid IncomingMessageList messages)
|
||||
throws IOException, RateLimitExceededException
|
||||
public SendMessageResponse sendMessage(@Auth Optional<Account> source,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@PathParam("destination") String destinationName,
|
||||
@Valid IncomingMessageList messages)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
if (!source.getNumber().equals(destinationName)) {
|
||||
rateLimiters.getMessagesLimiter().validate(source.getNumber() + "__" + destinationName);
|
||||
if (!source.isPresent() && !accessKey.isPresent()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (source.isPresent() && !source.get().getNumber().equals(destinationName)) {
|
||||
rateLimiters.getMessagesLimiter().validate(source.get().getNumber() + "__" + destinationName);
|
||||
}
|
||||
|
||||
try {
|
||||
boolean isSyncMessage = source.getNumber().equals(destinationName);
|
||||
boolean isSyncMessage = source.isPresent() && source.get().getNumber().equals(destinationName);
|
||||
|
||||
if (Util.isEmpty(messages.getRelay())) sendLocalMessage(source, destinationName, messages, isSyncMessage);
|
||||
else sendRelayMessage(source, destinationName, messages, isSyncMessage);
|
||||
Optional<Account> destination;
|
||||
|
||||
return new SendMessageResponse(!isSyncMessage && source.getActiveDeviceCount() > 1);
|
||||
if (!isSyncMessage) destination = accountsManager.get(destinationName);
|
||||
else destination = source;
|
||||
|
||||
OptionalAccess.verify(source, accessKey, destination);
|
||||
validateCompleteDeviceList(destination.get(), messages.getMessages(), isSyncMessage);
|
||||
validateRegistrationIds(destination.get(), messages.getMessages());
|
||||
|
||||
for (IncomingMessage incomingMessage : messages.getMessages()) {
|
||||
Optional<Device> destinationDevice = destination.get().getDevice(incomingMessage.getDestinationDeviceId());
|
||||
|
||||
if (destinationDevice.isPresent()) {
|
||||
sendMessage(source, destination.get(), destinationDevice.get(), messages.getTimestamp(), incomingMessage);
|
||||
}
|
||||
}
|
||||
|
||||
return new SendMessageResponse(!isSyncMessage && source.isPresent() && source.get().getActiveDeviceCount() > 1);
|
||||
} catch (NoSuchUserException e) {
|
||||
throw new WebApplicationException(Response.status(404).build());
|
||||
} catch (MismatchedDevicesException e) {
|
||||
|
@ -130,8 +147,6 @@ public class MessageController {
|
|||
.type(MediaType.APPLICATION_JSON)
|
||||
.entity(new StaleDevices(e.getStaleDevices()))
|
||||
.build());
|
||||
} catch (InvalidDestinationException e) {
|
||||
throw new WebApplicationException(Response.status(400).build());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,7 +170,6 @@ public class MessageController {
|
|||
public void removePendingMessage(@Auth Account account,
|
||||
@PathParam("source") String source,
|
||||
@PathParam("timestamp") long timestamp)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
WebSocketConnection.messageTime.update(System.currentTimeMillis() - timestamp);
|
||||
|
@ -167,45 +181,41 @@ public class MessageController {
|
|||
if (message.isPresent() && message.get().getType() != Envelope.Type.RECEIPT_VALUE) {
|
||||
receiptSender.sendReceipt(account,
|
||||
message.get().getSource(),
|
||||
message.get().getTimestamp(),
|
||||
Optional.fromNullable(message.get().getRelay()));
|
||||
message.get().getTimestamp());
|
||||
}
|
||||
} catch (NotPushRegisteredException e) {
|
||||
logger.info("User no longer push registered for delivery receipt: " + e.getMessage());
|
||||
} catch (NoSuchUserException | TransientPushFailureException e) {
|
||||
} catch (NoSuchUserException e) {
|
||||
logger.warn("Sending delivery receipt", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/uuid/{uuid}")
|
||||
public void removePendingMessage(@Auth Account account, @PathParam("uuid") UUID uuid) {
|
||||
try {
|
||||
Optional<OutgoingMessageEntity> message = messagesManager.delete(account.getNumber(),
|
||||
account.getAuthenticatedDevice().get().getId(),
|
||||
uuid);
|
||||
|
||||
private void sendLocalMessage(Account source,
|
||||
String destinationName,
|
||||
IncomingMessageList messages,
|
||||
boolean isSyncMessage)
|
||||
throws NoSuchUserException, MismatchedDevicesException, StaleDevicesException
|
||||
{
|
||||
Account destination;
|
||||
message.ifPresent(outgoingMessageEntity -> WebSocketConnection.messageTime.update(System.currentTimeMillis() - outgoingMessageEntity.getTimestamp()));
|
||||
|
||||
if (!isSyncMessage) destination = getDestinationAccount(destinationName);
|
||||
else destination = source;
|
||||
|
||||
validateCompleteDeviceList(destination, messages.getMessages(), isSyncMessage);
|
||||
validateRegistrationIds(destination, messages.getMessages());
|
||||
|
||||
for (IncomingMessage incomingMessage : messages.getMessages()) {
|
||||
Optional<Device> destinationDevice = destination.getDevice(incomingMessage.getDestinationDeviceId());
|
||||
|
||||
if (destinationDevice.isPresent()) {
|
||||
sendLocalMessage(source, destination, destinationDevice.get(), messages.getTimestamp(), incomingMessage);
|
||||
if (message.isPresent() && !Util.isEmpty(message.get().getSource()) && message.get().getType() != Envelope.Type.RECEIPT_VALUE) {
|
||||
receiptSender.sendReceipt(account, message.get().getSource(), message.get().getTimestamp());
|
||||
}
|
||||
} catch (NoSuchUserException e) {
|
||||
logger.warn("Sending delivery receipt", e);
|
||||
} catch (NotPushRegisteredException e) {
|
||||
logger.info("User no longer push registered for delivery receipt: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendLocalMessage(Account source,
|
||||
Account destinationAccount,
|
||||
Device destinationDevice,
|
||||
long timestamp,
|
||||
IncomingMessage incomingMessage)
|
||||
private void sendMessage(Optional<Account> source,
|
||||
Account destinationAccount,
|
||||
Device destinationDevice,
|
||||
long timestamp,
|
||||
IncomingMessage incomingMessage)
|
||||
throws NoSuchUserException
|
||||
{
|
||||
try {
|
||||
|
@ -214,9 +224,13 @@ public class MessageController {
|
|||
Envelope.Builder messageBuilder = Envelope.newBuilder();
|
||||
|
||||
messageBuilder.setType(Envelope.Type.valueOf(incomingMessage.getType()))
|
||||
.setSource(source.getNumber())
|
||||
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
|
||||
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId());
|
||||
.setServerTimestamp(System.currentTimeMillis());
|
||||
|
||||
if (source.isPresent()) {
|
||||
messageBuilder.setSource(source.get().getNumber())
|
||||
.setSourceDevice((int)source.get().getAuthenticatedDevice().get().getId());
|
||||
}
|
||||
|
||||
if (messageBody.isPresent()) {
|
||||
messageBuilder.setLegacyMessage(ByteString.copyFrom(messageBody.get()));
|
||||
|
@ -226,10 +240,6 @@ public class MessageController {
|
|||
messageBuilder.setContent(ByteString.copyFrom(messageContent.get()));
|
||||
}
|
||||
|
||||
if (source.getRelay().isPresent()) {
|
||||
messageBuilder.setRelay(source.getRelay().get());
|
||||
}
|
||||
|
||||
pushSender.sendMessage(destinationAccount, destinationDevice, messageBuilder.build());
|
||||
} catch (NotPushRegisteredException e) {
|
||||
if (destinationDevice.isMaster()) throw new NoSuchUserException(e);
|
||||
|
@ -237,35 +247,6 @@ public class MessageController {
|
|||
}
|
||||
}
|
||||
|
||||
private void sendRelayMessage(Account source,
|
||||
String destinationName,
|
||||
IncomingMessageList messages,
|
||||
boolean isSyncMessage)
|
||||
throws IOException, NoSuchUserException, InvalidDestinationException
|
||||
{
|
||||
if (isSyncMessage) throw new InvalidDestinationException("Transcript messages can't be relayed!");
|
||||
|
||||
try {
|
||||
FederatedClient client = federatedClientManager.getClient(messages.getRelay());
|
||||
client.sendMessages(source.getNumber(), source.getAuthenticatedDevice().get().getId(),
|
||||
destinationName, messages);
|
||||
} catch (NoSuchPeerException e) {
|
||||
throw new NoSuchUserException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Account getDestinationAccount(String destination)
|
||||
throws NoSuchUserException
|
||||
{
|
||||
Optional<Account> account = accountsManager.get(destination);
|
||||
|
||||
if (!account.isPresent() || !account.get().isActive()) {
|
||||
throw new NoSuchUserException(destination);
|
||||
}
|
||||
|
||||
return account.get();
|
||||
}
|
||||
|
||||
private void validateRegistrationIds(Account account, List<IncomingMessage> messages)
|
||||
throws StaleDevicesException
|
||||
{
|
||||
|
@ -326,24 +307,24 @@ public class MessageController {
|
|||
}
|
||||
|
||||
private Optional<byte[]> getMessageBody(IncomingMessage message) {
|
||||
if (Util.isEmpty(message.getBody())) return Optional.absent();
|
||||
if (Util.isEmpty(message.getBody())) return Optional.empty();
|
||||
|
||||
try {
|
||||
return Optional.of(Base64.decode(message.getBody()));
|
||||
} catch (IOException ioe) {
|
||||
logger.debug("Bad B64", ioe);
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<byte[]> getMessageContent(IncomingMessage message) {
|
||||
if (Util.isEmpty(message.getContent())) return Optional.absent();
|
||||
if (Util.isEmpty(message.getContent())) return Optional.empty();
|
||||
|
||||
try {
|
||||
return Optional.of(Base64.decode(message.getContent()));
|
||||
} catch (IOException ioe) {
|
||||
logger.debug("Bad B64", ioe);
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,12 @@ import com.amazonaws.auth.BasicAWSCredentials;
|
|||
import com.amazonaws.services.s3.AmazonS3;
|
||||
import com.amazonaws.services.s3.AmazonS3Client;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.base.Optional;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import org.hibernate.validator.valuehandling.UnwrapValidatedValue;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.auth.Anonymous;
|
||||
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum;
|
||||
import org.whispersystems.textsecuregcm.configuration.ProfilesConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.Profile;
|
||||
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
|
||||
|
@ -22,6 +24,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
|||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
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;
|
||||
|
@ -33,9 +36,11 @@ import javax.ws.rs.core.Response;
|
|||
import java.security.SecureRandom;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@Path("/v1/profile")
|
||||
public class ProfileController {
|
||||
|
||||
|
@ -75,22 +80,29 @@ public class ProfileController {
|
|||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{number}")
|
||||
public Profile getProfile(@Auth Account account,
|
||||
@PathParam("number") String number,
|
||||
@QueryParam("ca") boolean useCaCertificate)
|
||||
public Profile getProfile(@Auth Optional<Account> requestAccount,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@PathParam("number") String number,
|
||||
@QueryParam("ca") boolean useCaCertificate)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
rateLimiters.getProfileLimiter().validate(account.getNumber());
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(number);
|
||||
|
||||
if (!accountProfile.isPresent()) {
|
||||
throw new WebApplicationException(Response.status(404).build());
|
||||
if (!requestAccount.isPresent() && !accessKey.isPresent()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
return new Profile(accountProfile.get().getName(),
|
||||
if (requestAccount.isPresent()) {
|
||||
rateLimiters.getProfileLimiter().validate(requestAccount.get().getNumber());
|
||||
}
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(number);
|
||||
OptionalAccess.verify(requestAccount, accessKey, accountProfile);
|
||||
|
||||
//noinspection ConstantConditions,OptionalGetWithoutIsPresent
|
||||
return new Profile(accountProfile.get().getProfileName(),
|
||||
accountProfile.get().getAvatar(),
|
||||
accountProfile.get().getIdentityKey());
|
||||
accountProfile.get().getIdentityKey(),
|
||||
accountProfile.get().isUnauthenticatedDeliverySupported() ? UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()) : null,
|
||||
accountProfile.get().isUnrestrictedUnidentifiedAccess());
|
||||
}
|
||||
|
||||
@Timed
|
||||
|
@ -98,7 +110,7 @@ public class ProfileController {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/name/{name}")
|
||||
public void setProfile(@Auth Account account, @PathParam("name") @UnwrapValidatedValue(true) @Length(min = 72,max= 72) Optional<String> name) {
|
||||
account.setName(name.orNull());
|
||||
account.setProfileName(name.orElse(null));
|
||||
accountsManager.update(account);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
package org.whispersystems.textsecuregcm.crypto;
|
||||
|
||||
import org.whispersystems.curve25519.Curve25519;
|
||||
import org.whispersystems.curve25519.Curve25519KeyPair;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
|
||||
import static org.whispersystems.curve25519.Curve25519.BEST;
|
||||
|
||||
public class Curve {
|
||||
|
||||
public static final int DJB_TYPE = 0x05;
|
||||
|
||||
public static ECKeyPair generateKeyPair() {
|
||||
Curve25519KeyPair keyPair = Curve25519.getInstance(BEST).generateKeyPair();
|
||||
|
||||
return new ECKeyPair(new DjbECPublicKey(keyPair.getPublicKey()),
|
||||
new DjbECPrivateKey(keyPair.getPrivateKey()));
|
||||
}
|
||||
|
||||
public static ECPublicKey decodePoint(byte[] bytes, int offset)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
if (bytes == null || bytes.length - offset < 1) {
|
||||
throw new InvalidKeyException("No key type identifier");
|
||||
}
|
||||
|
||||
int type = bytes[offset] & 0xFF;
|
||||
|
||||
switch (type) {
|
||||
case Curve.DJB_TYPE:
|
||||
if (bytes.length - offset < 33) {
|
||||
throw new InvalidKeyException("Bad key length: " + bytes.length);
|
||||
}
|
||||
|
||||
byte[] keyBytes = new byte[32];
|
||||
System.arraycopy(bytes, offset+1, keyBytes, 0, keyBytes.length);
|
||||
return new DjbECPublicKey(keyBytes);
|
||||
default:
|
||||
throw new InvalidKeyException("Bad key type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
public static ECPrivateKey decodePrivatePoint(byte[] bytes) {
|
||||
return new DjbECPrivateKey(bytes);
|
||||
}
|
||||
|
||||
public static byte[] calculateAgreement(ECPublicKey publicKey, ECPrivateKey privateKey)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
if (publicKey == null) {
|
||||
throw new InvalidKeyException("public value is null");
|
||||
}
|
||||
|
||||
if (privateKey == null) {
|
||||
throw new InvalidKeyException("private value is null");
|
||||
}
|
||||
|
||||
if (publicKey.getType() != privateKey.getType()) {
|
||||
throw new InvalidKeyException("Public and private keys must be of the same type!");
|
||||
}
|
||||
|
||||
if (publicKey.getType() == DJB_TYPE) {
|
||||
return Curve25519.getInstance(BEST)
|
||||
.calculateAgreement(((DjbECPublicKey) publicKey).getPublicKey(),
|
||||
((DjbECPrivateKey) privateKey).getPrivateKey());
|
||||
} else {
|
||||
throw new InvalidKeyException("Unknown type: " + publicKey.getType());
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] calculateSignature(ECPrivateKey signingKey, byte[] message)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
if (signingKey == null || message == null) {
|
||||
throw new InvalidKeyException("Values must not be null");
|
||||
}
|
||||
|
||||
if (signingKey.getType() == DJB_TYPE) {
|
||||
return Curve25519.getInstance(BEST)
|
||||
.calculateSignature(((DjbECPrivateKey) signingKey).getPrivateKey(), message);
|
||||
} else {
|
||||
throw new InvalidKeyException("Unknown type: " + signingKey.getType());
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean verifySignature(ECPublicKey signingKey, byte[] message, byte[] signature)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
if (signingKey.getType() == DJB_TYPE) {
|
||||
return Curve25519.getInstance(BEST)
|
||||
.verifySignature(((DjbECPublicKey) signingKey).getPublicKey(), message, signature);
|
||||
} else {
|
||||
throw new InvalidKeyException("Unknown type: " + signingKey.getType());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package org.whispersystems.textsecuregcm.crypto;
|
||||
|
||||
public class DjbECPrivateKey implements ECPrivateKey {
|
||||
|
||||
private final byte[] privateKey;
|
||||
|
||||
DjbECPrivateKey(byte[] privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return Curve.DJB_TYPE;
|
||||
}
|
||||
|
||||
public byte[] getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package org.whispersystems.textsecuregcm.crypto;
|
||||
|
||||
import org.whispersystems.textsecuregcm.util.ByteUtil;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class DjbECPublicKey implements ECPublicKey {
|
||||
|
||||
private final byte[] publicKey;
|
||||
|
||||
DjbECPublicKey(byte[] publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
byte[] type = {Curve.DJB_TYPE};
|
||||
return ByteUtil.combine(type, publicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return Curve.DJB_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null) return false;
|
||||
if (!(other instanceof DjbECPublicKey)) return false;
|
||||
|
||||
DjbECPublicKey that = (DjbECPublicKey)other;
|
||||
return Arrays.equals(this.publicKey, that.publicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(publicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ECPublicKey another) {
|
||||
return new BigInteger(publicKey).compareTo(new BigInteger(((DjbECPublicKey)another).publicKey));
|
||||
}
|
||||
|
||||
public byte[] getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package org.whispersystems.textsecuregcm.crypto;
|
||||
|
||||
public class ECKeyPair {
|
||||
|
||||
private final ECPublicKey publicKey;
|
||||
private final ECPrivateKey privateKey;
|
||||
|
||||
ECKeyPair(ECPublicKey publicKey, ECPrivateKey privateKey) {
|
||||
this.publicKey = publicKey;
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
public ECPublicKey getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public ECPrivateKey getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package org.whispersystems.textsecuregcm.crypto;
|
||||
|
||||
public interface ECPrivateKey {
|
||||
public byte[] serialize();
|
||||
public int getType();
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package org.whispersystems.textsecuregcm.crypto;
|
||||
|
||||
public interface ECPublicKey extends Comparable<ECPublicKey> {
|
||||
|
||||
public static final int KEY_SIZE = 33;
|
||||
|
||||
public byte[] serialize();
|
||||
|
||||
public int getType();
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -46,6 +46,12 @@ public class AccountAttributes {
|
|||
@JsonProperty
|
||||
private String pin;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] unidentifiedAccessKey;
|
||||
|
||||
@JsonProperty
|
||||
private boolean unrestrictedUnidentifiedAccess;
|
||||
|
||||
public AccountAttributes() {}
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -91,4 +97,12 @@ public class AccountAttributes {
|
|||
public String getPin() {
|
||||
return pin;
|
||||
}
|
||||
|
||||
public byte[] getUnidentifiedAccessKey() {
|
||||
return unidentifiedAccessKey;
|
||||
}
|
||||
|
||||
public boolean isUnrestrictedUnidentifiedAccess() {
|
||||
return unrestrictedUnidentifiedAccess;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
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 com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class DeliveryCertificate {
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArraySerializer.class)
|
||||
@JsonDeserialize(using = ByteArrayDeserializer.class)
|
||||
private byte[] certificate;
|
||||
|
||||
public DeliveryCertificate(byte[] certificate) {
|
||||
this.certificate = certificate;
|
||||
}
|
||||
|
||||
public DeliveryCertificate() {}
|
||||
|
||||
@VisibleForTesting
|
||||
public byte[] getCertificate() {
|
||||
return certificate;
|
||||
}
|
||||
|
||||
public static class ByteArraySerializer extends JsonSerializer<byte[]> {
|
||||
@Override
|
||||
public void serialize(byte[] bytes, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
|
||||
jsonGenerator.writeString(Base64.encodeBytes(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
public static class ByteArrayDeserializer extends JsonDeserializer<byte[]> {
|
||||
@Override
|
||||
public byte[] deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
|
||||
return Base64.decode(jsonParser.getValueAsString());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,9 +29,6 @@ public class IncomingMessageList {
|
|||
@Valid
|
||||
private List<IncomingMessage> messages;
|
||||
|
||||
@JsonProperty
|
||||
private String relay;
|
||||
|
||||
@JsonProperty
|
||||
private long timestamp;
|
||||
|
||||
|
@ -41,14 +38,6 @@ public class IncomingMessageList {
|
|||
return messages;
|
||||
}
|
||||
|
||||
public String getRelay() {
|
||||
return relay;
|
||||
}
|
||||
|
||||
public void setRelay(String relay) {
|
||||
this.relay = relay;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,6 +3,8 @@ package org.whispersystems.textsecuregcm.entities;
|
|||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class OutgoingMessageEntity {
|
||||
|
||||
@JsonIgnore
|
||||
|
@ -11,6 +13,9 @@ public class OutgoingMessageEntity {
|
|||
@JsonIgnore
|
||||
private boolean cached;
|
||||
|
||||
@JsonProperty
|
||||
private UUID guid;
|
||||
|
||||
@JsonProperty
|
||||
private int type;
|
||||
|
||||
|
@ -32,21 +37,31 @@ public class OutgoingMessageEntity {
|
|||
@JsonProperty
|
||||
private byte[] content;
|
||||
|
||||
@JsonProperty
|
||||
private long serverTimestamp;
|
||||
|
||||
public OutgoingMessageEntity() {}
|
||||
|
||||
public OutgoingMessageEntity(long id, boolean cached, int type, String relay, long timestamp,
|
||||
public OutgoingMessageEntity(long id, boolean cached,
|
||||
UUID guid, int type, String relay, long timestamp,
|
||||
String source, int sourceDevice, byte[] message,
|
||||
byte[] content)
|
||||
byte[] content, long serverTimestamp)
|
||||
{
|
||||
this.id = id;
|
||||
this.cached = cached;
|
||||
this.type = type;
|
||||
this.relay = relay;
|
||||
this.timestamp = timestamp;
|
||||
this.source = source;
|
||||
this.sourceDevice = sourceDevice;
|
||||
this.message = message;
|
||||
this.content = content;
|
||||
this.id = id;
|
||||
this.cached = cached;
|
||||
this.guid = guid;
|
||||
this.type = type;
|
||||
this.relay = relay;
|
||||
this.timestamp = timestamp;
|
||||
this.source = source;
|
||||
this.sourceDevice = sourceDevice;
|
||||
this.message = message;
|
||||
this.content = content;
|
||||
this.serverTimestamp = serverTimestamp;
|
||||
}
|
||||
|
||||
public UUID getGuid() {
|
||||
return guid;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
|
@ -87,4 +102,8 @@ public class OutgoingMessageEntity {
|
|||
return cached;
|
||||
}
|
||||
|
||||
public long getServerTimestamp() {
|
||||
return serverTimestamp;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,12 +18,20 @@ public class Profile {
|
|||
@JsonProperty
|
||||
private String avatar;
|
||||
|
||||
@JsonProperty
|
||||
private String unidentifiedAccess;
|
||||
|
||||
@JsonProperty
|
||||
private boolean unrestrictedUnidentifiedAccess;
|
||||
|
||||
public Profile() {}
|
||||
|
||||
public Profile(String name, String avatar, String identityKey) {
|
||||
this.name = name;
|
||||
this.avatar = avatar;
|
||||
this.identityKey = identityKey;
|
||||
public Profile(String name, String avatar, String identityKey, String unidentifiedAccess, boolean unrestrictedUnidentifiedAccess) {
|
||||
this.name = name;
|
||||
this.avatar = avatar;
|
||||
this.identityKey = identityKey;
|
||||
this.unidentifiedAccess = unidentifiedAccess;
|
||||
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -41,4 +49,14 @@ public class Profile {
|
|||
return avatar;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public String getUnidentifiedAccess() {
|
||||
return unidentifiedAccess;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public boolean isUnrestrictedUnidentifiedAccess() {
|
||||
return unrestrictedUnidentifiedAccess;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,252 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.federation;
|
||||
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.apache.http.config.Registry;
|
||||
import org.apache.http.config.RegistryBuilder;
|
||||
import org.apache.http.conn.socket.ConnectionSocketFactory;
|
||||
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
|
||||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
|
||||
import org.bouncycastle.openssl.PEMReader;
|
||||
import org.glassfish.jersey.client.ClientProperties;
|
||||
import org.glassfish.jersey.client.RequestEntityProcessing;
|
||||
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountCount;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.ws.rs.ProcessingException;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.ByteArrayInputStream;
|
||||
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;
|
||||
|
||||
import io.dropwizard.client.JerseyClientBuilder;
|
||||
import io.dropwizard.client.JerseyClientConfiguration;
|
||||
import io.dropwizard.setup.Environment;
|
||||
|
||||
public class FederatedClient {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FederatedClient.class);
|
||||
|
||||
private static final String USER_COUNT_PATH = "/v1/federation/user_count";
|
||||
private static final String USER_TOKENS_PATH = "/v1/federation/user_tokens/%d";
|
||||
private static final String RELAY_MESSAGE_PATH = "/v1/federation/messages/%s/%d/%s";
|
||||
private static final String PREKEY_PATH_DEVICE_V1 = "/v1/federation/key/%s/%s";
|
||||
private static final String PREKEY_PATH_DEVICE_V2 = "/v2/federation/key/%s/%s";
|
||||
private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d";
|
||||
private static final String RECEIPT_PATH = "/v1/receipt/%s/%d/%s/%d";
|
||||
|
||||
private final FederatedPeer peer;
|
||||
private final Client client;
|
||||
|
||||
public FederatedClient(Environment environment, JerseyClientConfiguration configuration,
|
||||
String federationName, FederatedPeer peer)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
this.client = createClient(environment, configuration, federationName, peer);
|
||||
this.peer = 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 {
|
||||
AttachmentUri response = client.target(peer.getUrl())
|
||||
.path(String.format(ATTACHMENT_URI_PATH, attachmentId))
|
||||
.request()
|
||||
.accept(MediaType.APPLICATION_JSON_TYPE)
|
||||
.get(AttachmentUri.class);
|
||||
|
||||
return response.getLocation();
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("Bad URI", e);
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<PreKeyResponse> getKeysV2(String destination, String device) {
|
||||
try {
|
||||
PreKeyResponse response = client.target(peer.getUrl())
|
||||
.path(String.format(PREKEY_PATH_DEVICE_V2, destination, device))
|
||||
.request()
|
||||
.accept(MediaType.APPLICATION_JSON_TYPE)
|
||||
.get(PreKeyResponse.class);
|
||||
|
||||
return Optional.of(response);
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("PreKey", e);
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public int getUserCount() {
|
||||
try {
|
||||
AccountCount count = client.target(peer.getUrl())
|
||||
.path(USER_COUNT_PATH)
|
||||
.request()
|
||||
.accept(MediaType.APPLICATION_JSON_TYPE)
|
||||
.get(AccountCount.class);
|
||||
|
||||
return count.getCount();
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("User Count", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public List<ClientContact> getUserTokens(int offset) {
|
||||
try {
|
||||
ClientContacts contacts = client.target(peer.getUrl())
|
||||
.path(String.format(USER_TOKENS_PATH, offset))
|
||||
.request()
|
||||
.accept(MediaType.APPLICATION_JSON_TYPE)
|
||||
.get(ClientContacts.class);
|
||||
|
||||
return contacts.getContacts();
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("User Tokens", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMessages(String source, long sourceDeviceId, String destination, IncomingMessageList messages)
|
||||
throws IOException
|
||||
{
|
||||
Response response = null;
|
||||
|
||||
try {
|
||||
response = client.target(peer.getUrl())
|
||||
.path(String.format(RELAY_MESSAGE_PATH, source, sourceDeviceId, destination))
|
||||
.request()
|
||||
.put(Entity.json(messages));
|
||||
|
||||
if (response.getStatus() != 200 && response.getStatus() != 204) {
|
||||
if (response.getStatus() == 411) throw new WebApplicationException(Response.status(413).build());
|
||||
else throw new WebApplicationException(Response.status(response.getStatusInfo()).build());
|
||||
}
|
||||
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("sendMessage", e);
|
||||
throw new IOException(e);
|
||||
} finally {
|
||||
if (response != null) response.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void sendDeliveryReceipt(String source, long sourceDeviceId, String destination, long messageId)
|
||||
throws IOException
|
||||
{
|
||||
Response response = null;
|
||||
|
||||
try {
|
||||
response = client.target(peer.getUrl())
|
||||
.path(String.format(RECEIPT_PATH, source, sourceDeviceId, destination, messageId))
|
||||
.request()
|
||||
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
|
||||
.put(Entity.entity("", MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
if (response.getStatus() != 200 && response.getStatus() != 204) {
|
||||
if (response.getStatus() == 411) throw new WebApplicationException(Response.status(413).build());
|
||||
else throw new WebApplicationException(Response.status(response.getStatusInfo()).build());
|
||||
}
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("sendMessage", e);
|
||||
throw new IOException(e);
|
||||
} finally {
|
||||
if (response != null) response.close();
|
||||
}
|
||||
}
|
||||
|
||||
private Client createClient(Environment environment, JerseyClientConfiguration configuration,
|
||||
String federationName, 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(), new SecureRandom());
|
||||
|
||||
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new DefaultHostnameVerifier());
|
||||
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register("https", sslConnectionSocketFactory).build();
|
||||
|
||||
Client client = new JerseyClientBuilder(environment).using(configuration)
|
||||
.using(registry)
|
||||
.build("FederatedClient");
|
||||
|
||||
client.property(ClientProperties.CONNECT_TIMEOUT, 5000);
|
||||
client.property(ClientProperties.READ_TIMEOUT, 10000);
|
||||
client.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED);
|
||||
client.register(HttpAuthenticationFeature.basic(federationName, peer.getAuthenticationToken()));
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private KeyStore initializeTrustStore(String name, String pemCertificate)
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.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;
|
||||
|
||||
import io.dropwizard.client.JerseyClientConfiguration;
|
||||
import io.dropwizard.setup.Environment;
|
||||
|
||||
public class FederatedClientManager {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FederatedClientManager.class);
|
||||
|
||||
private final HashMap<String, FederatedClient> clients = new HashMap<>();
|
||||
|
||||
public FederatedClientManager(Environment environment,
|
||||
JerseyClientConfiguration clientConfig,
|
||||
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(environment, clientConfig, 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());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.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;
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.federation;
|
||||
|
||||
|
||||
public class NoSuchPeerException extends Exception {
|
||||
public NoSuchPeerException(String name) {
|
||||
super(name);
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package org.whispersystems.textsecuregcm.federation;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.google.common.base.Optional;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
public class NonLimitedAccount extends Account {
|
||||
|
||||
@JsonIgnore
|
||||
private final String number;
|
||||
|
||||
@JsonIgnore
|
||||
private final String relay;
|
||||
|
||||
@JsonIgnore
|
||||
private final long deviceId;
|
||||
|
||||
public NonLimitedAccount(String number, long deviceId, String relay) {
|
||||
this.number = number;
|
||||
this.deviceId = deviceId;
|
||||
this.relay = relay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRateLimited() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getRelay() {
|
||||
return Optional.of(relay);
|
||||
}
|
||||
|
||||
@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(), false, false, "NA"));
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ import com.codahale.metrics.Meter;
|
|||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
@ -37,6 +36,7 @@ import org.whispersystems.textsecuregcm.util.Constants;
|
|||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
|
|
@ -4,7 +4,6 @@ import com.codahale.metrics.Meter;
|
|||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.RatioGauge;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.redis.LuaScript;
|
||||
|
@ -21,6 +20,7 @@ import java.io.IOException;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
@ -28,7 +28,6 @@ import io.dropwizard.lifecycle.Managed;
|
|||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.exceptions.JedisException;
|
||||
|
||||
@SuppressWarnings("Guava")
|
||||
public class ApnFallbackManager implements Managed, Runnable {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ApnFallbackManager.class);
|
||||
|
@ -167,19 +166,19 @@ public class ApnFallbackManager implements Managed, Runnable {
|
|||
|
||||
private Optional<Pair<String, Long>> getSeparated(String encoded) {
|
||||
try {
|
||||
if (encoded == null) return Optional.absent();
|
||||
if (encoded == null) return Optional.empty();
|
||||
|
||||
String[] parts = encoded.split(":");
|
||||
|
||||
if (parts.length != 2) {
|
||||
logger.warn("Got strange encoded number: " + encoded);
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(new Pair<>(parts[0], Long.parseLong(parts[1])));
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Badly formatted: " + encoded, e);
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import com.codahale.metrics.Meter;
|
|||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
@ -22,6 +21,7 @@ import org.whispersystems.textsecuregcm.util.Constants;
|
|||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -174,7 +174,7 @@ public class GCMSender implements Managed {
|
|||
}
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private void markOutboundMeter(String key) {
|
||||
|
|
|
@ -1,64 +1,33 @@
|
|||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public class ReceiptSender {
|
||||
|
||||
private final PushSender pushSender;
|
||||
private final FederatedClientManager federatedClientManager;
|
||||
private final AccountsManager accountManager;
|
||||
private final PushSender pushSender;
|
||||
private final AccountsManager accountManager;
|
||||
|
||||
public ReceiptSender(AccountsManager accountManager,
|
||||
PushSender pushSender,
|
||||
FederatedClientManager federatedClientManager)
|
||||
public ReceiptSender(AccountsManager accountManager,
|
||||
PushSender pushSender)
|
||||
{
|
||||
this.federatedClientManager = federatedClientManager;
|
||||
this.accountManager = accountManager;
|
||||
this.pushSender = pushSender;
|
||||
this.accountManager = accountManager;
|
||||
this.pushSender = pushSender;
|
||||
}
|
||||
|
||||
public void sendReceipt(Account source, String destination,
|
||||
long messageId, Optional<String> relay)
|
||||
throws IOException, NoSuchUserException,
|
||||
NotPushRegisteredException, TransientPushFailureException
|
||||
public void sendReceipt(Account source, String destination, long messageId)
|
||||
throws NoSuchUserException, NotPushRegisteredException
|
||||
{
|
||||
if (source.getNumber().equals(destination)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (relay.isPresent() && !relay.get().isEmpty()) {
|
||||
sendRelayedReceipt(source, destination, messageId, relay.get());
|
||||
} else {
|
||||
sendDirectReceipt(source, destination, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendRelayedReceipt(Account source, String destination, long messageId, String relay)
|
||||
throws NoSuchUserException, IOException
|
||||
{
|
||||
try {
|
||||
federatedClientManager.getClient(relay)
|
||||
.sendDeliveryReceipt(source.getNumber(),
|
||||
source.getAuthenticatedDevice().get().getId(),
|
||||
destination, messageId);
|
||||
} catch (NoSuchPeerException e) {
|
||||
throw new NoSuchUserException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDirectReceipt(Account source, String destination, long messageId)
|
||||
throws NotPushRegisteredException, TransientPushFailureException, NoSuchUserException
|
||||
{
|
||||
Account destinationAccount = getDestinationAccount(destination);
|
||||
Set<Device> destinationDevices = destinationAccount.getDevices();
|
||||
Envelope.Builder message = Envelope.newBuilder()
|
||||
|
|
|
@ -17,13 +17,14 @@
|
|||
package org.whispersystems.textsecuregcm.sms;
|
||||
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.twilio.sdk.TwilioRestException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class SmsSender {
|
||||
|
||||
static final String SMS_IOS_VERIFICATION_TEXT = "Your Signal verification code: %s\n\nOr tap: sgnl://verify/%s";
|
||||
|
|
|
@ -19,7 +19,6 @@ package org.whispersystems.textsecuregcm.sms;
|
|||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.google.common.base.Optional;
|
||||
import com.twilio.sdk.TwilioRestClient;
|
||||
import com.twilio.sdk.TwilioRestException;
|
||||
import com.twilio.sdk.resource.factory.CallFactory;
|
||||
|
@ -36,10 +35,12 @@ import java.util.HashMap;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class TwilioSmsSender {
|
||||
|
||||
public static final String SAY_TWIML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
|
@ -81,7 +82,7 @@ public class TwilioSmsSender {
|
|||
messageParams.add(new BasicNameValuePair("MessagingServiceSid", messagingServicesId));
|
||||
}
|
||||
|
||||
if ("ios".equals(clientType.orNull())) {
|
||||
if ("ios".equals(clientType.orElse(null))) {
|
||||
messageParams.add(new BasicNameValuePair("Body", String.format(SmsSender.SMS_IOS_VERIFICATION_TEXT, verificationCode, verificationCode)));
|
||||
} else {
|
||||
messageParams.add(new BasicNameValuePair("Body", String.format(SmsSender.SMS_VERIFICATION_TEXT, verificationCode)));
|
||||
|
|
|
@ -20,13 +20,15 @@ package org.whispersystems.textsecuregcm.storage;
|
|||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Optional;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import java.security.Principal;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class Account {
|
||||
public class Account implements Principal {
|
||||
|
||||
public static final int MEMCACHE_VERION = 5;
|
||||
|
||||
|
@ -51,19 +53,26 @@ public class Account {
|
|||
@JsonProperty
|
||||
private String pin;
|
||||
|
||||
@JsonProperty("uak")
|
||||
private byte[] unidentifiedAccessKey;
|
||||
|
||||
@JsonProperty("uua")
|
||||
private boolean unrestrictedUnidentifiedAccess;
|
||||
|
||||
@JsonIgnore
|
||||
private Device authenticatedDevice;
|
||||
|
||||
public Account() {}
|
||||
|
||||
@VisibleForTesting
|
||||
public Account(String number, Set<Device> devices) {
|
||||
this.number = number;
|
||||
this.devices = devices;
|
||||
public Account(String number, Set<Device> devices, byte[] unidentifiedAccessKey) {
|
||||
this.number = number;
|
||||
this.devices = devices;
|
||||
this.unidentifiedAccessKey = unidentifiedAccessKey;
|
||||
}
|
||||
|
||||
public Optional<Device> getAuthenticatedDevice() {
|
||||
return Optional.fromNullable(authenticatedDevice);
|
||||
return Optional.ofNullable(authenticatedDevice);
|
||||
}
|
||||
|
||||
public void setAuthenticatedDevice(Device device) {
|
||||
|
@ -84,7 +93,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, false, false, "NA"));
|
||||
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, "NA", false));
|
||||
}
|
||||
|
||||
public Set<Device> getDevices() {
|
||||
|
@ -102,27 +111,11 @@ public class Account {
|
|||
}
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public boolean isVoiceSupported() {
|
||||
for (Device device : devices) {
|
||||
if (device.isActive() && device.isVoiceSupported()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isVideoSupported() {
|
||||
for (Device device : devices) {
|
||||
if (device.isActive() && device.isVideoSupported()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
public boolean isUnauthenticatedDeliverySupported() {
|
||||
return devices.stream().filter(Device::isActive).allMatch(Device::isUnauthenticatedDeliverySupported);
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
|
@ -161,7 +154,7 @@ public class Account {
|
|||
}
|
||||
|
||||
public Optional<String> getRelay() {
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public void setIdentityKey(String identityKey) {
|
||||
|
@ -184,11 +177,11 @@ public class Account {
|
|||
return lastSeen;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
public String getProfileName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
public void setProfileName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
@ -209,10 +202,40 @@ public class Account {
|
|||
}
|
||||
|
||||
public Optional<String> getPin() {
|
||||
return Optional.fromNullable(pin);
|
||||
return Optional.ofNullable(pin);
|
||||
}
|
||||
|
||||
public void setPin(String pin) {
|
||||
this.pin = pin;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getUnidentifiedAccessKey() {
|
||||
return Optional.ofNullable(unidentifiedAccessKey);
|
||||
}
|
||||
|
||||
public void setUnidentifiedAccessKey(byte[] unidentifiedAccessKey) {
|
||||
this.unidentifiedAccessKey = unidentifiedAccessKey;
|
||||
}
|
||||
|
||||
public boolean isUnrestrictedUnidentifiedAccess() {
|
||||
return unrestrictedUnidentifiedAccess;
|
||||
}
|
||||
|
||||
public void setUnrestrictedUnidentifiedAccess(boolean unrestrictedUnidentifiedAccess) {
|
||||
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
|
||||
}
|
||||
|
||||
// Principal implementation
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public String getName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public boolean implies(Subject subject) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ package org.whispersystems.textsecuregcm.storage;
|
|||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
|
@ -30,6 +29,7 @@ import org.whispersystems.textsecuregcm.util.Util;
|
|||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import redis.clients.jedis.Jedis;
|
||||
|
||||
|
@ -79,7 +79,7 @@ public class AccountsManager {
|
|||
Optional<Account> account = memcacheGet(number);
|
||||
|
||||
if (!account.isPresent()) {
|
||||
account = Optional.fromNullable(accounts.get(number));
|
||||
account = Optional.ofNullable(accounts.get(number));
|
||||
|
||||
if (account.isPresent()) {
|
||||
memcacheSet(number, account.get());
|
||||
|
@ -99,7 +99,7 @@ public class AccountsManager {
|
|||
private void updateDirectory(Account account) {
|
||||
if (account.isActive()) {
|
||||
byte[] token = Util.getContactToken(account.getNumber());
|
||||
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported(), account.isVideoSupported());
|
||||
ClientContact clientContact = new ClientContact(token, null, true, true);
|
||||
directory.add(clientContact);
|
||||
} else {
|
||||
directory.remove(account.getNumber());
|
||||
|
@ -123,10 +123,10 @@ public class AccountsManager {
|
|||
String json = jedis.get(getKey(number));
|
||||
|
||||
if (json != null) return Optional.of(mapper.readValue(json, Account.class));
|
||||
else return Optional.absent();
|
||||
else return Optional.empty();
|
||||
} catch (IOException e) {
|
||||
logger.warn("AccountsManager", "Deserialization error", e);
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -70,40 +70,36 @@ public class Device {
|
|||
@JsonProperty
|
||||
private long created;
|
||||
|
||||
@JsonProperty
|
||||
private boolean voice;
|
||||
|
||||
@JsonProperty
|
||||
private boolean video;
|
||||
|
||||
@JsonProperty
|
||||
private String userAgent;
|
||||
|
||||
@JsonProperty
|
||||
private boolean unauthenticatedDelivery;
|
||||
|
||||
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, boolean voice, boolean video,
|
||||
String userAgent)
|
||||
long lastSeen, long created, String userAgent,
|
||||
boolean unauthenticatedDelivery)
|
||||
{
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.authToken = authToken;
|
||||
this.salt = salt;
|
||||
this.signalingKey = signalingKey;
|
||||
this.gcmId = gcmId;
|
||||
this.apnId = apnId;
|
||||
this.voipApnId = voipApnId;
|
||||
this.fetchesMessages = fetchesMessages;
|
||||
this.registrationId = registrationId;
|
||||
this.signedPreKey = signedPreKey;
|
||||
this.lastSeen = lastSeen;
|
||||
this.created = created;
|
||||
this.voice = voice;
|
||||
this.video = video;
|
||||
this.userAgent = userAgent;
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.authToken = authToken;
|
||||
this.salt = salt;
|
||||
this.signalingKey = signalingKey;
|
||||
this.gcmId = gcmId;
|
||||
this.apnId = apnId;
|
||||
this.voipApnId = voipApnId;
|
||||
this.fetchesMessages = fetchesMessages;
|
||||
this.registrationId = registrationId;
|
||||
this.signedPreKey = signedPreKey;
|
||||
this.lastSeen = lastSeen;
|
||||
this.created = created;
|
||||
this.userAgent = userAgent;
|
||||
this.unauthenticatedDelivery = unauthenticatedDelivery;
|
||||
}
|
||||
|
||||
public String getApnId() {
|
||||
|
@ -170,20 +166,12 @@ public class Device {
|
|||
this.name = name;
|
||||
}
|
||||
|
||||
public boolean isVoiceSupported() {
|
||||
return voice;
|
||||
public boolean isUnauthenticatedDeliverySupported() {
|
||||
return unauthenticatedDelivery;
|
||||
}
|
||||
|
||||
public void setVoiceSupported(boolean voice) {
|
||||
this.voice = voice;
|
||||
}
|
||||
|
||||
public boolean isVideoSupported() {
|
||||
return video;
|
||||
}
|
||||
|
||||
public void setVideoSupported(boolean video) {
|
||||
this.video = video;
|
||||
public void setUnauthenticatedDeliverySupported(boolean unauthenticatedDelivery) {
|
||||
this.unauthenticatedDelivery = unauthenticatedDelivery;
|
||||
}
|
||||
|
||||
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -20,7 +20,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
|
@ -32,6 +31,7 @@ import org.whispersystems.textsecuregcm.util.Util;
|
|||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.Pipeline;
|
||||
|
@ -102,14 +102,14 @@ public class DirectoryManager {
|
|||
byte[] result = jedis.hget(DIRECTORY_KEY, token);
|
||||
|
||||
if (result == null) {
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
|
||||
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.voice, tokenValue.video));
|
||||
} catch (IOException e) {
|
||||
logger.warn("JSON Error", e);
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -205,7 +205,7 @@ public class DirectoryManager {
|
|||
byte[] result = response.get();
|
||||
|
||||
if (result == null) {
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright (C) 2018 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -21,8 +21,6 @@ import com.codahale.metrics.MetricRegistry;
|
|||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.Timer;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Optional;
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
|
@ -37,10 +35,13 @@ import javax.ws.rs.ProcessingException;
|
|||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class DirectoryReconciler implements Managed, Runnable {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DirectoryReconciler.class);
|
||||
|
@ -62,7 +63,6 @@ public class DirectoryReconciler implements Managed, Runnable {
|
|||
private final int chunkSize;
|
||||
private final long chunkIntervalMs;
|
||||
private final String workerId;
|
||||
private final SecureRandom random;
|
||||
|
||||
private boolean running;
|
||||
private boolean finished;
|
||||
|
@ -72,21 +72,15 @@ public class DirectoryReconciler implements Managed, Runnable {
|
|||
DirectoryManager directoryManager,
|
||||
Accounts accounts,
|
||||
int chunkSize,
|
||||
long chunkIntervalMs) {
|
||||
long chunkIntervalMs)
|
||||
{
|
||||
this.accounts = accounts;
|
||||
this.directoryManager = directoryManager;
|
||||
this.reconciliationClient = reconciliationClient;
|
||||
this.reconciliationCache = reconciliationCache;
|
||||
this.chunkSize = chunkSize;
|
||||
this.chunkIntervalMs = chunkIntervalMs;
|
||||
this.random = new SecureRandom();
|
||||
this.workerId = generateWorkerId(random);
|
||||
}
|
||||
|
||||
private static String generateWorkerId(SecureRandom random) {
|
||||
byte[] workerIdBytes = new byte[16];
|
||||
random.nextBytes(workerIdBytes);
|
||||
return Hex.toString(workerIdBytes);
|
||||
this.workerId = Hex.toString(Util.generateSecretBytes(16));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -141,6 +135,94 @@ public class DirectoryReconciler implements Managed, Runnable {
|
|||
return intervalMs;
|
||||
}
|
||||
|
||||
private boolean processChunk() {
|
||||
Optional<String> fromNumber = reconciliationCache.getLastNumber();
|
||||
List<Account> chunkAccounts = readChunk(fromNumber, chunkSize);
|
||||
|
||||
updateDirectoryCache(chunkAccounts);
|
||||
|
||||
DirectoryReconciliationRequest request = createChunkRequest(fromNumber, chunkAccounts);
|
||||
DirectoryReconciliationResponse sendChunkResponse = sendChunk(request);
|
||||
|
||||
if (sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.MISSING || request.getToNumber() == null) {
|
||||
reconciliationCache.clearAccelerate();
|
||||
}
|
||||
|
||||
if (sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.OK) {
|
||||
reconciliationCache.setLastNumber(Optional.ofNullable(request.getToNumber()));
|
||||
} else if (sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.MISSING) {
|
||||
reconciliationCache.setLastNumber(Optional.empty());
|
||||
}
|
||||
|
||||
return sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.OK;
|
||||
}
|
||||
|
||||
private List<Account> readChunk(Optional<String> fromNumber, int chunkSize) {
|
||||
try (Timer.Context timer = readChunkTimer.time()) {
|
||||
Optional<List<Account>> chunkAccounts;
|
||||
|
||||
if (fromNumber.isPresent()) {
|
||||
chunkAccounts = Optional.ofNullable(accounts.getAllFrom(fromNumber.get(), chunkSize));
|
||||
} else {
|
||||
chunkAccounts = Optional.ofNullable(accounts.getAllFrom(chunkSize));
|
||||
}
|
||||
|
||||
return chunkAccounts.orElse(Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDirectoryCache(List<Account> accounts) {
|
||||
if (accounts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BatchOperationHandle batchOperation = directoryManager.startBatchOperation();
|
||||
|
||||
try {
|
||||
for (Account account : accounts) {
|
||||
if (account.isActive()) {
|
||||
byte[] token = Util.getContactToken(account.getNumber());
|
||||
ClientContact clientContact = new ClientContact(token, null, true, true);
|
||||
|
||||
directoryManager.add(batchOperation, clientContact);
|
||||
} else {
|
||||
directoryManager.remove(batchOperation, account.getNumber());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
directoryManager.stopBatchOperation(batchOperation);
|
||||
}
|
||||
}
|
||||
|
||||
private DirectoryReconciliationRequest createChunkRequest(Optional<String> fromNumber, List<Account> accounts) {
|
||||
List<String> numbers = accounts.stream()
|
||||
.filter(Account::isActive)
|
||||
.map(Account::getNumber)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Optional<String> toNumber = Optional.empty();
|
||||
if (!accounts.isEmpty()) {
|
||||
toNumber = Optional.of(accounts.get(accounts.size() - 1).getNumber());
|
||||
}
|
||||
|
||||
return new DirectoryReconciliationRequest(fromNumber.orElse(null), toNumber.orElse(null), numbers);
|
||||
}
|
||||
|
||||
private DirectoryReconciliationResponse sendChunk(DirectoryReconciliationRequest request) {
|
||||
try (Timer.Context timer = sendChunkTimer.time()) {
|
||||
DirectoryReconciliationResponse response = reconciliationClient.sendChunk(request);
|
||||
if (response.getStatus() != DirectoryReconciliationResponse.Status.OK) {
|
||||
sendChunkErrorMeter.mark();
|
||||
logger.warn("reconciliation error: " + response.getStatus());
|
||||
}
|
||||
return response;
|
||||
} catch (ProcessingException ex) {
|
||||
sendChunkErrorMeter.mark();
|
||||
logger.warn("request error: ", ex);
|
||||
throw new ProcessingException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized boolean sleepWhileRunning(long delayMs) {
|
||||
long startTimeMs = System.currentTimeMillis();
|
||||
while (running && delayMs > 0) {
|
||||
|
@ -162,96 +244,8 @@ public class DirectoryReconciler implements Managed, Runnable {
|
|||
}
|
||||
|
||||
private long getDelayWithJitter(long delayMs) {
|
||||
long randomJitterMs = (long) (random.nextDouble() * JITTER_MAX * delayMs);
|
||||
long randomJitterMs = (long) (new SecureRandom().nextDouble() * JITTER_MAX * delayMs);
|
||||
return delayMs + randomJitterMs;
|
||||
}
|
||||
|
||||
private boolean processChunk() {
|
||||
Optional<String> fromNumber = reconciliationCache.getLastNumber();
|
||||
List<Account> chunkAccounts = readChunk(fromNumber, chunkSize);
|
||||
|
||||
writeChunktoDirectoryCache(chunkAccounts);
|
||||
|
||||
DirectoryReconciliationRequest request = createChunkRequest(fromNumber, chunkAccounts);
|
||||
DirectoryReconciliationResponse sendChunkResponse = sendChunk(request);
|
||||
|
||||
if (sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.MISSING ||
|
||||
request.getToNumber() == null) {
|
||||
reconciliationCache.clearAccelerate();
|
||||
}
|
||||
|
||||
if (sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.OK) {
|
||||
reconciliationCache.setLastNumber(Optional.fromNullable(request.getToNumber()));
|
||||
} else if (sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.MISSING) {
|
||||
reconciliationCache.setLastNumber(Optional.absent());
|
||||
}
|
||||
|
||||
return sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.OK;
|
||||
}
|
||||
|
||||
private List<Account> readChunk(Optional<String> fromNumber, int chunkSize) {
|
||||
try (Timer.Context timer = readChunkTimer.time()) {
|
||||
Optional<List<Account>> chunkAccounts;
|
||||
|
||||
if (fromNumber.isPresent()) {
|
||||
chunkAccounts = Optional.fromNullable(accounts.getAllFrom(fromNumber.get(), chunkSize));
|
||||
} else {
|
||||
chunkAccounts = Optional.fromNullable(accounts.getAllFrom(chunkSize));
|
||||
}
|
||||
|
||||
return chunkAccounts.or(Collections::emptyList);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeChunktoDirectoryCache(List<Account> accounts) {
|
||||
if (accounts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BatchOperationHandle batchOperation = directoryManager.startBatchOperation();
|
||||
try {
|
||||
for (Account account : accounts) {
|
||||
if (account.isActive()) {
|
||||
byte[] token = Util.getContactToken(account.getNumber());
|
||||
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported(), account.isVideoSupported());
|
||||
|
||||
directoryManager.add(batchOperation, clientContact);
|
||||
} else {
|
||||
directoryManager.remove(batchOperation, account.getNumber());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
directoryManager.stopBatchOperation(batchOperation);
|
||||
}
|
||||
}
|
||||
|
||||
private DirectoryReconciliationRequest createChunkRequest(Optional<String> fromNumber, List<Account> accounts) {
|
||||
List<String> numbers = accounts.stream()
|
||||
.filter(Account::isActive)
|
||||
.map(Account::getNumber)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Optional<String> toNumber = Optional.absent();
|
||||
if (!accounts.isEmpty()) {
|
||||
toNumber = Optional.of(accounts.get(accounts.size() - 1).getNumber());
|
||||
}
|
||||
|
||||
return new DirectoryReconciliationRequest(fromNumber.orNull(), toNumber.orNull(), numbers);
|
||||
}
|
||||
|
||||
private DirectoryReconciliationResponse sendChunk(DirectoryReconciliationRequest request) {
|
||||
try (Timer.Context timer = sendChunkTimer.time()) {
|
||||
DirectoryReconciliationResponse response = reconciliationClient.sendChunk(request);
|
||||
if (response.getStatus() != DirectoryReconciliationResponse.Status.OK) {
|
||||
sendChunkErrorMeter.mark();
|
||||
logger.warn("reconciliation error: " + response.getStatus());
|
||||
}
|
||||
return response;
|
||||
} catch (ProcessingException ex) {
|
||||
sendChunkErrorMeter.mark();
|
||||
logger.warn("request error: ", ex);
|
||||
throw new ProcessingException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
*/
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.whispersystems.textsecuregcm.redis.LuaScript;
|
||||
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
|
||||
import redis.clients.jedis.Jedis;
|
||||
|
@ -24,7 +23,9 @@ import redis.clients.jedis.Jedis;
|
|||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class DirectoryReconciliationCache {
|
||||
|
||||
private static final String ACTIVE_WORKER_KEY = "directory_reconciliation_active_worker";
|
||||
|
@ -62,7 +63,7 @@ public class DirectoryReconciliationCache {
|
|||
|
||||
public Optional<String> getLastNumber() {
|
||||
try (Jedis jedis = jedisPool.getWriteResource()) {
|
||||
return Optional.fromNullable(jedis.get(LAST_NUMBER_KEY));
|
||||
return Optional.ofNullable(jedis.get(LAST_NUMBER_KEY));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
*/
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.skife.jdbi.v2.SQLStatement;
|
||||
import org.skife.jdbi.v2.StatementContext;
|
||||
import org.skife.jdbi.v2.TransactionIsolationLevel;
|
||||
|
@ -41,6 +40,7 @@ import java.sql.ResultSet;
|
|||
import java.sql.SQLException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class Keys {
|
||||
|
||||
|
@ -84,7 +84,7 @@ public abstract class Keys {
|
|||
if (record != null && !record.isLastResort()) {
|
||||
removeKey(record.getId());
|
||||
} else if (record == null) {
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
List<KeyRecord> results = new LinkedList<>();
|
||||
|
@ -106,7 +106,7 @@ public abstract class Keys {
|
|||
}
|
||||
|
||||
if (preKeys != null) return Optional.of(preKeys);
|
||||
else return Optional.absent();
|
||||
else return Optional.empty();
|
||||
}
|
||||
|
||||
@SqlUpdate("VACUUM keys")
|
||||
|
|
|
@ -21,15 +21,18 @@ import java.lang.annotation.Target;
|
|||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class Messages {
|
||||
|
||||
static final int RESULT_SET_CHUNK_SIZE = 100;
|
||||
|
||||
private static final String ID = "id";
|
||||
private static final String GUID = "guid";
|
||||
private static final String TYPE = "type";
|
||||
private static final String RELAY = "relay";
|
||||
private static final String TIMESTAMP = "timestamp";
|
||||
private static final String SERVER_TIMESTAMP = "server_timestamp";
|
||||
private static final String SOURCE = "source";
|
||||
private static final String SOURCE_DEVICE = "source_device";
|
||||
private static final String DESTINATION = "destination";
|
||||
|
@ -37,11 +40,12 @@ public abstract class Messages {
|
|||
private static final String MESSAGE = "message";
|
||||
private static final String CONTENT = "content";
|
||||
|
||||
@SqlUpdate("INSERT INTO messages (" + TYPE + ", " + RELAY + ", " + TIMESTAMP + ", " + SOURCE + ", " + SOURCE_DEVICE + ", " + DESTINATION + ", " + DESTINATION_DEVICE + ", " + MESSAGE + ", " + CONTENT + ") " +
|
||||
"VALUES (:type, :relay, :timestamp, :source, :source_device, :destination, :destination_device, :message, :content)")
|
||||
abstract void store(@MessageBinder Envelope message,
|
||||
@Bind("destination") String destination,
|
||||
@Bind("destination_device") long destinationDevice);
|
||||
@SqlUpdate("INSERT INTO messages (" + GUID + ", " + TYPE + ", " + RELAY + ", " + TIMESTAMP + ", " + SERVER_TIMESTAMP + ", " + SOURCE + ", " + SOURCE_DEVICE + ", " + DESTINATION + ", " + DESTINATION_DEVICE + ", " + MESSAGE + ", " + CONTENT + ") " +
|
||||
"VALUES (:guid, :type, :relay, :timestamp, :server_timestamp, :source, :source_device, :destination, :destination_device, :message, :content)")
|
||||
abstract void store(@Bind("guid") UUID guid,
|
||||
@MessageBinder Envelope message,
|
||||
@Bind("destination") String destination,
|
||||
@Bind("destination_device") long destinationDevice);
|
||||
|
||||
@Mapper(MessageMapper.class)
|
||||
@SqlQuery("SELECT * FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device ORDER BY " + TIMESTAMP + " ASC LIMIT " + RESULT_SET_CHUNK_SIZE)
|
||||
|
@ -55,6 +59,10 @@ public abstract class Messages {
|
|||
@Bind("source") String source,
|
||||
@Bind("timestamp") long timestamp);
|
||||
|
||||
@Mapper(MessageMapper.class)
|
||||
@SqlQuery("DELETE FROM messages WHERE "+ ID + " IN (SELECT " + ID + " FROM MESSAGES WHERE " + GUID + " = :guid AND " + DESTINATION + " = :destination ORDER BY " + ID + " LIMIT 1) RETURNING *")
|
||||
abstract OutgoingMessageEntity remove(@Bind("destination") String destination, @Bind("guid") UUID guid);
|
||||
|
||||
@Mapper(MessageMapper.class)
|
||||
@SqlUpdate("DELETE FROM messages WHERE " + ID + " = :id AND " + DESTINATION + " = :destination")
|
||||
abstract void remove(@Bind("destination") String destination, @Bind("id") long id);
|
||||
|
@ -79,6 +87,7 @@ public abstract class Messages {
|
|||
|
||||
int type = resultSet.getInt(TYPE);
|
||||
byte[] legacyMessage = resultSet.getBytes(MESSAGE);
|
||||
String guid = resultSet.getString(GUID);
|
||||
|
||||
if (type == Envelope.Type.RECEIPT_VALUE && legacyMessage == null) {
|
||||
/// XXX - REMOVE AFTER 10/01/15
|
||||
|
@ -87,13 +96,15 @@ public abstract class Messages {
|
|||
|
||||
return new OutgoingMessageEntity(resultSet.getLong(ID),
|
||||
false,
|
||||
guid == null ? null : UUID.fromString(guid),
|
||||
type,
|
||||
resultSet.getString(RELAY),
|
||||
resultSet.getLong(TIMESTAMP),
|
||||
resultSet.getString(SOURCE),
|
||||
resultSet.getInt(SOURCE_DEVICE),
|
||||
legacyMessage,
|
||||
resultSet.getBytes(CONTENT));
|
||||
resultSet.getBytes(CONTENT),
|
||||
resultSet.getLong(SERVER_TIMESTAMP));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,8 +124,9 @@ public abstract class Messages {
|
|||
sql.bind(TYPE, message.getType().getNumber());
|
||||
sql.bind(RELAY, message.getRelay());
|
||||
sql.bind(TIMESTAMP, message.getTimestamp());
|
||||
sql.bind(SOURCE, message.getSource());
|
||||
sql.bind(SOURCE_DEVICE, message.getSourceDevice());
|
||||
sql.bind(SERVER_TIMESTAMP, message.getServerTimestamp());
|
||||
sql.bind(SOURCE, message.hasSource() ? message.getSource() : null);
|
||||
sql.bind(SOURCE_DEVICE, message.hasSourceDevice() ? message.getSourceDevice() : null);
|
||||
sql.bind(MESSAGE, message.hasLegacyMessage() ? message.getLegacyMessage().toByteArray() : null);
|
||||
sql.bind(CONTENT, message.hasContent() ? message.getContent().toByteArray() : null);
|
||||
}
|
||||
|
@ -122,6 +134,4 @@ public abstract class Messages {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import com.codahale.metrics.Histogram;
|
|||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.Timer;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -25,7 +24,9 @@ import java.util.Collections;
|
|||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
|
@ -43,6 +44,7 @@ public class MessagesCache implements Managed {
|
|||
private static final Timer insertTimer = metricRegistry.timer(name(MessagesCache.class, "insert" ));
|
||||
private static final Timer removeByIdTimer = metricRegistry.timer(name(MessagesCache.class, "removeById" ));
|
||||
private static final Timer removeByNameTimer = metricRegistry.timer(name(MessagesCache.class, "removeByName"));
|
||||
private static final Timer removeByGuidTimer = metricRegistry.timer(name(MessagesCache.class, "removeByGuid"));
|
||||
private static final Timer getTimer = metricRegistry.timer(name(MessagesCache.class, "get" ));
|
||||
private static final Timer clearAccountTimer = metricRegistry.timer(name(MessagesCache.class, "clearAccount"));
|
||||
private static final Timer clearDeviceTimer = metricRegistry.timer(name(MessagesCache.class, "clearDevice" ));
|
||||
|
@ -67,11 +69,13 @@ public class MessagesCache implements Managed {
|
|||
this.delayMinutes = delayMinutes;
|
||||
}
|
||||
|
||||
public void insert(String destination, long destinationDevice, Envelope message) {
|
||||
public void insert(UUID guid, String destination, long destinationDevice, Envelope message) {
|
||||
message = message.toBuilder().setServerGuid(guid.toString()).build();
|
||||
|
||||
Timer.Context timer = insertTimer.time();
|
||||
|
||||
try {
|
||||
insertOperation.insert(destination, destinationDevice, System.currentTimeMillis(), message);
|
||||
insertOperation.insert(guid, destination, destinationDevice, System.currentTimeMillis(), message);
|
||||
} finally {
|
||||
timer.stop();
|
||||
}
|
||||
|
@ -103,7 +107,26 @@ public class MessagesCache implements Managed {
|
|||
timer.stop();
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Optional<OutgoingMessageEntity> remove(String destination, long destinationDevice, UUID guid) {
|
||||
Timer.Context timer = removeByGuidTimer.time();
|
||||
|
||||
try {
|
||||
byte[] serialized = removeOperation.remove(destination, destinationDevice, guid);
|
||||
|
||||
if (serialized != null) {
|
||||
Envelope envelope = Envelope.parseFrom(serialized);
|
||||
return Optional.of(constructEntityFromEnvelope(0, envelope));
|
||||
}
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
logger.warn("Failed to parse envelope", e);
|
||||
} finally {
|
||||
timer.stop();
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public List<OutgoingMessageEntity> get(String destination, long destinationDevice, int limit) {
|
||||
|
@ -175,13 +198,15 @@ public class MessagesCache implements Managed {
|
|||
|
||||
private OutgoingMessageEntity constructEntityFromEnvelope(long id, Envelope envelope) {
|
||||
return new OutgoingMessageEntity(id, true,
|
||||
envelope.hasServerGuid() ? UUID.fromString(envelope.getServerGuid()) : null,
|
||||
envelope.getType().getNumber(),
|
||||
envelope.getRelay(),
|
||||
envelope.getTimestamp(),
|
||||
envelope.getSource(),
|
||||
envelope.getSourceDevice(),
|
||||
envelope.hasLegacyMessage() ? envelope.getLegacyMessage().toByteArray() : null,
|
||||
envelope.hasContent() ? envelope.getContent().toByteArray() : null);
|
||||
envelope.hasContent() ? envelope.getContent().toByteArray() : null,
|
||||
envelope.hasServerTimestamp() ? envelope.getServerTimestamp() : 0);
|
||||
}
|
||||
|
||||
private static class Key {
|
||||
|
@ -247,12 +272,12 @@ public class MessagesCache implements Managed {
|
|||
this.insert = LuaScript.fromResource(jedisPool, "lua/insert_item.lua");
|
||||
}
|
||||
|
||||
public void insert(String destination, long destinationDevice, long timestamp, Envelope message) {
|
||||
public void insert(UUID guid, String destination, long destinationDevice, long timestamp, Envelope message) {
|
||||
Key key = new Key(destination, destinationDevice);
|
||||
String sender = message.getSource() + "::" + message.getTimestamp();
|
||||
String sender = message.hasSource() ? (message.getSource() + "::" + message.getTimestamp()) : "nil";
|
||||
|
||||
List<byte[]> keys = Arrays.asList(key.getUserMessageQueue(), key.getUserMessageQueueMetadata(), Key.getUserMessageQueueIndex());
|
||||
List<byte[]> args = Arrays.asList(message.toByteArray(), String.valueOf(timestamp).getBytes(), sender.getBytes());
|
||||
List<byte[]> args = Arrays.asList(message.toByteArray(), String.valueOf(timestamp).getBytes(), sender.getBytes(), guid.toString().getBytes());
|
||||
|
||||
insert.execute(keys, args);
|
||||
}
|
||||
|
@ -262,11 +287,13 @@ public class MessagesCache implements Managed {
|
|||
|
||||
private final LuaScript removeById;
|
||||
private final LuaScript removeBySender;
|
||||
private final LuaScript removeByGuid;
|
||||
private final LuaScript removeQueue;
|
||||
|
||||
RemoveOperation(ReplicatedJedisPool jedisPool) throws IOException {
|
||||
this.removeById = LuaScript.fromResource(jedisPool, "lua/remove_item_by_id.lua" );
|
||||
this.removeBySender = LuaScript.fromResource(jedisPool, "lua/remove_item_by_sender.lua");
|
||||
this.removeByGuid = LuaScript.fromResource(jedisPool, "lua/remove_item_by_guid.lua" );
|
||||
this.removeQueue = LuaScript.fromResource(jedisPool, "lua/remove_queue.lua" );
|
||||
}
|
||||
|
||||
|
@ -289,6 +316,15 @@ public class MessagesCache implements Managed {
|
|||
return (byte[])this.removeBySender.execute(keys, args);
|
||||
}
|
||||
|
||||
public byte[] remove(String destination, long destinationDevice, UUID guid) {
|
||||
Key key = new Key(destination, destinationDevice);
|
||||
|
||||
List<byte[]> keys = Arrays.asList(key.getUserMessageQueue(), key.getUserMessageQueueMetadata(), Key.getUserMessageQueueIndex());
|
||||
List<byte[]> args = Collections.singletonList(guid.toString().getBytes());
|
||||
|
||||
return (byte[])this.removeByGuid.execute(keys, args);
|
||||
}
|
||||
|
||||
public void clear(String destination, long deviceId) {
|
||||
Key key = new Key(destination, deviceId);
|
||||
|
||||
|
@ -445,8 +481,11 @@ public class MessagesCache implements Managed {
|
|||
private void persistMessage(Key key, long score, byte[] message) {
|
||||
try {
|
||||
Envelope envelope = Envelope.parseFrom(message);
|
||||
database.store(envelope, key.getAddress(), key.getDeviceId());
|
||||
UUID guid = envelope.hasServerGuid() ? UUID.fromString(envelope.getServerGuid()) : null;
|
||||
|
||||
envelope = envelope.toBuilder().clearServerGuid().build();
|
||||
|
||||
database.store(guid, envelope, key.getAddress(), key.getDeviceId());
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
logger.error("Error parsing envelope", e);
|
||||
}
|
||||
|
@ -464,9 +503,7 @@ public class MessagesCache implements Managed {
|
|||
}
|
||||
}
|
||||
|
||||
private void notifyClients(AccountsManager accountsManager, PubSubManager pubSubManager, PushSender pushSender, Key key)
|
||||
throws IOException
|
||||
{
|
||||
private void notifyClients(AccountsManager accountsManager, PubSubManager pubSubManager, PushSender pushSender, Key key) {
|
||||
Timer.Context timer = notifyTimer.time();
|
||||
|
||||
try {
|
||||
|
|
|
@ -4,13 +4,14 @@ package org.whispersystems.textsecuregcm.storage;
|
|||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.google.common.base.Optional;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
|
@ -21,6 +22,9 @@ public class MessagesManager {
|
|||
private static final Meter cacheMissByIdMeter = metricRegistry.meter(name(MessagesManager.class, "cacheMissById" ));
|
||||
private static final Meter cacheHitByNameMeter = metricRegistry.meter(name(MessagesManager.class, "cacheHitByName" ));
|
||||
private static final Meter cacheMissByNameMeter = metricRegistry.meter(name(MessagesManager.class, "cacheMissByName"));
|
||||
private static final Meter cacheHitByGuidMeter = metricRegistry.meter(name(MessagesManager.class, "cacheHitByGuid" ));
|
||||
private static final Meter cacheMissByGuidMeter = metricRegistry.meter(name(MessagesManager.class, "cacheMissByGuid"));
|
||||
|
||||
|
||||
private final Messages messages;
|
||||
private final MessagesCache messagesCache;
|
||||
|
@ -31,7 +35,8 @@ public class MessagesManager {
|
|||
}
|
||||
|
||||
public void insert(String destination, long destinationDevice, Envelope message) {
|
||||
messagesCache.insert(destination, destinationDevice, message);
|
||||
UUID guid = UUID.randomUUID();
|
||||
messagesCache.insert(guid, destination, destinationDevice, message);
|
||||
}
|
||||
|
||||
public OutgoingMessageEntityList getMessagesForDevice(String destination, long destinationDevice) {
|
||||
|
@ -59,7 +64,7 @@ public class MessagesManager {
|
|||
Optional<OutgoingMessageEntity> removed = this.messagesCache.remove(destination, destinationDevice, source, timestamp);
|
||||
|
||||
if (!removed.isPresent()) {
|
||||
removed = Optional.fromNullable(this.messages.remove(destination, destinationDevice, source, timestamp));
|
||||
removed = Optional.ofNullable(this.messages.remove(destination, destinationDevice, source, timestamp));
|
||||
cacheMissByNameMeter.mark();
|
||||
} else {
|
||||
cacheHitByNameMeter.mark();
|
||||
|
@ -68,6 +73,19 @@ public class MessagesManager {
|
|||
return removed;
|
||||
}
|
||||
|
||||
public Optional<OutgoingMessageEntity> delete(String destination, long deviceId, UUID guid) {
|
||||
Optional<OutgoingMessageEntity> removed = this.messagesCache.remove(destination, deviceId, guid);
|
||||
|
||||
if (!removed.isPresent()) {
|
||||
removed = Optional.ofNullable(this.messages.remove(destination, guid));
|
||||
cacheMissByGuidMeter.mark();
|
||||
} else {
|
||||
cacheHitByGuidMeter.mark();
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
public void delete(String destination, long deviceId, long id, boolean cached) {
|
||||
if (cached) {
|
||||
this.messagesCache.remove(destination, deviceId, id);
|
||||
|
|
|
@ -18,7 +18,6 @@ package org.whispersystems.textsecuregcm.storage;
|
|||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
||||
|
@ -26,6 +25,7 @@ import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
|
|||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import redis.clients.jedis.Jedis;
|
||||
|
||||
|
@ -60,7 +60,7 @@ public class PendingAccountsManager {
|
|||
Optional<StoredVerificationCode> code = memcacheGet(number);
|
||||
|
||||
if (!code.isPresent()) {
|
||||
code = Optional.fromNullable(pendingAccounts.getCodeForNumber(number));
|
||||
code = Optional.ofNullable(pendingAccounts.getCodeForNumber(number));
|
||||
|
||||
if (code.isPresent()) {
|
||||
memcacheSet(number, code.get());
|
||||
|
@ -82,11 +82,11 @@ public class PendingAccountsManager {
|
|||
try (Jedis jedis = cacheClient.getReadResource()) {
|
||||
String json = jedis.get(CACHE_PREFIX + number);
|
||||
|
||||
if (json == null) return Optional.absent();
|
||||
if (json == null) return Optional.empty();
|
||||
else return Optional.of(mapper.readValue(json, StoredVerificationCode.class));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error deserializing value...", e);
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ package org.whispersystems.textsecuregcm.storage;
|
|||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
||||
|
@ -26,6 +25,7 @@ import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
|
|||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import redis.clients.jedis.Jedis;
|
||||
|
||||
|
@ -59,7 +59,7 @@ public class PendingDevicesManager {
|
|||
Optional<StoredVerificationCode> code = memcacheGet(number);
|
||||
|
||||
if (!code.isPresent()) {
|
||||
code = Optional.fromNullable(pendingDevices.getCodeForNumber(number));
|
||||
code = Optional.ofNullable(pendingDevices.getCodeForNumber(number));
|
||||
|
||||
if (code.isPresent()) {
|
||||
memcacheSet(number, code.get());
|
||||
|
@ -81,11 +81,11 @@ public class PendingDevicesManager {
|
|||
try (Jedis jedis = cacheClient.getReadResource()) {
|
||||
String json = jedis.get(CACHE_PREFIX + number);
|
||||
|
||||
if (json == null) return Optional.absent();
|
||||
if (json == null) return Optional.empty();
|
||||
else return Optional.of(mapper.readValue(json, StoredVerificationCode.class));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not parse pending device stored verification json");
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ByteUtil {
|
||||
|
||||
public static byte[] combine(byte[]... elements) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
for (byte[] element : elements) {
|
||||
baos.write(element);
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ import java.io.UnsupportedEncodingException;
|
|||
import java.net.URLEncoder;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -120,6 +121,12 @@ public class Util {
|
|||
return parts;
|
||||
}
|
||||
|
||||
public static byte[] generateSecretBytes(int size) {
|
||||
byte[] data = new byte[size];
|
||||
new SecureRandom().nextBytes(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
public static void sleep(long i) {
|
||||
try {
|
||||
Thread.sleep(i);
|
||||
|
|
|
@ -25,9 +25,10 @@ import static com.codahale.metrics.MetricRegistry.name;
|
|||
|
||||
public class AuthenticatedConnectListener implements WebSocketConnectListener {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
|
||||
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private static final Timer durationTimer = metricRegistry.timer(name(WebSocketConnection.class, "connected_duration"));
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
|
||||
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private static final Timer durationTimer = metricRegistry.timer(name(WebSocketConnection.class, "connected_duration" ));
|
||||
private static final Timer unauthenticatedDurationTimer = metricRegistry.timer(name(WebSocketConnection.class, "unauthenticated_connection_duration"));
|
||||
|
||||
private final PushSender pushSender;
|
||||
private final ReceiptSender receiptSender;
|
||||
|
@ -50,29 +51,34 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
|
|||
|
||||
@Override
|
||||
public void onWebSocketConnect(WebSocketSessionContext context) {
|
||||
final Account account = context.getAuthenticated(Account.class);
|
||||
final Device device = account.getAuthenticatedDevice().get();
|
||||
final String connectionId = String.valueOf(new SecureRandom().nextLong());
|
||||
final Timer.Context timer = durationTimer.time();
|
||||
final WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
|
||||
final WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender,
|
||||
messagesManager, account, device,
|
||||
context.getClient(), connectionId);
|
||||
final PubSubMessage connectMessage = PubSubMessage.newBuilder().setType(PubSubMessage.Type.CONNECTED)
|
||||
.setContent(ByteString.copyFrom(connectionId.getBytes()))
|
||||
.build();
|
||||
if (context.getAuthenticated() != null) {
|
||||
final Account account = context.getAuthenticated(Account.class);
|
||||
final Device device = account.getAuthenticatedDevice().get();
|
||||
final String connectionId = String.valueOf(new SecureRandom().nextLong());
|
||||
final Timer.Context timer = durationTimer.time();
|
||||
final WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
|
||||
final WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender,
|
||||
messagesManager, account, device,
|
||||
context.getClient(), connectionId);
|
||||
final PubSubMessage connectMessage = PubSubMessage.newBuilder().setType(PubSubMessage.Type.CONNECTED)
|
||||
.setContent(ByteString.copyFrom(connectionId.getBytes()))
|
||||
.build();
|
||||
|
||||
RedisOperation.unchecked(() -> apnFallbackManager.cancel(account, device));
|
||||
pubSubManager.publish(address, connectMessage);
|
||||
pubSubManager.subscribe(address, connection);
|
||||
RedisOperation.unchecked(() -> apnFallbackManager.cancel(account, device));
|
||||
pubSubManager.publish(address, connectMessage);
|
||||
pubSubManager.subscribe(address, connection);
|
||||
|
||||
context.addListener(new WebSocketSessionContext.WebSocketEventListener() {
|
||||
@Override
|
||||
public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason) {
|
||||
pubSubManager.unsubscribe(address, connection);
|
||||
timer.stop();
|
||||
}
|
||||
});
|
||||
context.addListener(new WebSocketSessionContext.WebSocketEventListener() {
|
||||
@Override
|
||||
public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason) {
|
||||
pubSubManager.unsubscribe(address, connection);
|
||||
timer.stop();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
final Timer.Context timer = unauthenticatedDurationTimer.time();
|
||||
context.addListener((context1, statusCode, reason) -> timer.stop());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.whispersystems.textsecuregcm.websocket;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
@ -13,6 +12,8 @@ import org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
|
|||
import org.whispersystems.websocket.WebSocketClient;
|
||||
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class ProvisioningConnection implements DispatchChannel {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ProvisioningConnection.class);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.whispersystems.textsecuregcm.websocket;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
@ -10,6 +9,7 @@ import org.whispersystems.websocket.auth.WebSocketAuthenticator;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
|
||||
|
@ -23,7 +23,7 @@ public class WebSocketAccountAuthenticator implements WebSocketAuthenticator<Acc
|
|||
}
|
||||
|
||||
@Override
|
||||
public Optional<Account> authenticate(UpgradeRequest request) throws AuthenticationException {
|
||||
public AuthenticationResult<Account> authenticate(UpgradeRequest request) throws AuthenticationException {
|
||||
try {
|
||||
Map<String, List<String>> parameters = request.getParameterMap();
|
||||
List<String> usernames = parameters.get("login");
|
||||
|
@ -32,13 +32,13 @@ public class WebSocketAccountAuthenticator implements WebSocketAuthenticator<Acc
|
|||
if (usernames == null || usernames.size() == 0 ||
|
||||
passwords == null || passwords.size() == 0)
|
||||
{
|
||||
return Optional.absent();
|
||||
return new AuthenticationResult<>(Optional.empty(), false);
|
||||
}
|
||||
|
||||
BasicCredentials credentials = new BasicCredentials(usernames.get(0).replace(" ", "+"),
|
||||
passwords.get(0).replace(" ", "+"));
|
||||
|
||||
return accountAuthenticator.authenticate(credentials);
|
||||
return new AuthenticationResult<>(accountAuthenticator.authenticate(credentials), true);
|
||||
} catch (io.dropwizard.auth.AuthenticationException e) {
|
||||
throw new AuthenticationException(e);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package org.whispersystems.textsecuregcm.websocket;
|
|||
import com.codahale.metrics.Histogram;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
@ -21,24 +20,25 @@ import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
|||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.websocket.WebSocketClient;
|
||||
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class WebSocketConnection implements DispatchChannel {
|
||||
|
||||
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
|
@ -82,7 +82,7 @@ public class WebSocketConnection implements DispatchChannel {
|
|||
processStoredMessages();
|
||||
break;
|
||||
case PubSubMessage.Type.DELIVER_VALUE:
|
||||
sendMessage(Envelope.parseFrom(pubSubMessage.getContent()), Optional.absent(), false);
|
||||
sendMessage(Envelope.parseFrom(pubSubMessage.getContent()), Optional.empty(), false);
|
||||
break;
|
||||
case PubSubMessage.Type.CONNECTED_VALUE:
|
||||
if (pubSubMessage.hasContent() && !new String(pubSubMessage.getContent().toByteArray()).equals(connectionId)) {
|
||||
|
@ -112,7 +112,7 @@ public class WebSocketConnection implements DispatchChannel {
|
|||
{
|
||||
try {
|
||||
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey());
|
||||
Optional<byte[]> body = Optional.fromNullable(encryptedMessage.toByteArray());
|
||||
Optional<byte[]> body = Optional.ofNullable(encryptedMessage.toByteArray());
|
||||
ListenableFuture<WebSocketResponseMessage> response = client.sendRequest("PUT", "/api/v1/message", null, body);
|
||||
|
||||
Futures.addCallback(response, new FutureCallback<WebSocketResponseMessage>() {
|
||||
|
@ -158,14 +158,12 @@ public class WebSocketConnection implements DispatchChannel {
|
|||
}
|
||||
|
||||
private void sendDeliveryReceiptFor(Envelope message) {
|
||||
if (!message.hasSource()) return;
|
||||
|
||||
try {
|
||||
receiptSender.sendReceipt(account, message.getSource(), message.getTimestamp(),
|
||||
message.hasRelay() ? Optional.of(message.getRelay()) :
|
||||
Optional.absent());
|
||||
receiptSender.sendReceipt(account, message.getSource(), message.getTimestamp());
|
||||
} catch (NoSuchUserException | NotPushRegisteredException e) {
|
||||
logger.info("No longer registered " + e.getMessage());
|
||||
} catch(IOException | TransientPushFailureException e) {
|
||||
logger.warn("Something wrong while sending receipt", e);
|
||||
} catch (WebApplicationException e) {
|
||||
logger.warn("Bad federated response for receipt: " + e.getResponse().getStatus());
|
||||
}
|
||||
|
@ -179,9 +177,13 @@ public class WebSocketConnection implements DispatchChannel {
|
|||
OutgoingMessageEntity message = iterator.next();
|
||||
Envelope.Builder builder = Envelope.newBuilder()
|
||||
.setType(Envelope.Type.valueOf(message.getType()))
|
||||
.setSourceDevice(message.getSourceDevice())
|
||||
.setSource(message.getSource())
|
||||
.setTimestamp(message.getTimestamp());
|
||||
.setTimestamp(message.getTimestamp())
|
||||
.setServerTimestamp(message.getServerTimestamp());
|
||||
|
||||
if (!Util.isEmpty(message.getSource())) {
|
||||
builder.setSource(message.getSource())
|
||||
.setSourceDevice(message.getSourceDevice());
|
||||
}
|
||||
|
||||
if (message.getMessage() != null) {
|
||||
builder.setLegacyMessage(ByteString.copyFrom(message.getMessage()));
|
||||
|
@ -199,7 +201,7 @@ public class WebSocketConnection implements DispatchChannel {
|
|||
}
|
||||
|
||||
if (!messages.hasMore()) {
|
||||
client.sendRequest("PUT", "/api/v1/queue/empty", null, Optional.<byte[]>absent());
|
||||
client.sendRequest("PUT", "/api/v1/queue/empty", null, Optional.empty());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
package org.whispersystems.textsecuregcm.workers;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.protobuf.ByteString;
|
||||
import net.sourceforge.argparse4j.impl.Arguments;
|
||||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
import org.whispersystems.curve25519.Curve25519;
|
||||
import org.whispersystems.curve25519.Curve25519KeyPair;
|
||||
import org.whispersystems.textsecuregcm.crypto.Curve;
|
||||
import org.whispersystems.textsecuregcm.crypto.ECKeyPair;
|
||||
import org.whispersystems.textsecuregcm.crypto.ECPrivateKey;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
|
||||
import io.dropwizard.cli.Command;
|
||||
import io.dropwizard.setup.Bootstrap;
|
||||
|
||||
public class CertificateCommand extends Command {
|
||||
|
||||
public CertificateCommand() {
|
||||
super("certificate", "Generates server certificates for unidentified delivery");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(Subparser subparser) {
|
||||
subparser.addArgument("-ca", "--ca")
|
||||
.dest("ca")
|
||||
.action(Arguments.storeTrue())
|
||||
.setDefault(Boolean.FALSE)
|
||||
.help("Generate CA parameters");
|
||||
|
||||
subparser.addArgument("-k", "--key")
|
||||
.dest("key")
|
||||
.type(String.class)
|
||||
.help("The CA private signing key");
|
||||
|
||||
subparser.addArgument("-i", "--id")
|
||||
.dest("keyId")
|
||||
.type(Integer.class)
|
||||
.help("The key ID to create");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(Bootstrap<?> bootstrap, Namespace namespace) throws Exception {
|
||||
if (MoreObjects.firstNonNull(namespace.getBoolean("ca"), false)) runCaCommand();
|
||||
else runCertificateCommand(namespace);
|
||||
}
|
||||
|
||||
private void runCaCommand() {
|
||||
ECKeyPair keyPair = Curve.generateKeyPair();
|
||||
System.out.println("Public key : " + Base64.encodeBytes(keyPair.getPublicKey().serialize()));
|
||||
System.out.println("Private key: " + Base64.encodeBytes(keyPair.getPrivateKey().serialize()));
|
||||
}
|
||||
|
||||
private void runCertificateCommand(Namespace namespace) throws IOException, InvalidKeyException {
|
||||
if (namespace.getString("key") == null) {
|
||||
System.out.println("No key specified!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (namespace.getInt("keyId") == null) {
|
||||
System.out.print("No key id specified!");
|
||||
return;
|
||||
}
|
||||
|
||||
ECPrivateKey key = Curve.decodePrivatePoint(Base64.decode(namespace.getString("key")));
|
||||
int keyId = namespace.getInt("keyId");
|
||||
|
||||
ECKeyPair keyPair = Curve.generateKeyPair();
|
||||
|
||||
byte[] certificate = MessageProtos.ServerCertificate.Certificate.newBuilder()
|
||||
.setId(keyId)
|
||||
.setKey(ByteString.copyFrom(keyPair.getPublicKey().serialize()))
|
||||
.build()
|
||||
.toByteArray();
|
||||
|
||||
byte[] signature = Curve.calculateSignature(key, certificate);
|
||||
|
||||
byte[] signedCertificate = MessageProtos.ServerCertificate.newBuilder()
|
||||
.setCertificate(ByteString.copyFrom(certificate))
|
||||
.setSignature(ByteString.copyFrom(signature))
|
||||
.build()
|
||||
.toByteArray();
|
||||
|
||||
System.out.println("Certificate: " + Base64.encodeBytes(signedCertificate));
|
||||
System.out.println("Private key: " + Base64.encodeBytes(keyPair.getPrivateKey().serialize()));
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package org.whispersystems.textsecuregcm.workers;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.google.common.base.Optional;
|
||||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
import org.skife.jdbi.v2.DBI;
|
||||
|
@ -19,6 +18,7 @@ import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
|||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.dropwizard.Application;
|
||||
import io.dropwizard.cli.EnvironmentCommand;
|
||||
|
|
|
@ -22,7 +22,6 @@ import org.skife.jdbi.v2.DBI;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
|
||||
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
|
||||
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||
|
@ -30,16 +29,13 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
|||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||
|
||||
import io.dropwizard.Application;
|
||||
import io.dropwizard.cli.ConfiguredCommand;
|
||||
import io.dropwizard.cli.EnvironmentCommand;
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
import io.dropwizard.jdbi.ImmutableListContainerFactory;
|
||||
import io.dropwizard.jdbi.ImmutableSetContainerFactory;
|
||||
import io.dropwizard.jdbi.OptionalContainerFactory;
|
||||
import io.dropwizard.jdbi.args.OptionalArgumentFactory;
|
||||
import io.dropwizard.setup.Bootstrap;
|
||||
import io.dropwizard.setup.Environment;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
|
||||
public class DirectoryCommand extends EnvironmentCommand<WhisperServerConfiguration> {
|
||||
|
||||
|
@ -77,14 +73,10 @@ public class DirectoryCommand extends EnvironmentCommand<WhisperServerConfigurat
|
|||
ReplicatedJedisPool redisClient = new RedisClientFactory(configuration.getDirectoryConfiguration().getRedisConfiguration().getUrl(), configuration.getDirectoryConfiguration().getRedisConfiguration().getReplicaUrls()).getRedisClientPool();
|
||||
DirectoryManager directory = new DirectoryManager(redisClient);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
|
||||
// FederatedClientManager federatedClientManager = new FederatedClientManager(environment,
|
||||
// configuration.getJerseyClientConfiguration(),
|
||||
// configuration.getFederationConfiguration());
|
||||
|
||||
DirectoryUpdater update = new DirectoryUpdater(accountsManager, directory);
|
||||
|
||||
update.updateFromLocalDatabase();
|
||||
// update.updateFromPeers();
|
||||
} catch (Exception ex) {
|
||||
logger.warn("Directory Exception", ex);
|
||||
throw new RuntimeException(ex);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
@ -16,25 +16,17 @@
|
|||
*/
|
||||
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.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.storage.DirectoryManager.PendingClientContact;
|
||||
|
||||
public class DirectoryUpdater {
|
||||
|
||||
private static final int CHUNK_SIZE = 10000;
|
||||
|
@ -68,7 +60,7 @@ public class DirectoryUpdater {
|
|||
for (Account account : accounts) {
|
||||
if (account.isActive()) {
|
||||
byte[] token = Util.getContactToken(account.getNumber());
|
||||
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported(), account.isVideoSupported());
|
||||
ClientContact clientContact = new ClientContact(token, null, true, true);
|
||||
|
||||
directory.add(batchOperation, clientContact);
|
||||
contactsAdded++;
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
-- keys: queue_key, queue_metadata_key, queue_total_index
|
||||
-- argv: message, current_time, sender_key
|
||||
-- keys: queue_key [1], queue_metadata_key [2], queue_total_index [3]
|
||||
-- argv: message [1], current_time [2], sender (possibly null) [3], guid [4]
|
||||
|
||||
local messageId = redis.call("HINCRBY", KEYS[2], "counter", 1)
|
||||
redis.call("ZADD", KEYS[1], "NX", messageId, ARGV[1])
|
||||
redis.call("HSET", KEYS[2], ARGV[3], messageId)
|
||||
redis.call("HSET", KEYS[2], messageId, ARGV[3])
|
||||
|
||||
if ARGV[3] ~= "nil" then
|
||||
redis.call("HSET", KEYS[2], ARGV[3], messageId)
|
||||
end
|
||||
|
||||
redis.call("HSET", KEYS[2], ARGV[4], messageId)
|
||||
|
||||
if ARGV[3] ~= "nil" then
|
||||
redis.call("HSET", KEYS[2], messageId, ARGV[3])
|
||||
end
|
||||
|
||||
redis.call("HSET", KEYS[2], messageId .. "guid", ARGV[4])
|
||||
|
||||
redis.call("EXPIRE", KEYS[1], 7776000)
|
||||
redis.call("EXPIRE", KEYS[2], 7776000)
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
-- keys: queue_key, queue_metadata_key, queue_index
|
||||
-- argv: guid_to_remove
|
||||
|
||||
local messageId = redis.call("HGET", KEYS[2], ARGV[1])
|
||||
|
||||
if messageId then
|
||||
local envelope = redis.call("ZRANGEBYSCORE", KEYS[1], messageId, messageId, "LIMIT", 0, 1)
|
||||
local sender = redis.call("HGET", KEYS[2], messageId)
|
||||
|
||||
redis.call("ZREMRANGEBYSCORE", KEYS[1], messageId, messageId)
|
||||
redis.call("HDEL", KEYS[2], ARGV[1])
|
||||
redis.call("HDEL", KEYS[2], messageId .. "guid")
|
||||
|
||||
if sender then
|
||||
redis.call("HDEL", KEYS[2], sender)
|
||||
redis.call("HDEL", KEYS[2], messageId)
|
||||
end
|
||||
|
||||
if (redis.call("ZCARD", KEYS[1]) == 0) then
|
||||
redis.call("ZREM", KEYS[3], KEYS[1])
|
||||
end
|
||||
|
||||
if envelope and next(envelope) then
|
||||
return envelope[1]
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
|
@ -3,12 +3,18 @@
|
|||
|
||||
local removedCount = redis.call("ZREMRANGEBYSCORE", KEYS[1], ARGV[1], ARGV[1])
|
||||
local senderIndex = redis.call("HGET", KEYS[2], ARGV[1])
|
||||
local guidIndex = redis.call("HGET", KEYS[2], ARGV[1] .. "guid")
|
||||
|
||||
if senderIndex then
|
||||
redis.call("HDEL", KEYS[2], senderIndex)
|
||||
redis.call("HDEL", KEYS[2], ARGV[1])
|
||||
end
|
||||
|
||||
if guidIndex then
|
||||
redis.call("HDEL", KEYS[2], guidIndex)
|
||||
redis.call("HDEL", KEYS[2], ARGV[1] .. "guid")
|
||||
end
|
||||
|
||||
if (redis.call("ZCARD", KEYS[1]) == 0) then
|
||||
redis.call("ZREM", KEYS[3], KEYS[1])
|
||||
end
|
||||
|
|
|
@ -5,11 +5,17 @@ local messageId = redis.call("HGET", KEYS[2], ARGV[1])
|
|||
|
||||
if messageId then
|
||||
local envelope = redis.call("ZRANGEBYSCORE", KEYS[1], messageId, messageId, "LIMIT", 0, 1)
|
||||
local guid = redis.call("HGET", KEYS[2], messageId .. "guid")
|
||||
|
||||
redis.call("ZREMRANGEBYSCORE", KEYS[1], messageId, messageId)
|
||||
redis.call("HDEL", KEYS[2], ARGV[1])
|
||||
redis.call("HDEL", KEYS[2], messageId)
|
||||
|
||||
if guid then
|
||||
redis.call("HDEL", KEYS[2], guid)
|
||||
redis.call("HDEL", KEYS[2], messageId .. "guid")
|
||||
end
|
||||
|
||||
if (redis.call("ZCARD", KEYS[1]) == 0) then
|
||||
redis.call("ZREM", KEYS[3], KEYS[1])
|
||||
end
|
||||
|
|
|
@ -104,5 +104,21 @@
|
|||
<sql>CREATE RULE bounded_message_queue AS ON INSERT TO messages DO ALSO DELETE FROM messages WHERE id IN (SELECT id FROM messages WHERE destination = NEW.destination AND destination_device = NEW.destination_device ORDER BY timestamp DESC OFFSET 1000);</sql>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="11" author="moxie">
|
||||
<addColumn tableName="messages">
|
||||
<column name="guid" type="uuid"/>
|
||||
</addColumn>
|
||||
|
||||
<addColumn tableName="messages">
|
||||
<column name="server_timestamp" type="bigint"/>
|
||||
</addColumn>
|
||||
|
||||
<dropNotNullConstraint tableName="messages" columnName="source"/>
|
||||
<dropNotNullConstraint tableName="messages" columnName="source_device"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="12" author="moxie" runInTransaction="false">
|
||||
<sql>CREATE INDEX CONCURRENTLY guid_index ON messages (guid);</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package org.whispersystems.dispatch;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import java.util.Optional;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExternalResource;
|
||||
|
@ -42,7 +42,7 @@ public class DispatchManagerTest {
|
|||
}
|
||||
});
|
||||
|
||||
dispatchManager = new DispatchManager(socketFactory, Optional.<DispatchChannel>absent());
|
||||
dispatchManager = new DispatchManager(socketFactory, Optional.empty());
|
||||
dispatchManager.start();
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@ public class DispatchManagerTest {
|
|||
public void testSubscribe() throws IOException {
|
||||
DispatchChannel dispatchChannel = mock(DispatchChannel.class);
|
||||
dispatchManager.subscribe("foo", dispatchChannel);
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.<byte[]>absent()));
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.empty()));
|
||||
|
||||
verify(dispatchChannel, timeout(1000)).onDispatchSubscribed(eq("foo"));
|
||||
}
|
||||
|
@ -72,8 +72,8 @@ public class DispatchManagerTest {
|
|||
dispatchManager.subscribe("foo", dispatchChannel);
|
||||
dispatchManager.unsubscribe("foo", dispatchChannel);
|
||||
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.<byte[]>absent()));
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, "foo", Optional.<byte[]>absent()));
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.empty()));
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, "foo", Optional.empty()));
|
||||
|
||||
verify(dispatchChannel, timeout(1000)).onDispatchUnsubscribed(eq("foo"));
|
||||
}
|
||||
|
@ -86,8 +86,8 @@ public class DispatchManagerTest {
|
|||
dispatchManager.subscribe("foo", fooChannel);
|
||||
dispatchManager.subscribe("bar", barChannel);
|
||||
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.<byte[]>absent()));
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "bar", Optional.<byte[]>absent()));
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.empty()));
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "bar", Optional.empty()));
|
||||
|
||||
verify(fooChannel, timeout(1000)).onDispatchSubscribed(eq("foo"));
|
||||
verify(barChannel, timeout(1000)).onDispatchSubscribed(eq("bar"));
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
package org.whispersystems.textsecuregcm.tests.auth;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.textsecuregcm.auth.Anonymous;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import java.util.Optional;
|
||||
|
||||
import static junit.framework.TestCase.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class OptionalAccessTest {
|
||||
|
||||
@Test
|
||||
public void testUnidentifiedMissingTarget() {
|
||||
try {
|
||||
OptionalAccess.verify(Optional.empty(), Optional.empty(), Optional.empty());
|
||||
throw new AssertionError("should fail");
|
||||
} catch (WebApplicationException e) {
|
||||
assertEquals(e.getResponse().getStatus(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnidentifiedMissingTargetDevice() {
|
||||
Account account = mock(Account.class);
|
||||
when(account.isActive()).thenReturn(true);
|
||||
when(account.getDevice(eq(10))).thenReturn(Optional.empty());
|
||||
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
|
||||
|
||||
try {
|
||||
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.encodeBytes("1234".getBytes()))), Optional.of(account), "10");
|
||||
} catch (WebApplicationException e) {
|
||||
assertEquals(e.getResponse().getStatus(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnidentifiedBadTargetDevice() {
|
||||
Account account = mock(Account.class);
|
||||
when(account.isActive()).thenReturn(true);
|
||||
when(account.getDevice(eq(10))).thenReturn(Optional.empty());
|
||||
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
|
||||
|
||||
try {
|
||||
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.encodeBytes("1234".getBytes()))), Optional.of(account), "$$");
|
||||
} catch (WebApplicationException e) {
|
||||
assertEquals(e.getResponse().getStatus(), 422);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUnidentifiedBadCode() {
|
||||
Account account = mock(Account.class);
|
||||
when(account.isActive()).thenReturn(true);
|
||||
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
|
||||
|
||||
try {
|
||||
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.encodeBytes("5678".getBytes()))), Optional.of(account));
|
||||
throw new AssertionError("should fail");
|
||||
} catch (WebApplicationException e) {
|
||||
assertEquals(e.getResponse().getStatus(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIdentifiedMissingTarget() {
|
||||
Account account = mock(Account.class);
|
||||
when(account.isActive()).thenReturn(true);
|
||||
|
||||
try {
|
||||
OptionalAccess.verify(Optional.of(account), Optional.empty(), Optional.empty());
|
||||
throw new AssertionError("should fail");
|
||||
} catch (WebApplicationException e) {
|
||||
assertEquals(e.getResponse().getStatus(), 404);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnsolicitedBadTarget() {
|
||||
Account account = mock(Account.class);
|
||||
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false);
|
||||
when(account.isActive()).thenReturn(true);
|
||||
|
||||
try {
|
||||
OptionalAccess.verify(Optional.empty(), Optional.empty(), Optional.of(account));
|
||||
throw new AssertionError("shold fai");
|
||||
} catch (WebApplicationException e) {
|
||||
assertEquals(e.getResponse().getStatus(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnsolicitedGoodTarget() {
|
||||
Account account = mock(Account.class);
|
||||
Anonymous random = mock(Anonymous.class);
|
||||
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(true);
|
||||
when(account.isActive()).thenReturn(true);
|
||||
OptionalAccess.verify(Optional.empty(), Optional.of(random), Optional.of(account));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnidentifiedGoodTarget() {
|
||||
Account account = mock(Account.class);
|
||||
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
|
||||
when(account.isActive()).thenReturn(true);
|
||||
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.encodeBytes("1234".getBytes()))), Optional.of(account));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnidentifiedInactive() {
|
||||
Account account = mock(Account.class);
|
||||
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
|
||||
when(account.isActive()).thenReturn(false);
|
||||
|
||||
try {
|
||||
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.encodeBytes("1234".getBytes()))), Optional.of(account));
|
||||
throw new AssertionError();
|
||||
} catch (WebApplicationException e) {
|
||||
assertEquals(e.getResponse().getStatus(), 401);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIdentifiedGoodTarget() {
|
||||
Account source = mock(Account.class);
|
||||
Account target = mock(Account.class);
|
||||
when(target.isActive()).thenReturn(true);
|
||||
OptionalAccess.verify(Optional.of(source), Optional.empty(), Optional.of(target));;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
||||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||
|
@ -30,8 +28,10 @@ import javax.ws.rs.client.Entity;
|
|||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
|
@ -59,7 +59,7 @@ public class AccountControllerTest {
|
|||
@Rule
|
||||
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
|
||||
.addProvider(new RateLimitExceededExceptionMapper())
|
||||
.setMapper(SystemMapper.getMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
|
@ -94,8 +94,8 @@ public class AccountControllerTest {
|
|||
|
||||
when(accountsManager.get(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount));
|
||||
when(accountsManager.get(eq(SENDER_OVER_PIN))).thenReturn(Optional.of(senderPinAccount));
|
||||
when(accountsManager.get(eq(SENDER))).thenReturn(Optional.absent());
|
||||
when(accountsManager.get(eq(SENDER_OLD))).thenReturn(Optional.absent());
|
||||
when(accountsManager.get(eq(SENDER))).thenReturn(Optional.empty());
|
||||
when(accountsManager.get(eq(SENDER_OLD))).thenReturn(Optional.empty());
|
||||
|
||||
doThrow(new RateLimitExceededException(SENDER_OVER_PIN)).when(pinLimiter).validate(eq(SENDER_OVER_PIN));
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ public class AccountControllerTest {
|
|||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
|
||||
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.absent()), anyString());
|
||||
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -270,6 +270,17 @@ public class AccountControllerTest {
|
|||
verify(AuthHelper.VALID_ACCOUNT).setPin(eq("31337"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetPinUnauthorized() throws Exception {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v1/accounts/pin/")
|
||||
.request()
|
||||
.put(Entity.json(new RegistrationLock("31337")));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetShortPin() throws Exception {
|
||||
Response response =
|
||||
|
|
|
@ -3,20 +3,20 @@ package org.whispersystems.textsecuregcm.tests.controllers;
|
|||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
|
||||
import org.whispersystems.textsecuregcm.configuration.AttachmentsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentController;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptor;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.UrlSigner;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
@ -24,10 +24,9 @@ import static org.mockito.Mockito.when;
|
|||
|
||||
public class AttachmentControllerTest {
|
||||
|
||||
private static AttachmentsConfiguration configuration = mock(AttachmentsConfiguration.class);
|
||||
private static FederatedClientManager federatedClientManager = mock(FederatedClientManager.class );
|
||||
private static RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||
private static RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||
private static AttachmentsConfiguration configuration = mock(AttachmentsConfiguration.class);
|
||||
private static RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||
private static RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||
|
||||
private static UrlSigner urlSigner;
|
||||
|
||||
|
@ -43,10 +42,10 @@ public class AttachmentControllerTest {
|
|||
@ClassRule
|
||||
public static final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
|
||||
.setMapper(SystemMapper.getMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new AttachmentController(rateLimiters, federatedClientManager, urlSigner))
|
||||
.addResource(new AttachmentController(rateLimiters, urlSigner))
|
||||
.build();
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.controllers.CertificateController;
|
||||
import org.whispersystems.textsecuregcm.crypto.Curve;
|
||||
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.SenderCertificate;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.ServerCertificate;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class CertificateControllerTest {
|
||||
|
||||
private static final String caPublicKey = "BWh+UOhT1hD8bkb+MFRvb6tVqhoG8YYGCzOd7mgjo8cV";
|
||||
private static final String caPrivateKey = "EO3Mnf0kfVlVnwSaqPoQnAxhnnGL1JTdXqktCKEe9Eo=";
|
||||
|
||||
private static final String signingCertificate = "CiUIDBIhBbTz4h1My+tt+vw+TVscgUe/DeHS0W02tPWAWbTO2xc3EkD+go4bJnU0AcnFfbOLKoiBfCzouZtDYMOVi69rE7r4U9cXREEqOkUmU2WJBjykAxWPCcSTmVTYHDw7hkSp/puG";
|
||||
private static final String signingKey = "ABOxG29xrfq4E7IrW11Eg7+HBbtba9iiS0500YoBjn4=";
|
||||
|
||||
private static CertificateGenerator certificateGenerator;
|
||||
|
||||
static {
|
||||
try {
|
||||
certificateGenerator = new CertificateGenerator(Base64.decode(signingCertificate), Curve.decodePrivatePoint(Base64.decode(signingKey)), 1);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ClassRule
|
||||
public static final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
|
||||
.setMapper(SystemMapper.getMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new CertificateController(certificateGenerator))
|
||||
.build();
|
||||
|
||||
|
||||
@Test
|
||||
public void testValidCertificate() throws Exception {
|
||||
DeliveryCertificate certificateObject = resources.getJerseyTest()
|
||||
.target("/v1/certificate/delivery")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.get(DeliveryCertificate.class);
|
||||
|
||||
|
||||
SenderCertificate certificateHolder = SenderCertificate.parseFrom(certificateObject.getCertificate());
|
||||
SenderCertificate.Certificate certificate = SenderCertificate.Certificate.parseFrom(certificateHolder.getCertificate());
|
||||
|
||||
ServerCertificate serverCertificateHolder = certificate.getSigner();
|
||||
ServerCertificate.Certificate serverCertificate = ServerCertificate.Certificate.parseFrom(serverCertificateHolder.getCertificate());
|
||||
|
||||
assertTrue(Curve.verifySignature(Curve.decodePoint(serverCertificate.getKey().toByteArray(), 0), certificateHolder.getCertificate().toByteArray(), certificateHolder.getSignature().toByteArray()));
|
||||
assertTrue(Curve.verifySignature(Curve.decodePoint(Base64.decode(caPublicKey), 0), serverCertificateHolder.getCertificate().toByteArray(), serverCertificateHolder.getSignature().toByteArray()));
|
||||
|
||||
assertEquals(certificate.getSender(), AuthHelper.VALID_NUMBER);
|
||||
assertEquals(certificate.getSenderDevice(), 1L);
|
||||
assertTrue(Arrays.equals(certificate.getIdentityKey().toByteArray(), Base64.decode(AuthHelper.VALID_IDENTITY)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadAuthentication() throws Exception {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/certificate/delivery")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertEquals(response.getStatus(), 401);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testNoAuthentication() throws Exception {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/certificate/delivery")
|
||||
.request()
|
||||
.get();
|
||||
|
||||
assertEquals(response.getStatus(), 401);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUnidentifiedAuthentication() throws Exception {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/certificate/delivery")
|
||||
.request()
|
||||
.header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("1234".getBytes()))
|
||||
.get();
|
||||
|
||||
assertEquals(response.getStatus(), 401);
|
||||
}
|
||||
|
||||
}
|
|
@ -16,12 +16,10 @@
|
|||
*/
|
||||
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
||||
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
|
@ -40,8 +38,10 @@ import javax.ws.rs.core.MediaType;
|
|||
import javax.ws.rs.core.Response;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
@ -83,7 +83,7 @@ public class DeviceControllerTest {
|
|||
@Rule
|
||||
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addProvider(new DeviceLimitExceededExceptionMapper())
|
||||
.addResource(new DumbVerificationDeviceController(pendingDevicesManager,
|
||||
|
@ -202,15 +202,4 @@ public class DeviceControllerTest {
|
|||
verifyNoMoreInteractions(messagesManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeDeviceTest() throws Exception {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/devices/12345")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.delete();
|
||||
|
||||
assertEquals(204, response.getStatus());
|
||||
verify(directoryQueue).deleteRegisteredUser(eq(AuthHelper.VALID_NUMBER));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
|
||||
import org.whispersystems.textsecuregcm.auth.DirectoryCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.DirectoryCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryClientConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
@ -26,11 +23,11 @@ import javax.ws.rs.core.Response;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyListOf;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Matchers.anyList;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
@ -46,7 +43,7 @@ public class DirectoryControllerTest {
|
|||
@Rule
|
||||
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new DirectoryController(rateLimiters,
|
||||
directoryManager,
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Optional;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
|
||||
import org.whispersystems.textsecuregcm.controllers.FederationControllerV1;
|
||||
import org.whispersystems.textsecuregcm.controllers.FederationControllerV2;
|
||||
import org.whispersystems.textsecuregcm.controllers.KeysController;
|
||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItem;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Set;
|
||||
|
||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.jsonFixture;
|
||||
|
||||
public class FederatedControllerTest {
|
||||
|
||||
private static final String SINGLE_DEVICE_RECIPIENT = "+14151111111";
|
||||
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
|
||||
|
||||
private PushSender pushSender = mock(PushSender.class );
|
||||
private ReceiptSender receiptSender = mock(ReceiptSender.class);
|
||||
private FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
|
||||
private AccountsManager accountsManager = mock(AccountsManager.class );
|
||||
private MessagesManager messagesManager = mock(MessagesManager.class);
|
||||
private RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||
private ApnFallbackManager apnFallbackManager = mock(ApnFallbackManager.class);
|
||||
|
||||
private final SignedPreKey signedPreKey = new SignedPreKey(3333, "foo", "baar");
|
||||
private final PreKeyResponse preKeyResponseV2 = new PreKeyResponse("foo", new LinkedList<PreKeyResponseItem>());
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
private final MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, federatedClientManager, apnFallbackManager);
|
||||
private final KeysController keysControllerV2 = mock(KeysController.class);
|
||||
|
||||
@Rule
|
||||
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new FederationControllerV1(accountsManager, null, messageController))
|
||||
.addResource(new FederationControllerV2(accountsManager, null, messageController, keysControllerV2))
|
||||
.build();
|
||||
|
||||
|
||||
|
||||
@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, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), false, false, "Test"));
|
||||
}};
|
||||
|
||||
Set<Device> multiDeviceList = new HashSet<Device>() {{
|
||||
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(222, "baz", "boop"), System.currentTimeMillis(), System.currentTimeMillis(), false, false, "Test"));
|
||||
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(333, "rad", "mad"), System.currentTimeMillis(), System.currentTimeMillis(), false, false, "Test"));
|
||||
}};
|
||||
|
||||
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList);
|
||||
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, multiDeviceList);
|
||||
|
||||
when(accountsManager.get(eq(SINGLE_DEVICE_RECIPIENT))).thenReturn(Optional.of(singleDeviceAccount));
|
||||
when(accountsManager.get(eq(MULTI_DEVICE_RECIPIENT))).thenReturn(Optional.of(multiDeviceAccount));
|
||||
|
||||
when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter);
|
||||
|
||||
when(keysControllerV2.getSignedKey(any(Account.class))).thenReturn(Optional.of(signedPreKey));
|
||||
when(keysControllerV2.getDeviceKeys(any(Account.class), anyString(), anyString(), any(Optional.class)))
|
||||
.thenReturn(Optional.of(preKeyResponseV2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleDeviceCurrent() throws Exception {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/federation/messages/+14152223333/1/%s", SINGLE_DEVICE_RECIPIENT))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader("cyanogen", "foofoo"))
|
||||
.put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat("Good Response", response.getStatus(), is(equalTo(204)));
|
||||
|
||||
verify(pushSender).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.Envelope.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignedPreKeyV2() throws Exception {
|
||||
PreKeyResponse response =
|
||||
resources.getJerseyTest()
|
||||
.target("/v2/federation/key/+14152223333/1")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader("cyanogen", "foofoo"))
|
||||
.get(PreKeyResponse.class);
|
||||
|
||||
assertThat("good response", response.getIdentityKey().equals(preKeyResponseV2.getIdentityKey()));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,12 +1,11 @@
|
|||
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.controllers.KeysController;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
|
||||
|
@ -28,8 +27,10 @@ import javax.ws.rs.core.Response;
|
|||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
@ -63,9 +64,9 @@ public class KeyControllerTest {
|
|||
@Rule
|
||||
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new KeysController(rateLimiters, keys, accounts, null))
|
||||
.addResource(new KeysController(rateLimiters, keys, accounts))
|
||||
.build();
|
||||
|
||||
@Before
|
||||
|
@ -103,14 +104,15 @@ public class KeyControllerTest {
|
|||
when(existsAccount.getDevice(2L)).thenReturn(Optional.of(sampleDevice2));
|
||||
when(existsAccount.getDevice(3L)).thenReturn(Optional.of(sampleDevice3));
|
||||
when(existsAccount.getDevice(4L)).thenReturn(Optional.of(sampleDevice4));
|
||||
when(existsAccount.getDevice(22L)).thenReturn(Optional.<Device>absent());
|
||||
when(existsAccount.getDevice(22L)).thenReturn(Optional.<Device>empty());
|
||||
when(existsAccount.getDevices()).thenReturn(allDevices);
|
||||
when(existsAccount.isActive()).thenReturn(true);
|
||||
when(existsAccount.getIdentityKey()).thenReturn("existsidentitykey");
|
||||
when(existsAccount.getNumber()).thenReturn(EXISTS_NUMBER);
|
||||
when(existsAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of("1337".getBytes()));
|
||||
|
||||
when(accounts.get(EXISTS_NUMBER)).thenReturn(Optional.of(existsAccount));
|
||||
when(accounts.get(NOT_EXISTS_NUMBER)).thenReturn(Optional.<Account>absent());
|
||||
when(accounts.get(NOT_EXISTS_NUMBER)).thenReturn(Optional.<Account>empty());
|
||||
|
||||
when(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter);
|
||||
|
||||
|
@ -118,7 +120,7 @@ public class KeyControllerTest {
|
|||
singleDevice.add(SAMPLE_KEY);
|
||||
when(keys.get(eq(EXISTS_NUMBER), eq(1L))).thenReturn(Optional.of(singleDevice));
|
||||
|
||||
when(keys.get(eq(NOT_EXISTS_NUMBER), eq(1L))).thenReturn(Optional.<List<KeyRecord>>absent());
|
||||
when(keys.get(eq(NOT_EXISTS_NUMBER), eq(1L))).thenReturn(Optional.<List<KeyRecord>>empty());
|
||||
|
||||
List<KeyRecord> multiDevice = new LinkedList<>();
|
||||
multiDevice.add(SAMPLE_KEY);
|
||||
|
@ -191,6 +193,49 @@ public class KeyControllerTest {
|
|||
verifyNoMoreInteractions(keys);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnidentifiedRequest() throws Exception {
|
||||
PreKeyResponse result = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
|
||||
.request()
|
||||
.header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("1337".getBytes()))
|
||||
.get(PreKeyResponse.class);
|
||||
|
||||
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||
assertThat(result.getDevicesCount()).isEqualTo(1);
|
||||
assertThat(result.getDevice(1).getPreKey().getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
|
||||
assertThat(result.getDevice(1).getPreKey().getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
|
||||
assertThat(result.getDevice(1).getSignedPreKey()).isEqualTo(existsAccount.getDevice(1).get().getSignedPreKey());
|
||||
|
||||
verify(keys).get(eq(EXISTS_NUMBER), eq(1L));
|
||||
verifyNoMoreInteractions(keys);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnauthorizedUnidentifiedRequest() throws Exception {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
|
||||
.request()
|
||||
.header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("9999".getBytes()))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
verifyNoMoreInteractions(keys);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMalformedUnidentifiedRequest() throws Exception {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
|
||||
.request()
|
||||
.header(OptionalAccess.UNIDENTIFIED, "$$$$$$$$$")
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
verifyNoMoreInteractions(keys);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void validMultiRequestTestV2() throws Exception {
|
||||
PreKeyResponse results = resources.getJerseyTest()
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Optional;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
|
@ -15,7 +15,6 @@ import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
|||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.StaleDevices;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||
|
@ -26,6 +25,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
|||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
@ -33,14 +33,17 @@ import javax.ws.rs.core.Response;
|
|||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
@ -54,7 +57,6 @@ public class MessageControllerTest {
|
|||
|
||||
private final PushSender pushSender = mock(PushSender.class );
|
||||
private final ReceiptSender receiptSender = mock(ReceiptSender.class);
|
||||
private final FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
|
||||
private final AccountsManager accountsManager = mock(AccountsManager.class );
|
||||
private final MessagesManager messagesManager = mock(MessagesManager.class);
|
||||
private final RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||
|
@ -66,27 +68,27 @@ public class MessageControllerTest {
|
|||
@Rule
|
||||
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new MessageController(rateLimiters, pushSender, receiptSender, accountsManager,
|
||||
messagesManager, federatedClientManager, apnFallbackManager))
|
||||
messagesManager, apnFallbackManager))
|
||||
.build();
|
||||
|
||||
|
||||
@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, new SignedPreKey(333, "baz", "boop"), System.currentTimeMillis(), System.currentTimeMillis(), false, false, "Test"));
|
||||
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, new SignedPreKey(333, "baz", "boop"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true));
|
||||
}};
|
||||
|
||||
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(), false, false, "Test"));
|
||||
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), false, false, "Test"));
|
||||
add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), false, false, "Test"));
|
||||
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true));
|
||||
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true));
|
||||
add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), "Test", true));
|
||||
}};
|
||||
|
||||
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList);
|
||||
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, multiDeviceList);
|
||||
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList, "1234".getBytes());
|
||||
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, multiDeviceList, "1234".getBytes());
|
||||
|
||||
when(accountsManager.get(eq(SINGLE_DEVICE_RECIPIENT))).thenReturn(Optional.of(singleDeviceAccount));
|
||||
when(accountsManager.get(eq(MULTI_DEVICE_RECIPIENT))).thenReturn(Optional.of(multiDeviceAccount));
|
||||
|
@ -106,7 +108,43 @@ public class MessageControllerTest {
|
|||
|
||||
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
|
||||
|
||||
verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), any(Envelope.class));
|
||||
ArgumentCaptor<Envelope> captor = ArgumentCaptor.forClass(Envelope.class);
|
||||
verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), captor.capture());
|
||||
|
||||
assertTrue(captor.getValue().hasSource());
|
||||
assertTrue(captor.getValue().hasSourceDevice());
|
||||
}
|
||||
|
||||
@Test
|
||||
public synchronized void testSingleDeviceCurrentUnidentified() throws Exception {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/messages/%s", SINGLE_DEVICE_RECIPIENT))
|
||||
.request()
|
||||
.header(OptionalAccess.UNIDENTIFIED, Base64.encodeBytes("1234".getBytes()))
|
||||
.put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
|
||||
|
||||
ArgumentCaptor<Envelope> captor = ArgumentCaptor.forClass(Envelope.class);
|
||||
verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), captor.capture());
|
||||
|
||||
assertFalse(captor.getValue().hasSource());
|
||||
assertFalse(captor.getValue().hasSourceDevice());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public synchronized void testSendBadAuth() throws Exception {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/messages/%s", SINGLE_DEVICE_RECIPIENT))
|
||||
.request()
|
||||
.put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat("Good Response", response.getStatus(), is(equalTo(401)));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -187,9 +225,11 @@ public class MessageControllerTest {
|
|||
final long timestampOne = 313377;
|
||||
final long timestampTwo = 313388;
|
||||
|
||||
final UUID uuidOne = UUID.randomUUID();
|
||||
|
||||
List<OutgoingMessageEntity> messages = new LinkedList<OutgoingMessageEntity>() {{
|
||||
add(new OutgoingMessageEntity(1L, false, Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", 2, "hi there".getBytes(), null));
|
||||
add(new OutgoingMessageEntity(2L, false, Envelope.Type.RECEIPT_VALUE, null, timestampTwo, "+14152222222", 2, null, null));
|
||||
add(new OutgoingMessageEntity(1L, false, uuidOne, Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", 2, "hi there".getBytes(), null, 0));
|
||||
add(new OutgoingMessageEntity(2L, false, null, Envelope.Type.RECEIPT_VALUE, null, timestampTwo, "+14152222222", 2, null, null, 0));
|
||||
}};
|
||||
|
||||
OutgoingMessageEntityList messagesList = new OutgoingMessageEntityList(messages, false);
|
||||
|
@ -211,6 +251,9 @@ public class MessageControllerTest {
|
|||
|
||||
assertEquals(response.getMessages().get(0).getTimestamp(), timestampOne);
|
||||
assertEquals(response.getMessages().get(1).getTimestamp(), timestampTwo);
|
||||
|
||||
assertEquals(response.getMessages().get(0).getGuid(), uuidOne);
|
||||
assertEquals(response.getMessages().get(1).getGuid(), null);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -219,8 +262,8 @@ public class MessageControllerTest {
|
|||
final long timestampTwo = 313388;
|
||||
|
||||
List<OutgoingMessageEntity> messages = new LinkedList<OutgoingMessageEntity>() {{
|
||||
add(new OutgoingMessageEntity(1L, false, Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", 2, "hi there".getBytes(), null));
|
||||
add(new OutgoingMessageEntity(2L, false, Envelope.Type.RECEIPT_VALUE, null, timestampTwo, "+14152222222", 2, null, null));
|
||||
add(new OutgoingMessageEntity(1L, false, UUID.randomUUID(), Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", 2, "hi there".getBytes(), null, 0));
|
||||
add(new OutgoingMessageEntity(2L, false, UUID.randomUUID(), Envelope.Type.RECEIPT_VALUE, null, timestampTwo, "+14152222222", 2, null, null, 0));
|
||||
}};
|
||||
|
||||
OutgoingMessageEntityList messagesList = new OutgoingMessageEntityList(messages, false);
|
||||
|
@ -240,21 +283,22 @@ public class MessageControllerTest {
|
|||
@Test
|
||||
public synchronized void testDeleteMessages() throws Exception {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
|
||||
when(messagesManager.delete(AuthHelper.VALID_NUMBER, 1, "+14152222222", 31337))
|
||||
.thenReturn(Optional.of(new OutgoingMessageEntity(31337L, true,
|
||||
.thenReturn(Optional.of(new OutgoingMessageEntity(31337L, true, null,
|
||||
Envelope.Type.CIPHERTEXT_VALUE,
|
||||
null, timestamp,
|
||||
"+14152222222", 1, "hi".getBytes(), null)));
|
||||
"+14152222222", 1, "hi".getBytes(), null, 0)));
|
||||
|
||||
when(messagesManager.delete(AuthHelper.VALID_NUMBER, 1, "+14152222222", 31338))
|
||||
.thenReturn(Optional.of(new OutgoingMessageEntity(31337L, true,
|
||||
.thenReturn(Optional.of(new OutgoingMessageEntity(31337L, true, null,
|
||||
Envelope.Type.RECEIPT_VALUE,
|
||||
null, System.currentTimeMillis(),
|
||||
"+14152222222", 1, null, null)));
|
||||
"+14152222222", 1, null, null, 0)));
|
||||
|
||||
|
||||
when(messagesManager.delete(AuthHelper.VALID_NUMBER, 1, "+14152222222", 31339))
|
||||
.thenReturn(Optional.<OutgoingMessageEntity>absent());
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
Response response = resources.getJerseyTest()
|
||||
.target(String.format("/v1/messages/%s/%d", "+14152222222", 31337))
|
||||
|
@ -263,7 +307,7 @@ public class MessageControllerTest {
|
|||
.delete();
|
||||
|
||||
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
|
||||
verify(receiptSender).sendReceipt(any(Account.class), eq("+14152222222"), eq(timestamp), eq(Optional.<String>absent()));
|
||||
verify(receiptSender).sendReceipt(any(Account.class), eq("+14152222222"), eq(timestamp));
|
||||
|
||||
response = resources.getJerseyTest()
|
||||
.target(String.format("/v1/messages/%s/%d", "+14152222222", 31338))
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
|
||||
import org.whispersystems.textsecuregcm.configuration.ProfilesConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.ProfileController;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
|
@ -17,6 +15,10 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
|||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||
import static org.assertj.core.api.Java6Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
@ -38,7 +40,7 @@ public class ProfileControllerTest {
|
|||
@ClassRule
|
||||
public static final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
|
||||
.setMapper(SystemMapper.getMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new ProfileController(rateLimiters,
|
||||
|
@ -53,9 +55,10 @@ public class ProfileControllerTest {
|
|||
Account profileAccount = mock(Account.class);
|
||||
|
||||
when(profileAccount.getIdentityKey()).thenReturn("bar");
|
||||
when(profileAccount.getName()).thenReturn("baz");
|
||||
when(profileAccount.getProfileName()).thenReturn("baz");
|
||||
when(profileAccount.getAvatar()).thenReturn("profiles/bang");
|
||||
when(profileAccount.getAvatarDigest()).thenReturn("buh");
|
||||
when(profileAccount.isActive()).thenReturn(true);
|
||||
|
||||
when(accountsManager.get(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(profileAccount));
|
||||
}
|
||||
|
@ -78,5 +81,14 @@ public class ProfileControllerTest {
|
|||
verify(rateLimiter, times(1)).validate(AuthHelper.VALID_NUMBER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProfileGetUnauthorized() throws Exception {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/profile/" + AuthHelper.VALID_NUMBER_TWO)
|
||||
.request()
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.whispersystems.textsecuregcm.tests.push;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.relayrides.pushy.apns.ApnsClient;
|
||||
import com.relayrides.pushy.apns.ApnsServerException;
|
||||
|
@ -22,6 +21,7 @@ import org.whispersystems.textsecuregcm.storage.Device;
|
|||
import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.netty.util.concurrent.DefaultEventExecutor;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.whispersystems.textsecuregcm.tests.push;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Matchers;
|
||||
|
@ -15,6 +14,8 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
|||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import org.whispersystems.textsecuregcm.storage.Account;
|
|||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
@ -49,21 +50,21 @@ public class AccountTest {
|
|||
Account recentAccount = new Account("+14152222222", new HashSet<Device>() {{
|
||||
add(recentMasterDevice);
|
||||
add(recentSecondaryDevice);
|
||||
}});
|
||||
}}, "1234".getBytes());
|
||||
|
||||
assertTrue(recentAccount.isActive());
|
||||
|
||||
Account oldSecondaryAccount = new Account("+14152222222", new HashSet<Device>() {{
|
||||
add(recentMasterDevice);
|
||||
add(agingSecondaryDevice);
|
||||
}});
|
||||
}}, "1234".getBytes());
|
||||
|
||||
assertTrue(oldSecondaryAccount.isActive());
|
||||
|
||||
Account agingPrimaryAccount = new Account("+14152222222", new HashSet<Device>() {{
|
||||
add(oldMasterDevice);
|
||||
add(agingSecondaryDevice);
|
||||
}});
|
||||
}}, "1234".getBytes());
|
||||
|
||||
assertTrue(agingPrimaryAccount.isActive());
|
||||
}
|
||||
|
@ -73,7 +74,7 @@ public class AccountTest {
|
|||
Account oldPrimaryAccount = new Account("+14152222222", new HashSet<Device>() {{
|
||||
add(oldMasterDevice);
|
||||
add(oldSecondaryDevice);
|
||||
}});
|
||||
}}, "1234".getBytes());
|
||||
|
||||
assertFalse(oldPrimaryAccount.isActive());
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.whispersystems.textsecuregcm.tests.storage;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
@ -18,17 +17,14 @@ import org.whispersystems.textsecuregcm.util.Util;
|
|||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
public class DirectoryReconcilerTest {
|
||||
|
||||
|
@ -53,8 +49,6 @@ public class DirectoryReconcilerTest {
|
|||
public void setup() {
|
||||
when(account.getNumber()).thenReturn(VALID_NUMBER);
|
||||
when(account.isActive()).thenReturn(true);
|
||||
when(account.isVideoSupported()).thenReturn(true);
|
||||
when(account.isVoiceSupported()).thenReturn(true);
|
||||
when(inactiveAccount.getNumber()).thenReturn(INACTIVE_NUMBER);
|
||||
when(inactiveAccount.isActive()).thenReturn(false);
|
||||
|
||||
|
@ -66,7 +60,7 @@ public class DirectoryReconcilerTest {
|
|||
|
||||
when(reconciliationClient.sendChunk(any())).thenReturn(successResponse);
|
||||
|
||||
when(reconciliationCache.getLastNumber()).thenReturn(Optional.absent());
|
||||
when(reconciliationCache.getLastNumber()).thenReturn(Optional.empty());
|
||||
when(reconciliationCache.claimActiveWork(any(), anyLong())).thenReturn(true);
|
||||
when(reconciliationCache.isAccelerated()).thenReturn(false);
|
||||
}
|
||||
|
@ -155,7 +149,7 @@ public class DirectoryReconcilerTest {
|
|||
assertThat(request.getValue().getNumbers()).isEqualTo(Collections.emptyList());
|
||||
|
||||
verify(reconciliationCache, times(1)).getLastNumber();
|
||||
verify(reconciliationCache, times(1)).setLastNumber(eq(Optional.absent()));
|
||||
verify(reconciliationCache, times(1)).setLastNumber(eq(Optional.empty()));
|
||||
verify(reconciliationCache, times(1)).clearAccelerate();
|
||||
verify(reconciliationCache, times(1)).isAccelerated();
|
||||
verify(reconciliationCache, times(2)).claimActiveWork(any(), anyLong());
|
||||
|
@ -192,7 +186,7 @@ public class DirectoryReconcilerTest {
|
|||
assertThat(addedContact.getValue().getToken()).isEqualTo(Util.getContactToken(VALID_NUMBER));
|
||||
|
||||
verify(reconciliationCache, times(1)).getLastNumber();
|
||||
verify(reconciliationCache, times(1)).setLastNumber(eq(Optional.absent()));
|
||||
verify(reconciliationCache, times(1)).setLastNumber(eq(Optional.empty()));
|
||||
verify(reconciliationCache, times(1)).clearAccelerate();
|
||||
verify(reconciliationCache, times(1)).claimActiveWork(any(), anyLong());
|
||||
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
package org.whispersystems.textsecuregcm.tests.util;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.whispersystems.dropwizard.simpleauth.AuthDynamicFeature;
|
||||
import org.whispersystems.dropwizard.simpleauth.BasicCredentialAuthFilter;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
|
||||
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.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.dropwizard.auth.AuthDynamicFeature;
|
||||
import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
|
||||
import static org.mockito.Matchers.anyLong;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
@ -31,6 +26,8 @@ public class AuthHelper {
|
|||
public static final String INVVALID_NUMBER = "+14151111111";
|
||||
public static final String INVALID_PASSWORD = "bar";
|
||||
|
||||
public static final String VALID_IDENTITY = "BcxxDU9FGMda70E7+Uvm7pnQcEdXQ64aJCpPUeRSfcFo";
|
||||
|
||||
public static AccountsManager ACCOUNTS_MANAGER = mock(AccountsManager.class );
|
||||
public static Account VALID_ACCOUNT = mock(Account.class );
|
||||
public static Account VALID_ACCOUNT_TWO = mock(Account.class);
|
||||
|
@ -53,29 +50,24 @@ public class AuthHelper {
|
|||
when(VALID_ACCOUNT_TWO.getNumber()).thenReturn(VALID_NUMBER_TWO);
|
||||
when(VALID_ACCOUNT.getAuthenticatedDevice()).thenReturn(Optional.of(VALID_DEVICE));
|
||||
when(VALID_ACCOUNT_TWO.getAuthenticatedDevice()).thenReturn(Optional.of(VALID_DEVICE_TWO));
|
||||
when(VALID_ACCOUNT.getRelay()).thenReturn(Optional.<String>absent());
|
||||
when(VALID_ACCOUNT_TWO.getRelay()).thenReturn(Optional.<String>absent());
|
||||
when(VALID_ACCOUNT.getRelay()).thenReturn(Optional.<String>empty());
|
||||
when(VALID_ACCOUNT_TWO.getRelay()).thenReturn(Optional.<String>empty());
|
||||
when(VALID_ACCOUNT.isActive()).thenReturn(true);
|
||||
when(VALID_ACCOUNT_TWO.isActive()).thenReturn(true);
|
||||
when(VALID_ACCOUNT.getIdentityKey()).thenReturn(VALID_IDENTITY);
|
||||
when(ACCOUNTS_MANAGER.get(VALID_NUMBER)).thenReturn(Optional.of(VALID_ACCOUNT));
|
||||
when(ACCOUNTS_MANAGER.get(VALID_NUMBER_TWO)).thenReturn(Optional.of(VALID_ACCOUNT_TWO));
|
||||
|
||||
List<FederatedPeer> peer = new LinkedList<FederatedPeer>() {{
|
||||
add(new FederatedPeer("cyanogen", "https://foo", "foofoo", "bazzzzz"));
|
||||
}};
|
||||
|
||||
FederationConfiguration federationConfiguration = mock(FederationConfiguration.class);
|
||||
when(federationConfiguration.getPeers()).thenReturn(peer);
|
||||
|
||||
return new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<Account>()
|
||||
.setAuthenticator(new AccountAuthenticator(ACCOUNTS_MANAGER))
|
||||
.setPrincipal(Account.class)
|
||||
.buildAuthFilter(),
|
||||
new BasicCredentialAuthFilter.Builder<FederatedPeer>()
|
||||
.setAuthenticator(new FederatedPeerAuthenticator(federationConfiguration))
|
||||
.setPrincipal(FederatedPeer.class)
|
||||
.buildAuthFilter());
|
||||
}
|
||||
|
||||
public static String getAuthHeader(String number, String password) {
|
||||
return "Basic " + Base64.encodeBytes((number + ":" + password).getBytes());
|
||||
}
|
||||
|
||||
public static String getUnidentifiedAccessHeader(byte[] key) {
|
||||
return Base64.encodeBytes(key);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.whispersystems.textsecuregcm.tests.websocket;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import com.google.protobuf.ByteString;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||
|
@ -27,6 +26,8 @@ import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
|
|||
import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||
import org.whispersystems.websocket.WebSocketClient;
|
||||
import org.whispersystems.websocket.auth.WebSocketAuthenticator;
|
||||
import org.whispersystems.websocket.auth.WebSocketAuthenticator.AuthenticationResult;
|
||||
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
|
||||
import org.whispersystems.websocket.session.WebSocketSessionContext;
|
||||
|
||||
|
@ -35,7 +36,9 @@ import java.util.HashMap;
|
|||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
import static org.junit.Assert.*;
|
||||
|
@ -72,7 +75,7 @@ public class WebSocketConnectionTest {
|
|||
.thenReturn(Optional.of(account));
|
||||
|
||||
when(accountAuthenticator.authenticate(eq(new BasicCredentials(INVALID_USER, INVALID_PASSWORD))))
|
||||
.thenReturn(Optional.<Account>absent());
|
||||
.thenReturn(Optional.<Account>empty());
|
||||
|
||||
when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device));
|
||||
|
||||
|
@ -81,8 +84,8 @@ public class WebSocketConnectionTest {
|
|||
put("password", new LinkedList<String>() {{add(VALID_PASSWORD);}});
|
||||
}});
|
||||
|
||||
Optional<Account> account = webSocketAuthenticator.authenticate(upgradeRequest);
|
||||
when(sessionContext.getAuthenticated(Account.class)).thenReturn(account.get());
|
||||
AuthenticationResult<Account> account = webSocketAuthenticator.authenticate(upgradeRequest);
|
||||
when(sessionContext.getAuthenticated(Account.class)).thenReturn(account.getUser().orElse(null));
|
||||
|
||||
connectListener.onWebSocketConnect(sessionContext);
|
||||
|
||||
|
@ -94,7 +97,8 @@ public class WebSocketConnectionTest {
|
|||
}});
|
||||
|
||||
account = webSocketAuthenticator.authenticate(upgradeRequest);
|
||||
assertFalse(account.isPresent());
|
||||
assertFalse(account.getUser().isPresent());
|
||||
assertTrue(account.isRequired());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -125,7 +129,7 @@ public class WebSocketConnectionTest {
|
|||
when(sender1.getDevices()).thenReturn(sender1devices);
|
||||
|
||||
when(accountsManager.get("sender1")).thenReturn(Optional.of(sender1));
|
||||
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>absent());
|
||||
when(accountsManager.get("sender2")).thenReturn(Optional.empty());
|
||||
|
||||
when(storedMessages.getMessagesForDevice(account.getNumber(), device.getId()))
|
||||
.thenReturn(outgoingMessagesList);
|
||||
|
@ -160,7 +164,7 @@ public class WebSocketConnectionTest {
|
|||
futures.get(2).setException(new IOException());
|
||||
|
||||
verify(storedMessages, times(1)).delete(eq(account.getNumber()), eq(2L), eq(2L), eq(false));
|
||||
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender1"), eq(2222L), eq(Optional.<String>absent()));
|
||||
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender1"), eq(2222L));
|
||||
|
||||
connection.onDispatchUnsubscribed(websocketAddress.serialize());
|
||||
verify(client).close(anyInt(), anyString());
|
||||
|
@ -208,7 +212,7 @@ public class WebSocketConnectionTest {
|
|||
when(sender1.getDevices()).thenReturn(sender1devices);
|
||||
|
||||
when(accountsManager.get("sender1")).thenReturn(Optional.of(sender1));
|
||||
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>absent());
|
||||
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>empty());
|
||||
|
||||
when(storedMessages.getMessagesForDevice(account.getNumber(), device.getId()))
|
||||
.thenReturn(pendingMessagesList);
|
||||
|
@ -250,7 +254,7 @@ public class WebSocketConnectionTest {
|
|||
futures.get(1).set(response);
|
||||
futures.get(0).setException(new IOException());
|
||||
|
||||
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender2"), eq(secondMessage.getTimestamp()), eq(Optional.<String>absent()));
|
||||
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender2"), eq(secondMessage.getTimestamp()));
|
||||
verify(websocketSender, times(1)).queueMessage(eq(account), eq(device), any(Envelope.class));
|
||||
verify(pushSender, times(1)).sendQueuedNotification(eq(account), eq(device));
|
||||
|
||||
|
@ -285,14 +289,14 @@ public class WebSocketConnectionTest {
|
|||
.build();
|
||||
|
||||
List<OutgoingMessageEntity> pendingMessages = new LinkedList<OutgoingMessageEntity>() {{
|
||||
add(new OutgoingMessageEntity(1, true, firstMessage.getType().getNumber(), firstMessage.getRelay(),
|
||||
add(new OutgoingMessageEntity(1, true, UUID.randomUUID(), firstMessage.getType().getNumber(), firstMessage.getRelay(),
|
||||
firstMessage.getTimestamp(), firstMessage.getSource(),
|
||||
firstMessage.getSourceDevice(), firstMessage.getLegacyMessage().toByteArray(),
|
||||
firstMessage.getContent().toByteArray()));
|
||||
add(new OutgoingMessageEntity(2, false, secondMessage.getType().getNumber(), secondMessage.getRelay(),
|
||||
firstMessage.getContent().toByteArray(), 0));
|
||||
add(new OutgoingMessageEntity(2, false, UUID.randomUUID(), secondMessage.getType().getNumber(), secondMessage.getRelay(),
|
||||
secondMessage.getTimestamp(), secondMessage.getSource(),
|
||||
secondMessage.getSourceDevice(), secondMessage.getLegacyMessage().toByteArray(),
|
||||
secondMessage.getContent().toByteArray()));
|
||||
secondMessage.getContent().toByteArray(), 0));
|
||||
}};
|
||||
|
||||
OutgoingMessageEntityList pendingMessagesList = new OutgoingMessageEntityList(pendingMessages, false);
|
||||
|
@ -313,7 +317,7 @@ public class WebSocketConnectionTest {
|
|||
when(sender1.getDevices()).thenReturn(sender1devices);
|
||||
|
||||
when(accountsManager.get("sender1")).thenReturn(Optional.of(sender1));
|
||||
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>absent());
|
||||
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>empty());
|
||||
|
||||
when(storedMessages.getMessagesForDevice(account.getNumber(), device.getId()))
|
||||
.thenReturn(pendingMessagesList);
|
||||
|
@ -346,7 +350,7 @@ public class WebSocketConnectionTest {
|
|||
futures.get(1).set(response);
|
||||
futures.get(0).setException(new IOException());
|
||||
|
||||
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender2"), eq(secondMessage.getTimestamp()), eq(Optional.<String>absent()));
|
||||
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender2"), eq(secondMessage.getTimestamp()));
|
||||
verifyNoMoreInteractions(websocketSender);
|
||||
verifyNoMoreInteractions(pushSender);
|
||||
|
||||
|
@ -356,8 +360,8 @@ public class WebSocketConnectionTest {
|
|||
|
||||
|
||||
private OutgoingMessageEntity createMessage(long id, boolean cached, String sender, long timestamp, boolean receipt, String content) {
|
||||
return new OutgoingMessageEntity(id, cached, receipt ? Envelope.Type.RECEIPT_VALUE : Envelope.Type.CIPHERTEXT_VALUE,
|
||||
null, timestamp, sender, 1, content.getBytes(), null);
|
||||
return new OutgoingMessageEntity(id, cached, UUID.randomUUID(), receipt ? Envelope.Type.RECEIPT_VALUE : Envelope.Type.CIPHERTEXT_VALUE,
|
||||
null, timestamp, sender, 1, content.getBytes(), null, 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue