Retire `AttachmentControllerV2`
This commit is contained in:
parent
5abfef50fc
commit
bda4788a34
|
@ -21,9 +21,6 @@ svr3.userIdTokenSharedSecret: dbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # bas
|
||||||
|
|
||||||
tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG=
|
tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG=
|
||||||
|
|
||||||
awsAttachments.accessKey: test
|
|
||||||
awsAttachments.accessSecret: test
|
|
||||||
|
|
||||||
gcpAttachments.rsaSigningKey: |
|
gcpAttachments.rsaSigningKey: |
|
||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||||
|
|
|
@ -239,13 +239,6 @@ messageCache: # Redis server configuration for message store cache
|
||||||
cluster:
|
cluster:
|
||||||
configurationUri: redis://redis.example.com:6379/
|
configurationUri: redis://redis.example.com:6379/
|
||||||
|
|
||||||
awsAttachments: # AWS S3 configuration
|
|
||||||
bucket: aws-attachments
|
|
||||||
credentials:
|
|
||||||
accessKeyId: secret://awsAttachments.accessKey
|
|
||||||
secretAccessKey: secret://awsAttachments.accessSecret
|
|
||||||
region: us-west-2
|
|
||||||
|
|
||||||
gcpAttachments: # GCP Storage configuration
|
gcpAttachments: # GCP Storage configuration
|
||||||
domain: example.com
|
domain: example.com
|
||||||
email: user@example.cocm
|
email: user@example.cocm
|
||||||
|
|
|
@ -17,7 +17,6 @@ import org.whispersystems.textsecuregcm.attachments.TusConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.AppleAppStoreConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.AppleAppStoreConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.AwsCredentialsProviderFactory;
|
import org.whispersystems.textsecuregcm.configuration.AwsCredentialsProviderFactory;
|
||||||
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.BraintreeConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.BraintreeConfiguration;
|
||||||
|
@ -109,11 +108,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private DynamoDbTables dynamoDbTables;
|
private DynamoDbTables dynamoDbTables;
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Valid
|
|
||||||
@JsonProperty
|
|
||||||
private AwsAttachmentsConfiguration awsAttachments;
|
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Valid
|
@Valid
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
|
@ -397,10 +391,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
return webSocket;
|
return webSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AwsAttachmentsConfiguration getAwsAttachmentsConfiguration() {
|
|
||||||
return awsAttachments;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GcpAttachmentsConfiguration getGcpAttachmentsConfiguration() {
|
public GcpAttachmentsConfiguration getGcpAttachmentsConfiguration() {
|
||||||
return gcpAttachments;
|
return gcpAttachments;
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,7 +110,6 @@ import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.AccountControllerV2;
|
import org.whispersystems.textsecuregcm.controllers.AccountControllerV2;
|
||||||
import org.whispersystems.textsecuregcm.controllers.ArchiveController;
|
import org.whispersystems.textsecuregcm.controllers.ArchiveController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.ArtController;
|
import org.whispersystems.textsecuregcm.controllers.ArtController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2;
|
|
||||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV4;
|
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV4;
|
||||||
import org.whispersystems.textsecuregcm.controllers.CallLinkController;
|
import org.whispersystems.textsecuregcm.controllers.CallLinkController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.CallRoutingController;
|
import org.whispersystems.textsecuregcm.controllers.CallRoutingController;
|
||||||
|
@ -1096,10 +1095,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
new AccountControllerV2(accountsManager, changeNumberManager, phoneVerificationTokenManager,
|
new AccountControllerV2(accountsManager, changeNumberManager, phoneVerificationTokenManager,
|
||||||
registrationLockVerificationManager, rateLimiters),
|
registrationLockVerificationManager, rateLimiters),
|
||||||
new ArtController(rateLimiters, artCredentialsGenerator),
|
new ArtController(rateLimiters, artCredentialsGenerator),
|
||||||
new AttachmentControllerV2(rateLimiters,
|
|
||||||
config.getAwsAttachmentsConfiguration().credentials().accessKeyId().value(),
|
|
||||||
config.getAwsAttachmentsConfiguration().credentials().secretAccessKey().value(),
|
|
||||||
config.getAwsAttachmentsConfiguration().region(), config.getAwsAttachmentsConfiguration().bucket()),
|
|
||||||
new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, tusAttachmentGenerator,
|
new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, tusAttachmentGenerator,
|
||||||
experimentEnrollmentManager),
|
experimentEnrollmentManager),
|
||||||
new ArchiveController(backupAuthManager, backupManager),
|
new ArchiveController(backupAuthManager, backupManager),
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2013 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
package org.whispersystems.textsecuregcm.configuration;
|
|
||||||
|
|
||||||
import javax.validation.Valid;
|
|
||||||
import javax.validation.constraints.NotBlank;
|
|
||||||
import javax.validation.constraints.NotNull;
|
|
||||||
|
|
||||||
public record AwsAttachmentsConfiguration(@NotNull @Valid StaticAwsCredentialsFactory credentials,
|
|
||||||
@NotBlank String bucket,
|
|
||||||
@NotBlank String region) {
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2013 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.controllers;
|
|
||||||
|
|
||||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
|
||||||
|
|
||||||
import com.google.common.net.HttpHeaders;
|
|
||||||
import io.dropwizard.auth.Auth;
|
|
||||||
import io.micrometer.core.instrument.Metrics;
|
|
||||||
import io.micrometer.core.instrument.Tags;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.time.ZoneOffset;
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import javax.ws.rs.GET;
|
|
||||||
import javax.ws.rs.HeaderParam;
|
|
||||||
import javax.ws.rs.Path;
|
|
||||||
import javax.ws.rs.Produces;
|
|
||||||
import javax.ws.rs.core.MediaType;
|
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV2;
|
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
|
||||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
|
||||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
|
||||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Conversions;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Pair;
|
|
||||||
import org.whispersystems.websocket.auth.ReadOnly;
|
|
||||||
|
|
||||||
// To be removed when Android 7.12 reaches saturation, likely some time toward the end of October 2024
|
|
||||||
@Deprecated(forRemoval = true)
|
|
||||||
@Path("/v2/attachments")
|
|
||||||
@Tag(name = "Attachments")
|
|
||||||
public class AttachmentControllerV2 {
|
|
||||||
|
|
||||||
private final PostPolicyGenerator policyGenerator;
|
|
||||||
private final PolicySigner policySigner;
|
|
||||||
private final RateLimiter rateLimiter;
|
|
||||||
|
|
||||||
private static final String CREATE_UPLOAD_COUNTER_NAME = name(AttachmentControllerV2.class, "uploadForm");
|
|
||||||
|
|
||||||
public AttachmentControllerV2(RateLimiters rateLimiters, String accessKey, String accessSecret, String region,
|
|
||||||
String bucket) {
|
|
||||||
this.rateLimiter = rateLimiters.getAttachmentLimiter();
|
|
||||||
this.policyGenerator = new PostPolicyGenerator(region, bucket, accessKey);
|
|
||||||
this.policySigner = new PolicySigner(accessSecret, region);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
@Path("/form/upload")
|
|
||||||
public AttachmentDescriptorV2 getAttachmentUploadForm(
|
|
||||||
@ReadOnly @Auth AuthenticatedDevice auth,
|
|
||||||
@HeaderParam(HttpHeaders.USER_AGENT) String userAgent)
|
|
||||||
throws RateLimitExceededException {
|
|
||||||
rateLimiter.validate(auth.getAccount().getUuid());
|
|
||||||
|
|
||||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
|
||||||
long attachmentId = generateAttachmentId();
|
|
||||||
String objectName = String.valueOf(attachmentId);
|
|
||||||
Pair<String, String> policy = policyGenerator.createFor(now, objectName, 100 * 1024 * 1024);
|
|
||||||
String signature = policySigner.getSignature(now, policy.second());
|
|
||||||
|
|
||||||
Metrics.counter(CREATE_UPLOAD_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment();
|
|
||||||
|
|
||||||
return new AttachmentDescriptorV2(attachmentId, objectName, policy.first(),
|
|
||||||
"private", "AWS4-HMAC-SHA256",
|
|
||||||
now.format(PostPolicyGenerator.AWS_DATE_TIME),
|
|
||||||
policy.second(), signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
private long generateAttachmentId() {
|
|
||||||
byte[] attachmentBytes = new byte[8];
|
|
||||||
new SecureRandom().nextBytes(attachmentBytes);
|
|
||||||
|
|
||||||
attachmentBytes[0] = (byte) (attachmentBytes[0] & 0x7F);
|
|
||||||
return Conversions.byteArrayToLong(attachmentBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -30,7 +30,8 @@ import org.whispersystems.websocket.auth.ReadOnly;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The V4 API is identical to the {@link AttachmentControllerV3} API, but supports an additional TUS based cdn type (cdn3)
|
* The attachment controller generates "upload forms" for authenticated users that permit them to upload files
|
||||||
|
* (message attachments) to a remote storage location. The location may be selected by the server at runtime.
|
||||||
*/
|
*/
|
||||||
@Path("/v4/attachments")
|
@Path("/v4/attachments")
|
||||||
@Tag(name = "Attachments")
|
@Tag(name = "Attachments")
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2013-2020 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.entities;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
|
|
||||||
public record AttachmentDescriptorV2(long attachmentId,
|
|
||||||
String key,
|
|
||||||
String credential,
|
|
||||||
String acl,
|
|
||||||
String algorithm,
|
|
||||||
String date,
|
|
||||||
String policy,
|
|
||||||
String signature) {
|
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
public String attachmentIdString() {
|
|
||||||
return String.valueOf(attachmentId);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -36,7 +36,6 @@ import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
|
||||||
import org.whispersystems.textsecuregcm.attachments.TusConfiguration;
|
import org.whispersystems.textsecuregcm.attachments.TusConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
||||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV2;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV3;
|
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV3;
|
||||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
|
@ -47,7 +46,7 @@ import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||||
|
|
||||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||||
class AttachmentControllerTest {
|
class AttachmentControllerV4Test {
|
||||||
|
|
||||||
private static final RateLimiter RATE_LIMITER = mock(RateLimiter.class);
|
private static final RateLimiter RATE_LIMITER = mock(RateLimiter.class);
|
||||||
|
|
||||||
|
@ -93,8 +92,6 @@ class AttachmentControllerTest {
|
||||||
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedDevice.class))
|
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedDevice.class))
|
||||||
.setMapper(SystemMapper.jsonMapper())
|
.setMapper(SystemMapper.jsonMapper())
|
||||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||||
.addResource(new AttachmentControllerV2(RATE_LIMITERS, "accessKey", "accessSecret", "us-east-1",
|
|
||||||
"attachmentv2-bucket"))
|
|
||||||
.addProvider(new AttachmentControllerV4(RATE_LIMITERS,
|
.addProvider(new AttachmentControllerV4(RATE_LIMITERS,
|
||||||
gcsAttachmentGenerator,
|
gcsAttachmentGenerator,
|
||||||
new TusAttachmentGenerator(new TusConfiguration(new SecretBytes(TUS_SECRET), TUS_URL)),
|
new TusAttachmentGenerator(new TusConfiguration(new SecretBytes(TUS_SECRET), TUS_URL)),
|
||||||
|
@ -179,33 +176,4 @@ class AttachmentControllerTest {
|
||||||
assertThat(credentialParts[3]).isEqualTo("storage");
|
assertThat(credentialParts[3]).isEqualTo("storage");
|
||||||
assertThat(credentialParts[4]).isEqualTo("goog4_request");
|
assertThat(credentialParts[4]).isEqualTo("goog4_request");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void testV2Form() throws IOException {
|
|
||||||
AttachmentDescriptorV2 descriptor = resources.getJerseyTest()
|
|
||||||
.target("/v2/attachments/form/upload")
|
|
||||||
.request()
|
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
|
||||||
.get(AttachmentDescriptorV2.class);
|
|
||||||
|
|
||||||
assertThat(descriptor.key()).isEqualTo(descriptor.attachmentIdString());
|
|
||||||
assertThat(descriptor.acl()).isEqualTo("private");
|
|
||||||
assertThat(descriptor.algorithm()).isEqualTo("AWS4-HMAC-SHA256");
|
|
||||||
assertThat(descriptor.attachmentId()).isGreaterThan(0);
|
|
||||||
assertThat(String.valueOf(descriptor.attachmentId())).isEqualTo(descriptor.attachmentIdString());
|
|
||||||
|
|
||||||
String[] credentialParts = descriptor.credential().split("/");
|
|
||||||
|
|
||||||
assertThat(credentialParts[0]).isEqualTo("accessKey");
|
|
||||||
assertThat(credentialParts[2]).isEqualTo("us-east-1");
|
|
||||||
assertThat(credentialParts[3]).isEqualTo("s3");
|
|
||||||
assertThat(credentialParts[4]).isEqualTo("aws4_request");
|
|
||||||
|
|
||||||
assertThat(descriptor.date()).isNotBlank();
|
|
||||||
assertThat(descriptor.policy()).isNotBlank();
|
|
||||||
assertThat(descriptor.signature()).isNotBlank();
|
|
||||||
|
|
||||||
assertThat(new String(Base64.getDecoder().decode(descriptor.policy()))).contains("[\"content-length-range\", 1, 104857600]");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -58,9 +58,6 @@ svr3.userIdTokenSharedSecret: dbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # bas
|
||||||
|
|
||||||
tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG=
|
tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG=
|
||||||
|
|
||||||
awsAttachments.accessKey: test
|
|
||||||
awsAttachments.accessSecret: test
|
|
||||||
|
|
||||||
# The below private key was key generated exclusively for testing purposes. Do not use it in any other context.
|
# The below private key was key generated exclusively for testing purposes. Do not use it in any other context.
|
||||||
gcpAttachments.rsaSigningKey: |
|
gcpAttachments.rsaSigningKey: |
|
||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
|
|
@ -235,13 +235,6 @@ messageCache: # Redis server configuration for message store cache
|
||||||
cluster:
|
cluster:
|
||||||
type: local
|
type: local
|
||||||
|
|
||||||
awsAttachments: # AWS S3 configuration
|
|
||||||
bucket: aws-attachments
|
|
||||||
credentials:
|
|
||||||
accessKeyId: secret://awsAttachments.accessKey
|
|
||||||
secretAccessKey: secret://awsAttachments.accessSecret
|
|
||||||
region: us-west-2
|
|
||||||
|
|
||||||
gcpAttachments: # GCP Storage configuration
|
gcpAttachments: # GCP Storage configuration
|
||||||
domain: example.com
|
domain: example.com
|
||||||
email: user@example.cocm
|
email: user@example.cocm
|
||||||
|
|
Loading…
Reference in New Issue