Add v4 attachment controller

Add AttachmentControllerV4 which can be configured to generate upload
forms for a TUS based CDN
This commit is contained in:
ravi-signal
2023-07-21 12:09:45 -05:00
committed by GitHub
parent 9df923d916
commit 705fb93e45
12 changed files with 344 additions and 33 deletions

View File

@@ -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() {

View File

@@ -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);
}
}