parent
8ea805e4e3
commit
322548f078
4
pom.xml
4
pom.xml
|
@ -181,8 +181,8 @@
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.2</version>
|
<version>3.2</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<source>1.7</source>
|
<source>1.8</source>
|
||||||
<target>1.7</target>
|
<target>1.8</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
|
|
|
@ -21,11 +21,12 @@ import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.ProfilesConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
|
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.TestDeviceConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||||
|
@ -57,7 +58,12 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
@NotNull
|
@NotNull
|
||||||
@Valid
|
@Valid
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private S3Configuration s3;
|
private AttachmentsConfiguration attachments;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private ProfilesConfiguration profiles;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Valid
|
@Valid
|
||||||
|
@ -145,8 +151,8 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
return httpClient;
|
return httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public S3Configuration getS3Configuration() {
|
public AttachmentsConfiguration getAttachmentsConfiguration() {
|
||||||
return s3;
|
return attachments;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RedisConfiguration getCacheConfiguration() {
|
public RedisConfiguration getCacheConfiguration() {
|
||||||
|
@ -193,6 +199,10 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
return apn;
|
return apn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ProfilesConfiguration getProfilesConfiguration() {
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, Integer> getTestDevices() {
|
public Map<String, Integer> getTestDevices() {
|
||||||
Map<String, Integer> results = new HashMap<>();
|
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.PushSender;
|
||||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
import org.whispersystems.textsecuregcm.push.WebsocketSender;
|
import org.whispersystems.textsecuregcm.push.WebsocketSender;
|
||||||
|
import org.whispersystems.textsecuregcm.s3.UrlSigner;
|
||||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||||
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
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.PendingDevicesManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||||
import org.whispersystems.textsecuregcm.util.Constants;
|
import org.whispersystems.textsecuregcm.util.Constants;
|
||||||
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
|
||||||
import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
|
import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
|
||||||
import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler;
|
import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler;
|
||||||
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
|
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
|
||||||
|
@ -183,7 +183,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(apnSender, pubSubManager);
|
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(apnSender, pubSubManager);
|
||||||
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
|
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
|
||||||
SmsSender smsSender = new SmsSender(twilioSmsSender);
|
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());
|
PushSender pushSender = new PushSender(apnFallbackManager, gcmSender, apnSender, websocketSender, config.getPushConfiguration().getQueueSize());
|
||||||
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
|
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
|
||||||
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration());
|
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration());
|
||||||
|
@ -197,7 +197,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
|
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
|
||||||
KeysController keysController = new KeysController(rateLimiters, keys, accountsManager, federatedClientManager);
|
KeysController keysController = new KeysController(rateLimiters, keys, accountsManager, federatedClientManager);
|
||||||
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, 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>()
|
environment.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<Account>()
|
||||||
.setAuthenticator(deviceAuthenticator)
|
.setAuthenticator(deviceAuthenticator)
|
||||||
|
|
|
@ -19,7 +19,7 @@ package org.whispersystems.textsecuregcm.configuration;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import org.hibernate.validator.constraints.NotEmpty;
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
public class S3Configuration {
|
public class AttachmentsConfiguration {
|
||||||
|
|
||||||
@NotEmpty
|
@NotEmpty
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
|
@ -31,7 +31,7 @@ public class S3Configuration {
|
||||||
|
|
||||||
@NotEmpty
|
@NotEmpty
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private String attachmentsBucket;
|
private String bucket;
|
||||||
|
|
||||||
public String getAccessKey() {
|
public String getAccessKey() {
|
||||||
return accessKey;
|
return accessKey;
|
||||||
|
@ -41,7 +41,7 @@ public class S3Configuration {
|
||||||
return accessSecret;
|
return accessSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAttachmentsBucket() {
|
public String getBucket() {
|
||||||
return attachmentsBucket;
|
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.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.util.Conversions;
|
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.GET;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
|
@ -40,7 +40,6 @@ import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
import io.dropwizard.auth.Auth;
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
|
@ -1,19 +1,38 @@
|
||||||
package org.whispersystems.textsecuregcm.controllers;
|
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.codahale.metrics.annotation.Timed;
|
||||||
import com.google.common.base.Optional;
|
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.Profile;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
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.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Pair;
|
||||||
|
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
import io.dropwizard.auth.Auth;
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
|
@ -23,16 +42,42 @@ public class ProfileController {
|
||||||
private final RateLimiters rateLimiters;
|
private final RateLimiters rateLimiters;
|
||||||
private final AccountsManager accountsManager;
|
private final AccountsManager accountsManager;
|
||||||
|
|
||||||
public ProfileController(RateLimiters rateLimiters, 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.rateLimiters = rateLimiters;
|
||||||
this.accountsManager = accountsManager;
|
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
|
@Timed
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/{number}")
|
@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
|
throws RateLimitExceededException
|
||||||
{
|
{
|
||||||
rateLimiters.getProfileLimiter().validate(account.getNumber());
|
rateLimiters.getProfileLimiter().validate(account.getNumber());
|
||||||
|
@ -43,8 +88,47 @@ public class ProfileController {
|
||||||
throw new WebApplicationException(Response.status(404).build());
|
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.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
import org.hibernate.validator.constraints.Length;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Max;
|
||||||
|
|
||||||
public class Profile {
|
public class Profile {
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private String identityKey;
|
private String identityKey;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
public Profile() {}
|
public Profile() {}
|
||||||
|
|
||||||
public Profile(String identityKey) {
|
public Profile(String name, String avatar, String identityKey) {
|
||||||
|
this.name = name;
|
||||||
|
this.avatar = avatar;
|
||||||
this.identityKey = identityKey;
|
this.identityKey = identityKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,4 +30,15 @@ public class Profile {
|
||||||
public String getIdentityKey() {
|
public String getIdentityKey() {
|
||||||
return identityKey;
|
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
|
* 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/>.
|
* 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.HttpMethod;
|
||||||
import com.amazonaws.auth.AWSCredentials;
|
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.AmazonS3Client;
|
||||||
import com.amazonaws.services.s3.S3ClientOptions;
|
import com.amazonaws.services.s3.S3ClientOptions;
|
||||||
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
|
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.net.URL;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
@ -35,9 +35,9 @@ public class UrlSigner {
|
||||||
private final AWSCredentials credentials;
|
private final AWSCredentials credentials;
|
||||||
private final String bucket;
|
private final String bucket;
|
||||||
|
|
||||||
public UrlSigner(S3Configuration config) {
|
public UrlSigner(AttachmentsConfiguration config) {
|
||||||
this.credentials = new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret());
|
this.credentials = new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret());
|
||||||
this.bucket = config.getAttachmentsBucket();
|
this.bucket = config.getBucket();
|
||||||
}
|
}
|
||||||
|
|
||||||
public URL getPreSignedUrl(long attachmentId, HttpMethod method) {
|
public URL getPreSignedUrl(long attachmentId, HttpMethod method) {
|
|
@ -39,6 +39,15 @@ public class Account {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private String identityKey;
|
private String identityKey;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String avatarDigest;
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private Device authenticatedDevice;
|
private Device authenticatedDevice;
|
||||||
|
|
||||||
|
@ -171,4 +180,28 @@ public class Account {
|
||||||
|
|
||||||
return lastSeen;
|
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;
|
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||||
|
|
||||||
import com.google.common.base.Optional;
|
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.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.ProfileController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||||
import org.whispersystems.textsecuregcm.entities.Profile;
|
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.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
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 io.dropwizard.testing.junit.ResourceTestRule;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.assertj.core.api.Java6Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
public class ProfileControllerTest {
|
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
|
@Test
|
||||||
public void testProfileGet() throws RateLimitExceededException {
|
public void testProfileGet() throws RateLimitExceededException {
|
||||||
Account requestAccount = mock(Account.class );
|
Profile profile= resources.getJerseyTest()
|
||||||
Account profileAccount = mock(Account.class );
|
.target("/v1/profile/" + AuthHelper.VALID_NUMBER_TWO)
|
||||||
RateLimiter rateLimiter = mock(RateLimiter.class );
|
.request()
|
||||||
RateLimiters rateLimiters = mock(RateLimiters.class );
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
AccountsManager accountsManager = mock(AccountsManager.class);
|
.get(Profile.class);
|
||||||
|
|
||||||
when(rateLimiters.getProfileLimiter()).thenReturn(rateLimiter);
|
assertThat(profile.getIdentityKey()).isEqualTo("bar");
|
||||||
when(requestAccount.getNumber()).thenReturn("foo");
|
assertThat(profile.getName()).isEqualTo("baz");
|
||||||
when(profileAccount.getIdentityKey()).thenReturn("bar");
|
assertThat(profile.getAvatar()).isEqualTo("profiles/bang");
|
||||||
when(accountsManager.get(eq("baz"))).thenReturn(Optional.of(profileAccount));
|
|
||||||
|
|
||||||
ProfileController profileController = new ProfileController(rateLimiters, accountsManager);
|
verify(accountsManager, times(1)).get(AuthHelper.VALID_NUMBER_TWO);
|
||||||
Profile result = profileController.getProfile(requestAccount, "baz");
|
|
||||||
|
|
||||||
assertEquals(result.getIdentityKey(), "bar");
|
|
||||||
|
|
||||||
verify(accountsManager, times(1)).get(eq("baz"));
|
|
||||||
verify(rateLimiters, times(1)).getProfileLimiter();
|
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 com.amazonaws.HttpMethod;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
|
import org.whispersystems.textsecuregcm.configuration.AttachmentsConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
import org.whispersystems.textsecuregcm.s3.UrlSigner;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
|
@ -15,10 +15,10 @@ public class UrlSignerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransferAcceleration() {
|
public void testTransferAcceleration() {
|
||||||
S3Configuration configuration = mock(S3Configuration.class);
|
AttachmentsConfiguration configuration = mock(AttachmentsConfiguration.class);
|
||||||
when(configuration.getAccessKey()).thenReturn("foo");
|
when(configuration.getAccessKey()).thenReturn("foo");
|
||||||
when(configuration.getAccessSecret()).thenReturn("bar");
|
when(configuration.getAccessSecret()).thenReturn("bar");
|
||||||
when(configuration.getAttachmentsBucket()).thenReturn("attachments-test");
|
when(configuration.getBucket()).thenReturn("attachments-test");
|
||||||
|
|
||||||
UrlSigner signer = new UrlSigner(configuration);
|
UrlSigner signer = new UrlSigner(configuration);
|
||||||
URL url = signer.getPreSignedUrl(1234, HttpMethod.GET);
|
URL url = signer.getPreSignedUrl(1234, HttpMethod.GET);
|
||||||
|
|
Loading…
Reference in New Issue