Add v4 attachment controller
Add AttachmentControllerV4 which can be configured to generate upload forms for a TUS based CDN
This commit is contained in:
parent
9df923d916
commit
705fb93e45
|
@ -11,6 +11,8 @@ directoryV2.client.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789
|
|||
svr2.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth tokens for Signal users
|
||||
svr2.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth identity tokens for Signal users
|
||||
|
||||
tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG=
|
||||
|
||||
awsAttachments.accessKey: test
|
||||
awsAttachments.accessSecret: test
|
||||
|
||||
|
|
|
@ -202,6 +202,10 @@ gcpAttachments: # GCP Storage configuration
|
|||
pathPrefix:
|
||||
rsaSigningKey: secret://gcpAttachments.rsaSigningKey
|
||||
|
||||
tus:
|
||||
uploadUri: https://example.org/upload
|
||||
userAuthenticationTokenSharedSecret: secret://tus.userAuthenticationTokenSharedSecret
|
||||
|
||||
accountDatabaseCrawler:
|
||||
chunkSize: 10 # accounts per run
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import java.util.Map;
|
|||
import java.util.Set;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.attachments.TusConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AccountDatabaseCrawlerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AdminEventLoggingConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||
|
@ -270,6 +271,11 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
@JsonProperty
|
||||
private TurnSecretConfiguration turn;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private TusConfiguration tus;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
|
@ -454,6 +460,10 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
return turn;
|
||||
}
|
||||
|
||||
public TusConfiguration getTus() {
|
||||
return tus;
|
||||
}
|
||||
|
||||
public int getGrpcPort() {
|
||||
return grpcPort;
|
||||
}
|
||||
|
|
|
@ -63,6 +63,8 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
|
|||
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.attachments.GcsAttachmentGenerator;
|
||||
import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.BaseAccountAuthenticator;
|
||||
|
@ -89,6 +91,7 @@ import org.whispersystems.textsecuregcm.controllers.AccountControllerV2;
|
|||
import org.whispersystems.textsecuregcm.controllers.ArtController;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV3;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV4;
|
||||
import org.whispersystems.textsecuregcm.controllers.CallLinkController;
|
||||
import org.whispersystems.textsecuregcm.controllers.CertificateController;
|
||||
import org.whispersystems.textsecuregcm.controllers.ChallengeController;
|
||||
|
@ -596,6 +599,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
.credentialsProvider(cdnCredentialsProvider)
|
||||
.region(Region.of(config.getCdnConfiguration().region()))
|
||||
.build();
|
||||
|
||||
final GcsAttachmentGenerator gcsAttachmentGenerator = new GcsAttachmentGenerator(
|
||||
config.getGcpAttachmentsConfiguration().domain(),
|
||||
config.getGcpAttachmentsConfiguration().email(),
|
||||
config.getGcpAttachmentsConfiguration().maxSizeInBytes(),
|
||||
config.getGcpAttachmentsConfiguration().pathPrefix(),
|
||||
config.getGcpAttachmentsConfiguration().rsaSigningKey().value());
|
||||
|
||||
PostPolicyGenerator profileCdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().region(),
|
||||
config.getCdnConfiguration().bucket(), config.getCdnConfiguration().accessKey().value());
|
||||
PolicySigner profileCdnPolicySigner = new PolicySigner(config.getCdnConfiguration().accessSecret().value(),
|
||||
|
@ -718,7 +729,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
registrationLockVerificationManager, rateLimiters),
|
||||
new ArtController(rateLimiters, artCredentialsGenerator),
|
||||
new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().accessKey().value(), config.getAwsAttachmentsConfiguration().accessSecret().value(), config.getAwsAttachmentsConfiguration().region(), config.getAwsAttachmentsConfiguration().bucket()),
|
||||
new AttachmentControllerV3(rateLimiters, config.getGcpAttachmentsConfiguration().domain(), config.getGcpAttachmentsConfiguration().email(), config.getGcpAttachmentsConfiguration().maxSizeInBytes(), config.getGcpAttachmentsConfiguration().pathPrefix(), config.getGcpAttachmentsConfiguration().rsaSigningKey().value()),
|
||||
new AttachmentControllerV3(rateLimiters, gcsAttachmentGenerator),
|
||||
new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, new TusAttachmentGenerator(config.getTus()), experimentEnrollmentManager),
|
||||
new CallLinkController(rateLimiters, genericZkSecretParams),
|
||||
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(), config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()), zkAuthOperations, genericZkSecretParams, clock),
|
||||
new ChallengeController(rateLimitChallengeManager),
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.attachments;
|
||||
import java.util.Map;
|
||||
|
||||
public interface AttachmentGenerator {
|
||||
|
||||
record Descriptor(Map<String, String> headers, String signedUploadLocation) {}
|
||||
|
||||
Descriptor generateAttachment(final String key);
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.attachments;
|
||||
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequest;
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestGenerator;
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestSigner;
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
public class GcsAttachmentGenerator implements AttachmentGenerator {
|
||||
@Nonnull
|
||||
private final CanonicalRequestGenerator canonicalRequestGenerator;
|
||||
|
||||
@Nonnull
|
||||
private final CanonicalRequestSigner canonicalRequestSigner;
|
||||
|
||||
public GcsAttachmentGenerator(@Nonnull String domain, @Nonnull String email,
|
||||
int maxSizeInBytes, @Nonnull String pathPrefix, @Nonnull String rsaSigningKey)
|
||||
throws IOException, InvalidKeyException, InvalidKeySpecException {
|
||||
this.canonicalRequestGenerator = new CanonicalRequestGenerator(domain, email, maxSizeInBytes, pathPrefix);
|
||||
this.canonicalRequestSigner = new CanonicalRequestSigner(rsaSigningKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Descriptor generateAttachment(final String key) {
|
||||
final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
final CanonicalRequest canonicalRequest = canonicalRequestGenerator.createFor(key, now);
|
||||
return new Descriptor(getHeaderMap(canonicalRequest), getSignedUploadLocation(canonicalRequest));
|
||||
}
|
||||
|
||||
private String getSignedUploadLocation(@Nonnull CanonicalRequest canonicalRequest) {
|
||||
return "https://" + canonicalRequest.getDomain() + canonicalRequest.getResourcePath()
|
||||
+ '?' + canonicalRequest.getCanonicalQuery()
|
||||
+ "&X-Goog-Signature=" + canonicalRequestSigner.sign(canonicalRequest);
|
||||
}
|
||||
|
||||
private static Map<String, String> getHeaderMap(@Nonnull CanonicalRequest canonicalRequest) {
|
||||
return Map.of(
|
||||
"host", canonicalRequest.getDomain(),
|
||||
"x-goog-content-length-range", "1," + canonicalRequest.getMaxSizeInBytes(),
|
||||
"x-goog-resumable", "start");
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.attachments;
|
||||
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.util.HeaderUtils;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
public class TusAttachmentGenerator implements AttachmentGenerator {
|
||||
|
||||
private static final String ATTACHMENTS = "attachments";
|
||||
|
||||
final ExternalServiceCredentialsGenerator credentialsGenerator;
|
||||
final String tusUri;
|
||||
|
||||
public TusAttachmentGenerator(final TusConfiguration cfg) {
|
||||
this.tusUri = cfg.uploadUri();
|
||||
this.credentialsGenerator = credentialsGenerator(Clock.systemUTC(), cfg);
|
||||
}
|
||||
|
||||
private static ExternalServiceCredentialsGenerator credentialsGenerator(final Clock clock, final TusConfiguration cfg) {
|
||||
return ExternalServiceCredentialsGenerator
|
||||
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||
.prependUsername(false)
|
||||
.withClock(clock)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Descriptor generateAttachment(final String key) {
|
||||
final ExternalServiceCredentials credentials = credentialsGenerator.generateFor(ATTACHMENTS + "/" + key);
|
||||
final String b64Key = Base64.getEncoder().encodeToString(key.getBytes(StandardCharsets.UTF_8));
|
||||
final Map<String, String> headers = Map.of(
|
||||
HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(credentials),
|
||||
"Upload-Metadata", String.format("filename %s", b64Key)
|
||||
);
|
||||
return new Descriptor(headers, tusUri + "/" + ATTACHMENTS);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.attachments;
|
||||
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public record TusConfiguration(
|
||||
@ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret,
|
||||
@NotEmpty String uploadUri
|
||||
){}
|
|
@ -21,6 +21,8 @@ import javax.ws.rs.GET;
|
|||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import org.whispersystems.textsecuregcm.attachments.AttachmentGenerator;
|
||||
import org.whispersystems.textsecuregcm.attachments.GcsAttachmentGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV3;
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequest;
|
||||
|
@ -37,20 +39,15 @@ public class AttachmentControllerV3 {
|
|||
private final RateLimiter rateLimiter;
|
||||
|
||||
@Nonnull
|
||||
private final CanonicalRequestGenerator canonicalRequestGenerator;
|
||||
|
||||
@Nonnull
|
||||
private final CanonicalRequestSigner canonicalRequestSigner;
|
||||
private final GcsAttachmentGenerator gcsAttachmentGenerator;
|
||||
|
||||
@Nonnull
|
||||
private final SecureRandom secureRandom;
|
||||
|
||||
public AttachmentControllerV3(@Nonnull RateLimiters rateLimiters, @Nonnull String domain, @Nonnull String email,
|
||||
int maxSizeInBytes, @Nonnull String pathPrefix, @Nonnull String rsaSigningKey)
|
||||
public AttachmentControllerV3(@Nonnull RateLimiters rateLimiters, @Nonnull GcsAttachmentGenerator gcsAttachmentGenerator)
|
||||
throws IOException, InvalidKeyException, InvalidKeySpecException {
|
||||
this.rateLimiter = rateLimiters.getAttachmentLimiter();
|
||||
this.canonicalRequestGenerator = new CanonicalRequestGenerator(domain, email, maxSizeInBytes, pathPrefix);
|
||||
this.canonicalRequestSigner = new CanonicalRequestSigner(rsaSigningKey);
|
||||
this.gcsAttachmentGenerator = gcsAttachmentGenerator;
|
||||
this.secureRandom = new SecureRandom();
|
||||
}
|
||||
|
||||
|
@ -61,26 +58,9 @@ public class AttachmentControllerV3 {
|
|||
public AttachmentDescriptorV3 getAttachmentUploadForm(@Auth AuthenticatedAccount auth)
|
||||
throws RateLimitExceededException {
|
||||
rateLimiter.validate(auth.getAccount().getUuid());
|
||||
|
||||
final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
final String key = generateAttachmentKey();
|
||||
final CanonicalRequest canonicalRequest = canonicalRequestGenerator.createFor(key, now);
|
||||
|
||||
return new AttachmentDescriptorV3(2, key, getHeaderMap(canonicalRequest),
|
||||
getSignedUploadLocation(canonicalRequest));
|
||||
}
|
||||
|
||||
private String getSignedUploadLocation(@Nonnull CanonicalRequest canonicalRequest) {
|
||||
return "https://" + canonicalRequest.getDomain() + canonicalRequest.getResourcePath()
|
||||
+ '?' + canonicalRequest.getCanonicalQuery()
|
||||
+ "&X-Goog-Signature=" + canonicalRequestSigner.sign(canonicalRequest);
|
||||
}
|
||||
|
||||
private static Map<String, String> getHeaderMap(@Nonnull CanonicalRequest canonicalRequest) {
|
||||
return Map.of(
|
||||
"host", canonicalRequest.getDomain(),
|
||||
"x-goog-content-length-range", "1," + canonicalRequest.getMaxSizeInBytes(),
|
||||
"x-goog-resumable", "start");
|
||||
final AttachmentGenerator.Descriptor descriptor = this.gcsAttachmentGenerator.generateAttachment(key);
|
||||
return new AttachmentDescriptorV3(2, key, descriptor.headers(), descriptor.signedUploadLocation());
|
||||
}
|
||||
|
||||
private String generateAttachmentKey() {
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.headers.Header;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.whispersystems.textsecuregcm.attachments.AttachmentGenerator;
|
||||
import org.whispersystems.textsecuregcm.attachments.GcsAttachmentGenerator;
|
||||
import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV3;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
|
||||
|
||||
/**
|
||||
* The V4 API is identical to the {@link AttachmentControllerV3} API, but supports an additional TUS based cdn type (cdn3)
|
||||
*/
|
||||
@Path("/v4/attachments")
|
||||
@Tag(name = "Attachments")
|
||||
public class AttachmentControllerV4 {
|
||||
|
||||
public static final String CDN3_EXPERIMENT_NAME = "cdn3";
|
||||
|
||||
private final ExperimentEnrollmentManager experimentEnrollmentManager;
|
||||
private final RateLimiter rateLimiter;
|
||||
|
||||
private final Map<Integer, AttachmentGenerator> attachmentGenerators;
|
||||
|
||||
@Nonnull
|
||||
private final SecureRandom secureRandom;
|
||||
|
||||
public AttachmentControllerV4(
|
||||
final RateLimiters rateLimiters,
|
||||
final GcsAttachmentGenerator gcsAttachmentGenerator,
|
||||
final TusAttachmentGenerator tusAttachmentGenerator,
|
||||
final ExperimentEnrollmentManager experimentEnrollmentManager) {
|
||||
this.rateLimiter = rateLimiters.getAttachmentLimiter();
|
||||
this.experimentEnrollmentManager = experimentEnrollmentManager;
|
||||
this.secureRandom = new SecureRandom();
|
||||
this.attachmentGenerators = Map.of(
|
||||
2, gcsAttachmentGenerator,
|
||||
3, tusAttachmentGenerator
|
||||
);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/form/upload")
|
||||
@Operation(
|
||||
summary = "Get an upload form",
|
||||
description = """
|
||||
Retrieve an upload form that can be used to perform a resumable upload. The response will include a cdn number
|
||||
indicating what protocol should be used to perform the upload.
|
||||
"""
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "Success, response body includes upload form", useReturnTypeSchema = true)
|
||||
@ApiResponse(responseCode = "413", description = "Too many attempts", headers = @Header(
|
||||
name = "Retry-After",
|
||||
description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed"))
|
||||
@ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header(
|
||||
name = "Retry-After",
|
||||
description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed"))
|
||||
public AttachmentDescriptorV3 getAttachmentUploadForm(@Auth AuthenticatedAccount auth)
|
||||
throws RateLimitExceededException {
|
||||
rateLimiter.validate(auth.getAccount().getUuid());
|
||||
final String key = generateAttachmentKey();
|
||||
final boolean useCdn3 = this.experimentEnrollmentManager.isEnrolled(auth.getAccount().getUuid(), CDN3_EXPERIMENT_NAME);
|
||||
int cdn = useCdn3 ? 3 : 2;
|
||||
final AttachmentGenerator.Descriptor descriptor = this.attachmentGenerators.get(cdn).generateAttachment(key);
|
||||
return new AttachmentDescriptorV3(cdn, key, descriptor.headers(), descriptor.signedUploadLocation());
|
||||
}
|
||||
|
||||
private String generateAttachmentKey() {
|
||||
final byte[] bytes = new byte[15];
|
||||
secureRandom.nextBytes(bytes);
|
||||
return Base64.getUrlEncoder().encodeToString(bytes);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,22 @@
|
|||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Map;
|
||||
|
||||
public record AttachmentDescriptorV3(int cdn, String key, Map<String, String> headers, String signedUploadLocation) {
|
||||
public record AttachmentDescriptorV3(
|
||||
@Schema(description = """
|
||||
Indicates the CDN type. 2 in the v3 API, 2 or 3 in the v4 API.
|
||||
2 indicates resumable uploads using GCS,
|
||||
3 indicates resumable uploads using TUS
|
||||
""")
|
||||
int cdn,
|
||||
@Schema(description = "The location within the specified cdn where the finished upload can be found")
|
||||
String key,
|
||||
@Schema(description = "A map of headers to include with all upload requests. Potentially contains time-limited upload credentials")
|
||||
Map<String, String> headers,
|
||||
|
||||
@Schema(description = "The URL to upload to with the appropriate protocol")
|
||||
String signedUploadLocation) {
|
||||
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import java.security.spec.InvalidKeySpecException;
|
|||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.assertj.core.api.Condition;
|
||||
|
@ -33,10 +34,15 @@ import org.assertj.core.api.InstanceOfAssertFactories;
|
|||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.whispersystems.textsecuregcm.attachments.GcsAttachmentGenerator;
|
||||
import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
|
||||
import org.whispersystems.textsecuregcm.attachments.TusConfiguration;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV2;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV3;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
|
@ -51,6 +57,17 @@ class AttachmentControllerTest {
|
|||
private static final RateLimiters RATE_LIMITERS = MockUtils.buildMock(RateLimiters.class, rateLimiters ->
|
||||
when(rateLimiters.getAttachmentLimiter()).thenReturn(RATE_LIMITER));
|
||||
|
||||
|
||||
private static String CDN3_ENABLED_CREDS = AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD);
|
||||
private static String CDN3_DISABLED_CREDS = AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO);
|
||||
private static final ExperimentEnrollmentManager EXPERIMENT_MANAGER = MockUtils.buildMock(ExperimentEnrollmentManager.class, mgr -> {
|
||||
when(mgr.isEnrolled(AuthHelper.VALID_UUID, AttachmentControllerV4.CDN3_EXPERIMENT_NAME)).thenReturn(true);
|
||||
when(mgr.isEnrolled(AuthHelper.VALID_UUID_TWO, AttachmentControllerV4.CDN3_EXPERIMENT_NAME)).thenReturn(false);
|
||||
});
|
||||
|
||||
private static final byte[] TUS_SECRET = getRandomBytes(32);
|
||||
private static final String TUS_URL = "https://example.com/uploads";
|
||||
|
||||
public static final String RSA_PRIVATE_KEY_PEM;
|
||||
|
||||
static {
|
||||
|
@ -67,10 +84,13 @@ class AttachmentControllerTest {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private static final ResourceExtension resources;
|
||||
|
||||
static {
|
||||
try {
|
||||
final GcsAttachmentGenerator gcsAttachmentGenerator = new GcsAttachmentGenerator("some-cdn.signal.org",
|
||||
"signal@example.com", 1000, "/attach-here", RSA_PRIVATE_KEY_PEM);
|
||||
resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
|
@ -78,13 +98,43 @@ class AttachmentControllerTest {
|
|||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new AttachmentControllerV2(RATE_LIMITERS, "accessKey", "accessSecret", "us-east-1", "attachmentv2-bucket"))
|
||||
.addResource(new AttachmentControllerV3(RATE_LIMITERS, "some-cdn.signal.org", "signal@example.com", 1000, "/attach-here", RSA_PRIVATE_KEY_PEM))
|
||||
.build();
|
||||
.addResource(new AttachmentControllerV3(RATE_LIMITERS, gcsAttachmentGenerator))
|
||||
.addProvider(new AttachmentControllerV4(RATE_LIMITERS,
|
||||
gcsAttachmentGenerator,
|
||||
new TusAttachmentGenerator(new TusConfiguration( new SecretBytes(TUS_SECRET), TUS_URL)),
|
||||
EXPERIMENT_MANAGER))
|
||||
.build();
|
||||
} catch (IOException | InvalidKeyException | InvalidKeySpecException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testV4TusForm() {
|
||||
AttachmentDescriptorV3 descriptor = resources.getJerseyTest()
|
||||
.target("/v4/attachments/form/upload")
|
||||
.request()
|
||||
.header("Authorization", CDN3_ENABLED_CREDS)
|
||||
.get(AttachmentDescriptorV3.class);
|
||||
assertThat(descriptor.cdn()).isEqualTo(3);
|
||||
assertThat(descriptor.key()).isNotBlank();
|
||||
assertThat(descriptor.signedUploadLocation()).isEqualTo(TUS_URL + "/" + "attachments");
|
||||
final String filenameb64 = descriptor.headers().get("Upload-Metadata").split(" ")[1];
|
||||
final String filename = new String(Base64.getDecoder().decode(filenameb64));
|
||||
assertThat(descriptor.key()).isEqualTo(filename);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testV4GcsForm() {
|
||||
AttachmentDescriptorV3 descriptor = resources.getJerseyTest()
|
||||
.target("/v4/attachments/form/upload")
|
||||
.request()
|
||||
.header("Authorization", CDN3_DISABLED_CREDS)
|
||||
.get(AttachmentDescriptorV3.class);
|
||||
assertThat(descriptor.cdn()).isEqualTo(2);
|
||||
assertValidCdn2Response(descriptor);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testV3Form() {
|
||||
AttachmentDescriptorV3 descriptor = resources.getJerseyTest()
|
||||
|
@ -92,7 +142,10 @@ class AttachmentControllerTest {
|
|||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(AttachmentDescriptorV3.class);
|
||||
assertValidCdn2Response(descriptor);
|
||||
}
|
||||
|
||||
private static void assertValidCdn2Response(final AttachmentDescriptorV3 descriptor) {
|
||||
assertThat(descriptor.key()).isNotBlank();
|
||||
assertThat(descriptor.cdn()).isEqualTo(2);
|
||||
assertThat(descriptor.headers()).hasSize(3);
|
||||
|
@ -123,8 +176,8 @@ class AttachmentControllerTest {
|
|||
for (final String queryTerm : queryTerms) {
|
||||
final String[] keyValueArray = queryTerm.split("=", 2);
|
||||
queryParamMap.put(
|
||||
URLDecoder.decode(keyValueArray[0], StandardCharsets.UTF_8),
|
||||
URLDecoder.decode(keyValueArray[1], StandardCharsets.UTF_8));
|
||||
URLDecoder.decode(keyValueArray[0], StandardCharsets.UTF_8),
|
||||
URLDecoder.decode(keyValueArray[1], StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
assertThat(queryParamMap).extractingByKey("X-Goog-Algorithm").isEqualTo("GOOG4-RSA-SHA256");
|
||||
|
@ -191,4 +244,9 @@ class AttachmentControllerTest {
|
|||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
}
|
||||
|
||||
private static byte[] getRandomBytes(int length) {
|
||||
byte[] result = new byte[length];
|
||||
ThreadLocalRandom.current().nextBytes(result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue