parent
8ea805e4e3
commit
322548f078
4
pom.xml
4
pom.xml
|
@ -181,8 +181,8 @@
|
|||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.2</version>
|
||||
<configuration>
|
||||
<source>1.7</source>
|
||||
<target>1.7</target>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
|
|
|
@ -21,11 +21,12 @@ import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
|||
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ProfilesConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AttachmentsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||
|
@ -57,7 +58,12 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private S3Configuration s3;
|
||||
private AttachmentsConfiguration attachments;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private ProfilesConfiguration profiles;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
|
@ -145,8 +151,8 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
return httpClient;
|
||||
}
|
||||
|
||||
public S3Configuration getS3Configuration() {
|
||||
return s3;
|
||||
public AttachmentsConfiguration getAttachmentsConfiguration() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public RedisConfiguration getCacheConfiguration() {
|
||||
|
@ -193,6 +199,10 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
return apn;
|
||||
}
|
||||
|
||||
public ProfilesConfiguration getProfilesConfiguration() {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getTestDevices() {
|
||||
Map<String, Integer> results = new HashMap<>();
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@ import org.whispersystems.textsecuregcm.push.GCMSender;
|
|||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
import org.whispersystems.textsecuregcm.push.WebsocketSender;
|
||||
import org.whispersystems.textsecuregcm.s3.UrlSigner;
|
||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
@ -81,7 +82,6 @@ import org.whispersystems.textsecuregcm.storage.PendingDevices;
|
|||
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
||||
import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
|
||||
import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler;
|
||||
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
|
||||
|
@ -183,7 +183,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(apnSender, pubSubManager);
|
||||
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
|
||||
SmsSender smsSender = new SmsSender(twilioSmsSender);
|
||||
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
|
||||
UrlSigner urlSigner = new UrlSigner(config.getAttachmentsConfiguration());
|
||||
PushSender pushSender = new PushSender(apnFallbackManager, gcmSender, apnSender, websocketSender, config.getPushConfiguration().getQueueSize());
|
||||
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
|
||||
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration());
|
||||
|
@ -197,7 +197,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
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);
|
||||
ProfileController profileController = new ProfileController(rateLimiters , accountsManager);
|
||||
ProfileController profileController = new ProfileController(rateLimiters , accountsManager, config.getProfilesConfiguration());
|
||||
|
||||
environment.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<Account>()
|
||||
.setAuthenticator(deviceAuthenticator)
|
||||
|
|
|
@ -19,7 +19,7 @@ package org.whispersystems.textsecuregcm.configuration;
|
|||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
public class S3Configuration {
|
||||
public class AttachmentsConfiguration {
|
||||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
|
@ -31,7 +31,7 @@ public class S3Configuration {
|
|||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private String attachmentsBucket;
|
||||
private String bucket;
|
||||
|
||||
public String getAccessKey() {
|
||||
return accessKey;
|
||||
|
@ -41,7 +41,7 @@ public class S3Configuration {
|
|||
return accessSecret;
|
||||
}
|
||||
|
||||
public String getAttachmentsBucket() {
|
||||
return attachmentsBucket;
|
||||
public String getBucket() {
|
||||
return bucket;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
public class ProfilesConfiguration {
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private String accessKey;
|
||||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private String accessSecret;
|
||||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private String bucket;
|
||||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private String region;
|
||||
|
||||
public String getAccessKey() {
|
||||
return accessKey;
|
||||
}
|
||||
|
||||
public String getAccessSecret() {
|
||||
return accessSecret;
|
||||
}
|
||||
|
||||
public String getBucket() {
|
||||
return bucket;
|
||||
}
|
||||
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
}
|
|
@ -28,7 +28,7 @@ import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
|||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.Conversions;
|
||||
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
||||
import org.whispersystems.textsecuregcm.s3.UrlSigner;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
|
@ -40,7 +40,6 @@ import javax.ws.rs.core.MediaType;
|
|||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
|
|
@ -1,38 +1,83 @@
|
|||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.amazonaws.auth.AWSCredentials;
|
||||
import com.amazonaws.auth.AWSCredentialsProvider;
|
||||
import com.amazonaws.auth.AWSStaticCredentialsProvider;
|
||||
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.configuration.ProfilesConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.Profile;
|
||||
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v1/profile")
|
||||
public class ProfileController {
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final AccountsManager accountsManager;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final AccountsManager accountsManager;
|
||||
|
||||
public ProfileController(RateLimiters rateLimiters, AccountsManager accountsManager) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.accountsManager = accountsManager;
|
||||
private final PolicySigner policySigner;
|
||||
private final PostPolicyGenerator policyGenerator;
|
||||
|
||||
private final AmazonS3 s3client;
|
||||
private final String bucket;
|
||||
|
||||
public ProfileController(RateLimiters rateLimiters,
|
||||
AccountsManager accountsManager,
|
||||
ProfilesConfiguration profilesConfiguration)
|
||||
{
|
||||
AWSCredentials credentials = new BasicAWSCredentials(profilesConfiguration.getAccessKey(), profilesConfiguration.getAccessSecret());
|
||||
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
|
||||
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.accountsManager = accountsManager;
|
||||
this.bucket = profilesConfiguration.getBucket();
|
||||
this.s3client = AmazonS3Client.builder()
|
||||
.withCredentials(credentialsProvider)
|
||||
.withRegion(profilesConfiguration.getRegion())
|
||||
.build();
|
||||
|
||||
this.policyGenerator = new PostPolicyGenerator(profilesConfiguration.getRegion(),
|
||||
profilesConfiguration.getBucket(),
|
||||
profilesConfiguration.getAccessKey());
|
||||
|
||||
this.policySigner = new PolicySigner(profilesConfiguration.getAccessSecret(),
|
||||
profilesConfiguration.getRegion());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{number}")
|
||||
public Profile getProfile(@Auth Account account, @PathParam("number") String number)
|
||||
public Profile getProfile(@Auth Account account,
|
||||
@PathParam("number") String number,
|
||||
@QueryParam("ca") boolean useCaCertificate)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
rateLimiters.getProfileLimiter().validate(account.getNumber());
|
||||
|
@ -43,8 +88,47 @@ public class ProfileController {
|
|||
throw new WebApplicationException(Response.status(404).build());
|
||||
}
|
||||
|
||||
return new Profile(accountProfile.get().getIdentityKey());
|
||||
return new Profile(accountProfile.get().getName(),
|
||||
accountProfile.get().getAvatar(),
|
||||
accountProfile.get().getIdentityKey());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@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());
|
||||
accountsManager.update(account);
|
||||
}
|
||||
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/form/avatar")
|
||||
public ProfileAvatarUploadAttributes getAvatarUploadForm(@Auth Account account) {
|
||||
String previousAvatar = account.getAvatar();
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
String objectName = generateAvatarObjectName();
|
||||
Pair<String, String> policy = policyGenerator.createFor(now, objectName);
|
||||
String signature = policySigner.getSignature(now, policy.second());
|
||||
|
||||
if (previousAvatar != null && previousAvatar.startsWith("profiles/")) {
|
||||
s3client.deleteObject(bucket, previousAvatar);
|
||||
}
|
||||
|
||||
account.setAvatar(objectName);
|
||||
accountsManager.update(account);
|
||||
|
||||
return new ProfileAvatarUploadAttributes(objectName, policy.first(), "private", "AWS4-HMAC-SHA256",
|
||||
now.format(PostPolicyGenerator.AWS_DATE_TIME), policy.second(), signature);
|
||||
}
|
||||
|
||||
private String generateAvatarObjectName() {
|
||||
byte[] object = new byte[16];
|
||||
new SecureRandom().nextBytes(object);
|
||||
|
||||
return "profiles/" + Base64.encodeBase64URLSafeString(object);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,14 +3,26 @@ package org.whispersystems.textsecuregcm.entities;
|
|||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.Max;
|
||||
|
||||
public class Profile {
|
||||
|
||||
@JsonProperty
|
||||
private String identityKey;
|
||||
|
||||
@JsonProperty
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private String avatar;
|
||||
|
||||
public Profile() {}
|
||||
|
||||
public Profile(String identityKey) {
|
||||
public Profile(String name, String avatar, String identityKey) {
|
||||
this.name = name;
|
||||
this.avatar = avatar;
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
|
@ -18,4 +30,15 @@ public class Profile {
|
|||
public String getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public String getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class ProfileAvatarUploadAttributes {
|
||||
|
||||
@JsonProperty
|
||||
private String key;
|
||||
|
||||
@JsonProperty
|
||||
private String credential;
|
||||
|
||||
@JsonProperty
|
||||
private String acl;
|
||||
|
||||
@JsonProperty
|
||||
private String algorithm;
|
||||
|
||||
@JsonProperty
|
||||
private String date;
|
||||
|
||||
@JsonProperty
|
||||
private String policy;
|
||||
|
||||
@JsonProperty
|
||||
private String signature;
|
||||
|
||||
public ProfileAvatarUploadAttributes() {}
|
||||
|
||||
public ProfileAvatarUploadAttributes(String key, String credential,
|
||||
String acl, String algorithm,
|
||||
String date, String policy,
|
||||
String signature)
|
||||
{
|
||||
this.key = key;
|
||||
this.credential = credential;
|
||||
this.acl = acl;
|
||||
this.algorithm = algorithm;
|
||||
this.date = date;
|
||||
this.policy = policy;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package org.whispersystems.textsecuregcm.s3;
|
||||
|
||||
import com.amazonaws.util.Base16Lower;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class PolicySigner {
|
||||
|
||||
private final String awsAccessSecret;
|
||||
private final String region;
|
||||
|
||||
public PolicySigner(String awsAccessSecret, String region) {
|
||||
this.awsAccessSecret = awsAccessSecret;
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
public String getSignature(ZonedDateTime now, String policy) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
|
||||
mac.init(new SecretKeySpec(("AWS4" + awsAccessSecret).getBytes("UTF-8"), "HmacSHA256"));
|
||||
byte[] dateKey = mac.doFinal(now.format(DateTimeFormatter.ofPattern("yyyyMMdd")).getBytes("UTF-8"));
|
||||
|
||||
mac.init(new SecretKeySpec(dateKey, "HmacSHA256"));
|
||||
byte[] dateRegionKey = mac.doFinal(region.getBytes("UTF-8"));
|
||||
|
||||
mac.init(new SecretKeySpec(dateRegionKey, "HmacSHA256"));
|
||||
byte[] dateRegionServiceKey = mac.doFinal("s3".getBytes("UTF-8"));
|
||||
|
||||
mac.init(new SecretKeySpec(dateRegionServiceKey, "HmacSHA256"));
|
||||
byte[] signingKey = mac.doFinal("aws4_request".getBytes("UTF-8"));
|
||||
|
||||
mac.init(new SecretKeySpec(signingKey, "HmacSHA256"));
|
||||
|
||||
return Base16Lower.encodeAsString(mac.doFinal(policy.getBytes("UTF-8")));
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException | UnsupportedEncodingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package org.whispersystems.textsecuregcm.s3;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class PostPolicyGenerator {
|
||||
|
||||
public static final DateTimeFormatter AWS_DATE_TIME = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssX");
|
||||
private static final DateTimeFormatter CREDENTIAL_DATE = DateTimeFormatter.ofPattern("yyyyMMdd" );
|
||||
|
||||
private final String region;
|
||||
private final String bucket;
|
||||
private final String awsAccessId;
|
||||
|
||||
public PostPolicyGenerator(String region, String bucket, String awsAccessId) {
|
||||
this.region = region;
|
||||
this.bucket = bucket;
|
||||
this.awsAccessId = awsAccessId;
|
||||
}
|
||||
|
||||
public Pair<String, String> createFor(ZonedDateTime now, String object) {
|
||||
try {
|
||||
String expiration = now.plusMinutes(30).format(DateTimeFormatter.ISO_INSTANT);
|
||||
String credentialDate = now.format(CREDENTIAL_DATE);
|
||||
String requestDate = now.format(AWS_DATE_TIME );
|
||||
String credential = String.format("%s/%s/%s/s3/aws4_request", awsAccessId, credentialDate, region);
|
||||
|
||||
String policy = String.format("{ \"expiration\": \"%s\",\n" +
|
||||
" \"conditions\": [\n" +
|
||||
" {\"bucket\": \"%s\"},\n" +
|
||||
" {\"key\": \"%s\"},\n" +
|
||||
" {\"acl\": \"private\"},\n" +
|
||||
" [\"starts-with\", \"$Content-Type\", \"\"],\n" +
|
||||
"\n" +
|
||||
" {\"x-amz-credential\": \"%s\"},\n" +
|
||||
" {\"x-amz-algorithm\": \"AWS4-HMAC-SHA256\"},\n" +
|
||||
" {\"x-amz-date\": \"%s\" }\n" +
|
||||
" ]\n" +
|
||||
"}", expiration, bucket, object, credential, requestDate);
|
||||
|
||||
return new Pair<>(credential, Base64.encodeBase64String(policy.getBytes("UTF-8")));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
package org.whispersystems.textsecuregcm.s3;
|
||||
|
||||
import com.amazonaws.HttpMethod;
|
||||
import com.amazonaws.auth.AWSCredentials;
|
||||
|
@ -23,7 +23,7 @@ import com.amazonaws.services.s3.AmazonS3;
|
|||
import com.amazonaws.services.s3.AmazonS3Client;
|
||||
import com.amazonaws.services.s3.S3ClientOptions;
|
||||
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
|
||||
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AttachmentsConfiguration;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Date;
|
||||
|
@ -35,9 +35,9 @@ public class UrlSigner {
|
|||
private final AWSCredentials credentials;
|
||||
private final String bucket;
|
||||
|
||||
public UrlSigner(S3Configuration config) {
|
||||
public UrlSigner(AttachmentsConfiguration config) {
|
||||
this.credentials = new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret());
|
||||
this.bucket = config.getAttachmentsBucket();
|
||||
this.bucket = config.getBucket();
|
||||
}
|
||||
|
||||
public URL getPreSignedUrl(long attachmentId, HttpMethod method) {
|
|
@ -39,6 +39,15 @@ public class Account {
|
|||
@JsonProperty
|
||||
private String identityKey;
|
||||
|
||||
@JsonProperty
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private String avatar;
|
||||
|
||||
@JsonProperty
|
||||
private String avatarDigest;
|
||||
|
||||
@JsonIgnore
|
||||
private Device authenticatedDevice;
|
||||
|
||||
|
@ -171,4 +180,28 @@ public class Account {
|
|||
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
public void setAvatar(String avatar) {
|
||||
this.avatar = avatar;
|
||||
}
|
||||
|
||||
public String getAvatarDigest() {
|
||||
return avatarDigest;
|
||||
}
|
||||
|
||||
public void setAvatarDigest(String avatarDigest) {
|
||||
this.avatarDigest = avatarDigest;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
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;
|
||||
import org.whispersystems.textsecuregcm.entities.Profile;
|
||||
|
@ -9,34 +14,68 @@ import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
|||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||
import static org.assertj.core.api.Java6Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
public class ProfileControllerTest {
|
||||
|
||||
private static AccountsManager accountsManager = mock(AccountsManager.class );
|
||||
private static RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||
private static RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||
private static ProfilesConfiguration configuration = mock(ProfilesConfiguration.class);
|
||||
|
||||
static {
|
||||
when(configuration.getAccessKey()).thenReturn("accessKey");
|
||||
when(configuration.getAccessSecret()).thenReturn("accessSecret");
|
||||
when(configuration.getRegion()).thenReturn("us-east-1");
|
||||
when(configuration.getBucket()).thenReturn("profile-bucket");
|
||||
}
|
||||
|
||||
@ClassRule
|
||||
public static final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder())
|
||||
.setMapper(SystemMapper.getMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new ProfileController(rateLimiters,
|
||||
accountsManager,
|
||||
configuration))
|
||||
.build();
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
when(rateLimiters.getProfileLimiter()).thenReturn(rateLimiter);
|
||||
|
||||
Account profileAccount = mock(Account.class);
|
||||
|
||||
when(profileAccount.getIdentityKey()).thenReturn("bar");
|
||||
when(profileAccount.getName()).thenReturn("baz");
|
||||
when(profileAccount.getAvatar()).thenReturn("profiles/bang");
|
||||
when(profileAccount.getAvatarDigest()).thenReturn("buh");
|
||||
|
||||
when(accountsManager.get(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(profileAccount));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testProfileGet() throws RateLimitExceededException {
|
||||
Account requestAccount = mock(Account.class );
|
||||
Account profileAccount = mock(Account.class );
|
||||
RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||
RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||
AccountsManager accountsManager = mock(AccountsManager.class);
|
||||
Profile profile= resources.getJerseyTest()
|
||||
.target("/v1/profile/" + AuthHelper.VALID_NUMBER_TWO)
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||
.get(Profile.class);
|
||||
|
||||
when(rateLimiters.getProfileLimiter()).thenReturn(rateLimiter);
|
||||
when(requestAccount.getNumber()).thenReturn("foo");
|
||||
when(profileAccount.getIdentityKey()).thenReturn("bar");
|
||||
when(accountsManager.get(eq("baz"))).thenReturn(Optional.of(profileAccount));
|
||||
assertThat(profile.getIdentityKey()).isEqualTo("bar");
|
||||
assertThat(profile.getName()).isEqualTo("baz");
|
||||
assertThat(profile.getAvatar()).isEqualTo("profiles/bang");
|
||||
|
||||
ProfileController profileController = new ProfileController(rateLimiters, accountsManager);
|
||||
Profile result = profileController.getProfile(requestAccount, "baz");
|
||||
|
||||
assertEquals(result.getIdentityKey(), "bar");
|
||||
|
||||
verify(accountsManager, times(1)).get(eq("baz"));
|
||||
verify(accountsManager, times(1)).get(AuthHelper.VALID_NUMBER_TWO);
|
||||
verify(rateLimiters, times(1)).getProfileLimiter();
|
||||
verify(rateLimiter, times(1)).validate("foo");
|
||||
verify(rateLimiter, times(1)).validate(AuthHelper.VALID_NUMBER);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package org.whispersystems.textsecuregcm.tests.s3;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class PolicySignerTest {
|
||||
|
||||
@Test
|
||||
public void testSignature() throws UnsupportedEncodingException {
|
||||
Instant time = Instant.parse("2015-12-29T00:00:00Z");
|
||||
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(time, ZoneOffset.UTC);
|
||||
String encodedPolicy = "eyAiZXhwaXJhdGlvbiI6ICIyMDE1LTEyLTMwVDEyOjAwOjAwLjAwMFoiLA0KICAiY29uZGl0aW9ucyI6IFsNCiAgICB7ImJ1Y2tldCI6ICJzaWd2NGV4YW1wbGVidWNrZXQifSwNCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci91c2VyMS8iXSwNCiAgICB7ImFjbCI6ICJwdWJsaWMtcmVhZCJ9LA0KICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL3NpZ3Y0ZXhhbXBsZWJ1Y2tldC5zMy5hbWF6b25hd3MuY29tL3N1Y2Nlc3NmdWxfdXBsb2FkLmh0bWwifSwNCiAgICBbInN0YXJ0cy13aXRoIiwgIiRDb250ZW50LVR5cGUiLCAiaW1hZ2UvIl0sDQogICAgeyJ4LWFtei1tZXRhLXV1aWQiOiAiMTQzNjUxMjM2NTEyNzQifSwNCiAgICB7IngtYW16LXNlcnZlci1zaWRlLWVuY3J5cHRpb24iOiAiQUVTMjU2In0sDQogICAgWyJzdGFydHMtd2l0aCIsICIkeC1hbXotbWV0YS10YWciLCAiIl0sDQoNCiAgICB7IngtYW16LWNyZWRlbnRpYWwiOiAiQUtJQUlPU0ZPRE5ON0VYQU1QTEUvMjAxNTEyMjkvdXMtZWFzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LA0KICAgIHsieC1hbXotYWxnb3JpdGhtIjogIkFXUzQtSE1BQy1TSEEyNTYifSwNCiAgICB7IngtYW16LWRhdGUiOiAiMjAxNTEyMjlUMDAwMDAwWiIgfQ0KICBdDQp9";
|
||||
PolicySigner policySigner = new PolicySigner("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "us-east-1");
|
||||
|
||||
assertEquals(policySigner.getSignature(zonedDateTime, encodedPolicy), "8afdbf4008c03f22c2cd3cdb72e4afbb1f6a588f3255ac628749a66d7f09699e");
|
||||
}
|
||||
}
|
|
@ -2,8 +2,8 @@ package org.whispersystems.textsecuregcm.tests.util;
|
|||
|
||||
import com.amazonaws.HttpMethod;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
|
||||
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
||||
import org.whispersystems.textsecuregcm.configuration.AttachmentsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.s3.UrlSigner;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
|
@ -15,10 +15,10 @@ public class UrlSignerTest {
|
|||
|
||||
@Test
|
||||
public void testTransferAcceleration() {
|
||||
S3Configuration configuration = mock(S3Configuration.class);
|
||||
AttachmentsConfiguration configuration = mock(AttachmentsConfiguration.class);
|
||||
when(configuration.getAccessKey()).thenReturn("foo");
|
||||
when(configuration.getAccessSecret()).thenReturn("bar");
|
||||
when(configuration.getAttachmentsBucket()).thenReturn("attachments-test");
|
||||
when(configuration.getBucket()).thenReturn("attachments-test");
|
||||
|
||||
UrlSigner signer = new UrlSigner(configuration);
|
||||
URL url = signer.getPreSignedUrl(1234, HttpMethod.GET);
|
||||
|
|
Loading…
Reference in New Issue