Last active
February 6, 2024 10:52
-
-
Save aaronanderson/f9e2806cc5e2c18fab4d7e60c589d160 to your computer and use it in GitHub Desktop.
Workaround to create presigned S3 URL using the new aws-sdk-java-v2 library preview
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
//updated for 2.0.0-preview-11 | |
import java.io.ByteArrayInputStream; | |
import java.net.URI; | |
import java.time.Duration; | |
import java.time.Instant; | |
import java.util.Optional; | |
import software.amazon.awssdk.auth.signer.AwsS3V4Signer; | |
import software.amazon.awssdk.auth.signer.internal.AwsSignerExecutionAttribute; | |
import software.amazon.awssdk.core.ResponseInputStream; | |
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; | |
import software.amazon.awssdk.core.exception.SdkClientException; | |
import software.amazon.awssdk.core.interceptor.Context.BeforeTransmission; | |
import software.amazon.awssdk.core.interceptor.ExecutionAttributes; | |
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; | |
import software.amazon.awssdk.http.Abortable; | |
import software.amazon.awssdk.http.AbortableCallable; | |
import software.amazon.awssdk.http.AbortableInputStream; | |
import software.amazon.awssdk.http.SdkHttpClient; | |
import software.amazon.awssdk.http.SdkHttpConfigurationOption; | |
import software.amazon.awssdk.http.SdkHttpFullRequest; | |
import software.amazon.awssdk.http.SdkHttpFullResponse; | |
import software.amazon.awssdk.http.SdkRequestContext; | |
import software.amazon.awssdk.regions.Region; | |
import software.amazon.awssdk.services.s3.S3Client; | |
import software.amazon.awssdk.services.s3.S3ClientBuilder; | |
import software.amazon.awssdk.services.s3.model.GetObjectRequest; | |
import software.amazon.awssdk.services.s3.model.GetObjectResponse; | |
public class S3Util { | |
public static URI presignS3DownloadLink(String bucketName, String fileName) throws SdkClientException { | |
try { | |
S3ClientBuilder s3Builder = S3Client.builder().region(Region.US_WEST_1); | |
S3PresignExecutionInterceptor presignInterceptor = new S3PresignExecutionInterceptor(Region.US_WEST_1, Duration.ofDays(4)); | |
s3Builder.overrideConfiguration(ClientOverrideConfiguration.builder().addExecutionInterceptor(presignInterceptor).build()); | |
s3Builder.httpClient(new NullSdkHttpClient()); | |
S3Client s3Client = s3Builder.build(); | |
GetObjectRequest s3GetRequest = GetObjectRequest.builder().bucket(bucketName).key(fileName).build(); | |
ResponseInputStream<GetObjectResponse> response = s3Client.getObject(s3GetRequest); | |
response.close(); | |
return presignInterceptor.getSignedURI(); | |
} catch (Throwable t) { | |
if (t instanceof SdkClientException) { | |
throw (SdkClientException) t; | |
} | |
throw SdkClientException.builder().cause(t).build(); | |
} | |
} | |
public static class NullSdkHttpClient implements SdkHttpClient { | |
@Override | |
public void close() { | |
} | |
@Override | |
public <T> Optional<T> getConfigurationValue(SdkHttpConfigurationOption<T> key) { | |
return Optional.empty(); | |
} | |
@Override | |
public AbortableCallable<SdkHttpFullResponse> prepareRequest(SdkHttpFullRequest request, SdkRequestContext requestContext) { | |
return new AbortableCallable<SdkHttpFullResponse>() { | |
@Override | |
public SdkHttpFullResponse call() throws Exception { | |
return SdkHttpFullResponse.builder().statusCode(200).content(new AbortableInputStream(new ByteArrayInputStream(new byte[0]), new Abortable() { | |
@Override | |
public void abort() { | |
} | |
})).build(); | |
} | |
@Override | |
public void abort() { | |
} | |
}; | |
} | |
} | |
public static class S3PresignExecutionInterceptor implements ExecutionInterceptor { | |
final private AwsS3V4Signer signer; | |
final private String serviceName; | |
final private Region region; | |
final private Duration expirationTime; | |
final private Integer timeOffset; | |
private URI signedURI; | |
public S3PresignExecutionInterceptor(Region region, Duration expirationTime) { | |
this.signer = AwsS3V4Signer.create(); | |
this.serviceName = "s3"; | |
this.region = region; | |
this.expirationTime = expirationTime; | |
this.timeOffset = 2; | |
} | |
@Override | |
public void beforeTransmission(BeforeTransmission context, ExecutionAttributes executionAttributes) { | |
// remove all headers because a Browser that downloads the shared URL will not send the exact values. X-Amz-SignedHeaders should only contain the host header. | |
SdkHttpFullRequest modifiedSdkRequest = context.httpRequest().toBuilder().clearHeaders().build(); | |
executionAttributes.putAttribute(AwsSignerExecutionAttribute.PRESIGNER_EXPIRATION, Instant.ofEpochSecond(0).plus(expirationTime)); | |
executionAttributes.putAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, serviceName); | |
executionAttributes.putAttribute(AwsSignerExecutionAttribute.SIGNING_REGION, region); | |
executionAttributes.putAttribute(AwsSignerExecutionAttribute.TIME_OFFSET, timeOffset); | |
SdkHttpFullRequest signedRequest = signer.presign(modifiedSdkRequest, executionAttributes);// sign(getRequest, new ExecutionAttributes()); | |
signedURI = signedRequest.getUri(); | |
} | |
public URI getSignedURI() { | |
return signedURI; | |
} | |
} | |
public static void main(String[] args) throws Exception { | |
System.out.format("Download URL: %s\n", presignS3DownloadLink("some-bucket", "some-file.txt")); | |
} | |
} | |
New version of code may looks like this:
public class S3Util {
public static URI presignS3DownloadLink(String bucketName, String fileName) throws SdkClientException {
try {
S3ClientBuilder s3Builder = S3Client.builder().region(Region.EU_WEST_1);
S3PresignExecutionInterceptor presignInterceptor = new S3PresignExecutionInterceptor(Region.EU_WEST_1, Duration.ofDays(4));
s3Builder.overrideConfiguration(ClientOverrideConfiguration.builder().addExecutionInterceptor(presignInterceptor).build());
s3Builder.httpClient(new NullSdkHttpClient());
S3Client s3Client = s3Builder.build();
GetObjectRequest s3GetRequest = GetObjectRequest.builder().bucket(bucketName).key(fileName).build();
ResponseInputStream<GetObjectResponse> response = s3Client.getObject(s3GetRequest);
response.close();
return presignInterceptor.getSignedURI();
} catch (Throwable t) {
if (t instanceof SdkClientException) {
throw (SdkClientException) t;
}
throw SdkClientException.builder().cause(t).build();
}
}
public static class NullSdkHttpClient implements SdkHttpClient {
@Override
public void close() {
}
@Override
public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) {
return new ExecutableHttpRequest() {
@Override
public HttpExecuteResponse call() {
AbortableInputStream abortableInputStream = AbortableInputStream.create(new ByteArrayInputStream(new byte[0]), () -> { });
return HttpExecuteResponse.builder().responseBody(abortableInputStream)
.response(SdkHttpFullResponse.builder()
.statusCode(200)
.content(abortableInputStream)
.build())
.build();
}
@Override
public void abort() {
}
};
}
}
public static class S3PresignExecutionInterceptor implements ExecutionInterceptor {
final private AwsS3V4Signer signer;
final private String serviceName;
final private Region region;
final private Duration expirationTime;
final private Integer timeOffset;
private URI signedURI;
public S3PresignExecutionInterceptor(Region region, Duration expirationTime) {
this.signer = AwsS3V4Signer.create();
this.serviceName = "s3";
this.region = region;
this.expirationTime = expirationTime;
this.timeOffset = 2;
}
@Override
public void beforeTransmission(BeforeTransmission context, ExecutionAttributes executionAttributes) {
// remove all headers because a Browser that downloads the shared URL will not send the exact values. X-Amz-SignedHeaders should only contain the host header.
SdkHttpRequest modifiedSdkRequest = context.httpRequest().toBuilder().clearHeaders().build();
SdkHttpFullRequest sdkHttpFullRequest = SdkHttpFullRequest.builder()
.uri(modifiedSdkRequest.getUri())
.method(modifiedSdkRequest.method())
.build();
executionAttributes.putAttribute(AwsSignerExecutionAttribute.PRESIGNER_EXPIRATION, Instant.ofEpochSecond(0).plus(expirationTime));
executionAttributes.putAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, serviceName);
executionAttributes.putAttribute(AwsSignerExecutionAttribute.SIGNING_REGION, region);
executionAttributes.putAttribute(AwsSignerExecutionAttribute.TIME_OFFSET, timeOffset);
SdkHttpFullRequest signedRequest = signer.presign(sdkHttpFullRequest, executionAttributes);// sign(getRequest, new ExecutionAttributes());
signedURI = signedRequest.getUri();
}
public URI getSignedURI() {
return signedURI;
}
}
}
Thank you for the above example and for your effort.
I am trying to get this signed URL to execute a PUT with a file from a browser. Until now all I receive is : The request signature we calculated does not match the signature you provided. Check your key and signing method. .
I suppose it might be related to the file type not being taken into calculation in the above case, while AWS considers it when it calculates the signature. I will keep trying and come back with a solution.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Please see this pure Java JAX-RS 2.0 alternative for presigning URLs