Skip to content

Instantly share code, notes, and snippets.

@aaronanderson
Last active February 6, 2024 10:52
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aaronanderson/f9e2806cc5e2c18fab4d7e60c589d160 to your computer and use it in GitHub Desktop.
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
//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"));
}
}
@aaronanderson
Copy link
Author

Please see this pure Java JAX-RS 2.0 alternative for presigning URLs

@Hixon10
Copy link

Hixon10 commented Jan 15, 2019

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

    }
}

@mkbrv
Copy link

mkbrv commented Feb 1, 2019

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