-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add AttachmentControllerV4 which can be configured to generate upload forms for a TUS based CDN
- Loading branch information
1 parent
9df923d
commit 705fb93
Showing
12 changed files
with
344 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
service/src/main/java/org/whispersystems/textsecuregcm/attachments/AttachmentGenerator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
|
||
} |
54 changes: 54 additions & 0 deletions
54
...ce/src/main/java/org/whispersystems/textsecuregcm/attachments/GcsAttachmentGenerator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"); | ||
} | ||
|
||
|
||
} |
47 changes: 47 additions & 0 deletions
47
...ce/src/main/java/org/whispersystems/textsecuregcm/attachments/TusAttachmentGenerator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
service/src/main/java/org/whispersystems/textsecuregcm/attachments/TusConfiguration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
){} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
99 changes: 99 additions & 0 deletions
99
...ce/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV4.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} | ||
|
Oops, something went wrong.