JCLOUDS-255: Support S3 signed URLs with expiry

Mostly code motion from AWSS3BlobRequestSigner to S3BlobRequestSigner
with some additional cleanups.
This commit is contained in:
Andrew Gaul 2017-04-18 12:48:19 -07:00
parent 24f961eac2
commit 7a110b31ba
11 changed files with 74 additions and 171 deletions

View File

@ -33,6 +33,7 @@ import org.jclouds.rest.internal.RestAnnotationProcessor;
import org.jclouds.s3.S3Client;
import org.jclouds.s3.blobstore.functions.BlobToObject;
import org.jclouds.s3.domain.S3Object;
import org.jclouds.s3.filters.RequestAuthorizeSignature;
import org.jclouds.s3.options.PutObjectOptions;
import com.google.common.collect.ImmutableList;
@ -40,6 +41,8 @@ import com.google.common.reflect.Invokable;
@Singleton
public class S3BlobRequestSigner<T extends S3Client> implements BlobRequestSigner {
private final RequestAuthorizeSignature authSigner;
protected final RestAnnotationProcessor processor;
protected final BlobToObject blobToObject;
protected final BlobToHttpGetOptions blob2HttpGetOptions;
@ -50,14 +53,16 @@ public class S3BlobRequestSigner<T extends S3Client> implements BlobRequestSigne
@Inject
public S3BlobRequestSigner(RestAnnotationProcessor processor, BlobToObject blobToObject,
BlobToHttpGetOptions blob2HttpGetOptions, Class<T> interfaceClass) throws SecurityException,
NoSuchMethodException {
BlobToHttpGetOptions blob2HttpGetOptions, Class<T> interfaceClass,
RequestAuthorizeSignature authSigner)
throws SecurityException, NoSuchMethodException {
this.processor = checkNotNull(processor, "processor");
this.blobToObject = checkNotNull(blobToObject, "blobToObject");
this.blob2HttpGetOptions = checkNotNull(blob2HttpGetOptions, "blob2HttpGetOptions");
this.getMethod = method(interfaceClass, "getObject", String.class, String.class, GetOptions[].class);
this.deleteMethod = method(interfaceClass, "deleteObject", String.class, String.class);
this.createMethod = method(interfaceClass, "putObject", String.class, S3Object.class, PutObjectOptions[].class);
this.authSigner = authSigner;
}
@Override
@ -69,7 +74,10 @@ public class S3BlobRequestSigner<T extends S3Client> implements BlobRequestSigne
@Override
public HttpRequest signGetBlob(String container, String name, long timeInSeconds) {
throw new UnsupportedOperationException();
checkNotNull(container, "container");
checkNotNull(name, "name");
HttpRequest request = processor.apply(Invocation.create(getMethod, ImmutableList.<Object> of(container, name)));
return cleanRequest(authSigner.signForTemporaryAccess(request, timeInSeconds));
}
@Override
@ -82,7 +90,11 @@ public class S3BlobRequestSigner<T extends S3Client> implements BlobRequestSigne
@Override
public HttpRequest signPutBlob(String container, Blob blob, long timeInSeconds) {
throw new UnsupportedOperationException();
checkNotNull(container, "container");
checkNotNull(blob, "blob");
HttpRequest request = processor.apply(Invocation.create(createMethod,
ImmutableList.<Object>of(container, blobToObject.apply(blob))));
return cleanRequest(authSigner.signForTemporaryAccess(request, timeInSeconds));
}
@Override

View File

@ -16,6 +16,7 @@
*/
package org.jclouds.s3.filters;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpRequestFilter;
/**
@ -23,4 +24,5 @@ import org.jclouds.http.HttpRequestFilter;
*/
public interface RequestAuthorizeSignature extends HttpRequestFilter {
HttpRequest signForTemporaryAccess(HttpRequest request, long timeInSeconds);
}

View File

@ -29,9 +29,11 @@ import static org.jclouds.s3.reference.S3Constants.PROPERTY_S3_VIRTUAL_HOST_BUCK
import static org.jclouds.util.Strings2.toInputStream;
import java.util.Collection;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import javax.inject.Inject;
@ -42,14 +44,17 @@ import javax.inject.Singleton;
import org.jclouds.Constants;
import org.jclouds.aws.domain.SessionCredentials;
import org.jclouds.crypto.Crypto;
import org.jclouds.date.DateService;
import org.jclouds.date.TimeStamp;
import org.jclouds.domain.Credentials;
import org.jclouds.http.HttpException;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpRequestFilter;
import org.jclouds.http.HttpUtils;
import org.jclouds.http.internal.SignatureWire;
import org.jclouds.logging.Logger;
import org.jclouds.rest.RequestSigner;
import org.jclouds.s3.reference.S3Constants;
import org.jclouds.s3.util.S3Utils;
import com.google.common.annotations.VisibleForTesting;
@ -90,13 +95,15 @@ public class RequestAuthorizeSignatureV2 implements RequestAuthorizeSignature, R
private final String headerTag;
private final String servicePath;
private final boolean isVhostStyle;
private final DateService dateService;
@Inject
public RequestAuthorizeSignatureV2(SignatureWire signatureWire, @Named(PROPERTY_AUTH_TAG) String authTag,
@Named(PROPERTY_S3_VIRTUAL_HOST_BUCKETS) boolean isVhostStyle,
@Named(PROPERTY_S3_SERVICE_PATH) String servicePath, @Named(PROPERTY_HEADER_TAG) String headerTag,
@org.jclouds.location.Provider Supplier<Credentials> creds,
@TimeStamp Provider<String> timeStampProvider, Crypto crypto, HttpUtils utils) {
@TimeStamp Provider<String> timeStampProvider, Crypto crypto, HttpUtils utils,
DateService dateService) {
this.isVhostStyle = isVhostStyle;
this.servicePath = servicePath;
this.headerTag = headerTag;
@ -106,6 +113,7 @@ public class RequestAuthorizeSignatureV2 implements RequestAuthorizeSignature, R
this.timeStampProvider = timeStampProvider;
this.crypto = crypto;
this.utils = utils;
this.dateService = dateService;
}
public HttpRequest filter(HttpRequest request) throws HttpException {
@ -261,4 +269,32 @@ public class RequestAuthorizeSignatureV2 implements RequestAuthorizeSignature, R
}
}
}
@Override
public HttpRequest signForTemporaryAccess(HttpRequest request, long timeInSeconds) {
// Update the 'DATE' header
String dateString = request.getFirstHeaderOrNull(HttpHeaders.DATE);
if (dateString == null) {
dateString = timeStampProvider.get();
}
Date date = dateService.rfc1123DateParse(dateString);
String expiration = String.valueOf(TimeUnit.MILLISECONDS.toSeconds(date.getTime()) + timeInSeconds);
HttpRequest.Builder<?> builder = request.toBuilder()
.removeHeader(HttpHeaders.AUTHORIZATION)
.replaceHeader(HttpHeaders.DATE, expiration);
String stringToSign = createStringToSign(builder.build());
String signature = sign(stringToSign);
HttpRequest ret = builder
.addQueryParam(HttpHeaders.EXPIRES, expiration)
.addQueryParam("AWSAccessKeyId", creds.get().identity)
// Signature MUST be the last parameter because if it isn't, even encoded '+' values in the
// signature will be converted to a space by a subsequent addQueryParameter.
// See HttpRequestTest.testAddBase64AndUrlEncodedQueryParams for more details.
.addQueryParam(S3Constants.TEMPORARY_SIGNATURE_PARAM, signature)
// remove signer created by RestAnnotationProcessor
.removeHeader(HttpHeaders.DATE)
.filters(ImmutableList.<HttpRequestFilter>of())
.build();
return ret;
}
}

View File

@ -106,6 +106,7 @@ public class RequestAuthorizeSignatureV4 implements RequestAuthorizeSignature {
* For example, you might store videos in an Amazon S3 bucket and make them available on your website by using presigned URLs.
* Identifies the version of AWS Signature and the algorithm that you used to calculate the signature.
*/
@Override
public HttpRequest signForTemporaryAccess(HttpRequest request, long timeInSeconds) {
return signerForQueryString.sign(request, timeInSeconds);
}

View File

@ -33,6 +33,8 @@ public final class S3Constants {
public static final String PROPERTY_S3_VIRTUAL_HOST_BUCKETS = "jclouds.s3.virtual-host-buckets";
public static final String PROPERTY_JCLOUDS_S3_CHUNKED_SIZE = "jclouds.s3.chunked.size";
public static final String TEMPORARY_SIGNATURE_PARAM = "Signature";
private S3Constants() {
throw new AssertionError("intentionally unimplemented");
}

View File

@ -16,13 +16,10 @@
*/
package org.jclouds.s3.blobstore.integration;
import static org.testng.Assert.fail;
import java.io.IOException;
import org.jclouds.blobstore.integration.internal.BaseBlobSignerLiveTest;
import org.jclouds.blobstore.integration.internal.BaseBlobStoreIntegrationTest;
import org.testng.SkipException;
import org.testng.annotations.Test;
@Test(groups = "live", testName = "S3BlobSignerLiveTest")
@ -33,59 +30,17 @@ public class S3BlobSignerLiveTest extends BaseBlobSignerLiveTest {
BaseBlobStoreIntegrationTest.SANITY_CHECK_RETURNED_BUCKET_NAME = true;
}
protected boolean supportsUrlWithTime() {
return false;
}
@Test
public void testSignGetUrlWithTime() throws InterruptedException, IOException {
try {
super.testSignGetUrlWithTime();
if (!supportsUrlWithTime()) {
fail();
}
} catch (UnsupportedOperationException uoe) {
throw new SkipException("not supported by S3 signer", uoe);
}
}
@Test
public void testSignGetUrlWithTimeExpired() throws InterruptedException, IOException {
try {
// Intentionally try with a timeout of 0. AWS signature v4 throws an error if
// the timeout is negative.
super.testSignGetUrlWithTime(/*timeout=*/ 0);
if (!supportsUrlWithTime()) {
fail();
}
} catch (UnsupportedOperationException uoe) {
throw new SkipException("not supported by S3 signer", uoe);
}
}
@Test
public void testSignPutUrlWithTime() throws Exception {
try {
super.testSignPutUrlWithTime();
if (!supportsUrlWithTime()) {
fail();
}
} catch (UnsupportedOperationException uoe) {
throw new SkipException("not supported by S3 signer", uoe);
}
// Intentionally try with a timeout of 0. AWS signature v4 throws an error if
// the timeout is negative.
super.testSignGetUrlWithTime(/*timeout=*/ 0);
}
@Test
public void testSignPutUrlWithTimeExpired() throws Exception {
try {
// Intentionally try with a timeout of 0. AWS signature v4 throws an error if
// the timeout is negative.
super.testSignPutUrlWithTime(/*timeout=*/ 0);
if (!supportsUrlWithTime()) {
fail();
}
} catch (UnsupportedOperationException uoe) {
throw new SkipException("not supported by S3 signer", uoe);
}
// Intentionally try with a timeout of 0. AWS signature v4 throws an error if
// the timeout is negative.
super.testSignPutUrlWithTime(/*timeout=*/ 0);
}
}

View File

@ -1,102 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.aws.s3.blobstore;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.jclouds.blobstore.util.BlobStoreUtils.cleanRequest;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.jclouds.aws.s3.AWSS3Client;
import org.jclouds.blobstore.domain.Blob;
import org.jclouds.blobstore.functions.BlobToHttpGetOptions;
import org.jclouds.date.DateService;
import org.jclouds.date.TimeStamp;
import org.jclouds.domain.Credentials;
import org.jclouds.http.HttpRequest;
import org.jclouds.reflect.Invocation;
import org.jclouds.rest.internal.RestAnnotationProcessor;
import org.jclouds.s3.blobstore.S3BlobRequestSigner;
import org.jclouds.s3.blobstore.functions.BlobToObject;
import org.jclouds.s3.filters.RequestAuthorizeSignatureV2;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.net.HttpHeaders;
import com.google.inject.Inject;
import com.google.inject.Provider;
public class AWSS3BlobRequestSigner extends S3BlobRequestSigner<AWSS3Client> {
public static final String TEMPORARY_SIGNATURE_PARAM = "Signature";
private final RequestAuthorizeSignatureV2 authSigner;
private final String identity;
private final DateService dateService;
private final Provider<String> timeStampProvider;
@Inject
public AWSS3BlobRequestSigner(RestAnnotationProcessor processor, BlobToObject blobToObject,
BlobToHttpGetOptions blob2HttpGetOptions, Class<AWSS3Client> interfaceClass,
@org.jclouds.location.Provider Supplier<Credentials> credentials,
RequestAuthorizeSignatureV2 authSigner, @TimeStamp Provider<String> timeStampProvider,
DateService dateService) throws SecurityException, NoSuchMethodException {
super(processor, blobToObject, blob2HttpGetOptions, interfaceClass);
this.authSigner = authSigner;
this.identity = credentials.get().identity;
this.dateService = dateService;
this.timeStampProvider = timeStampProvider;
}
@Override
public HttpRequest signGetBlob(String container, String name, long timeInSeconds) {
checkNotNull(container, "container");
checkNotNull(name, "name");
HttpRequest request = processor.apply(Invocation.create(getMethod, ImmutableList.<Object> of(container, name)));
return cleanRequest(signForTemporaryAccess(request, timeInSeconds));
}
@Override
public HttpRequest signPutBlob(String container, Blob blob, long timeInSeconds) {
checkNotNull(container, "container");
checkNotNull(blob, "blob");
HttpRequest request = processor.apply(Invocation.create(createMethod,
ImmutableList.<Object>of(container, blobToObject.apply(blob))));
return cleanRequest(signForTemporaryAccess(request, timeInSeconds));
}
private HttpRequest signForTemporaryAccess(HttpRequest request, long timeInSeconds) {
// Update the 'DATE' header
String dateString = request.getFirstHeaderOrNull(HttpHeaders.DATE);
if (dateString == null) {
dateString = timeStampProvider.get();
}
Date date = dateService.rfc1123DateParse(dateString);
String expiration = String.valueOf(TimeUnit.MILLISECONDS.toSeconds(date.getTime()) + timeInSeconds);
HttpRequest.Builder<?> builder = request.toBuilder().replaceHeader(HttpHeaders.DATE, expiration);
String stringToSign = authSigner.createStringToSign(builder.build());
String signature = authSigner.sign(stringToSign);
HttpRequest ret = builder.addQueryParam(HttpHeaders.EXPIRES, expiration)
.addQueryParam("AWSAccessKeyId", identity)
// Signature MUST be the last parameter because if it isn't, even encoded '+' values in the
// signature will be converted to a space by a subsequent addQueryParameter.
// See HttpRequestTest.testAddBase64AndUrlEncodedQueryParams for more details.
.addQueryParam(TEMPORARY_SIGNATURE_PARAM, signature)
.build();
return ret;
}
}

View File

@ -28,20 +28,20 @@ import org.jclouds.reflect.Invocation;
import org.jclouds.rest.internal.RestAnnotationProcessor;
import org.jclouds.s3.blobstore.S3BlobRequestSigner;
import org.jclouds.s3.blobstore.functions.BlobToObject;
import org.jclouds.s3.filters.RequestAuthorizeSignatureV4;
import org.jclouds.s3.filters.RequestAuthorizeSignature;
import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
public class AWSS3BlobRequestSignerV4 extends S3BlobRequestSigner<AWSS3Client> {
private final RequestAuthorizeSignatureV4 authSigner;
private final RequestAuthorizeSignature authSigner;
@Inject
public AWSS3BlobRequestSignerV4(RestAnnotationProcessor processor, BlobToObject blobToObject,
BlobToHttpGetOptions blob2HttpGetOptions, Class<AWSS3Client> interfaceClass,
RequestAuthorizeSignatureV4 authSigner) throws SecurityException, NoSuchMethodException {
super(processor, blobToObject, blob2HttpGetOptions, interfaceClass);
RequestAuthorizeSignature authSigner) throws SecurityException, NoSuchMethodException {
super(processor, blobToObject, blob2HttpGetOptions, interfaceClass, authSigner);
this.authSigner = authSigner;
}

View File

@ -18,10 +18,10 @@ package org.jclouds.aws.s3.filters;
import static org.jclouds.aws.reference.AWSConstants.PROPERTY_AUTH_TAG;
import static org.jclouds.aws.reference.AWSConstants.PROPERTY_HEADER_TAG;
import static org.jclouds.aws.s3.blobstore.AWSS3BlobRequestSigner.TEMPORARY_SIGNATURE_PARAM;
import static org.jclouds.http.utils.Queries.queryParser;
import static org.jclouds.s3.reference.S3Constants.PROPERTY_S3_SERVICE_PATH;
import static org.jclouds.s3.reference.S3Constants.PROPERTY_S3_VIRTUAL_HOST_BUCKETS;
import static org.jclouds.s3.reference.S3Constants.TEMPORARY_SIGNATURE_PARAM;
import javax.inject.Inject;
import javax.inject.Named;
@ -29,6 +29,7 @@ import javax.inject.Provider;
import javax.inject.Singleton;
import org.jclouds.crypto.Crypto;
import org.jclouds.date.DateService;
import org.jclouds.date.TimeStamp;
import org.jclouds.domain.Credentials;
import org.jclouds.http.HttpRequest;
@ -47,9 +48,10 @@ public class AWSRequestAuthorizeSignature extends RequestAuthorizeSignatureV2 {
@Named(PROPERTY_S3_VIRTUAL_HOST_BUCKETS) boolean isVhostStyle,
@Named(PROPERTY_S3_SERVICE_PATH) String servicePath, @Named(PROPERTY_HEADER_TAG) String headerTag,
@org.jclouds.location.Provider Supplier<Credentials> creds,
@TimeStamp Provider<String> timeStampProvider, Crypto crypto, HttpUtils utils) {
@TimeStamp Provider<String> timeStampProvider, Crypto crypto, HttpUtils utils,
DateService dateService) {
super(signatureWire, authTag, isVhostStyle, servicePath, headerTag, creds, timeStampProvider, crypto,
utils);
utils, dateService);
}
@Override

View File

@ -17,8 +17,8 @@
package org.jclouds.aws.s3.filters;
import static org.jclouds.http.utils.Queries.queryParser;
import static org.jclouds.aws.s3.blobstore.AWSS3BlobRequestSigner.TEMPORARY_SIGNATURE_PARAM;
import static org.jclouds.s3.filters.AwsSignatureV4Constants.AMZ_SIGNATURE_PARAM;
import static org.jclouds.s3.reference.S3Constants.TEMPORARY_SIGNATURE_PARAM;
import javax.inject.Inject;
import javax.inject.Singleton;

View File

@ -35,9 +35,4 @@ public class AWSS3BlobSignerLiveTest extends S3BlobSignerLiveTest {
overrides.setProperty(Constants.PROPERTY_SESSION_INTERVAL, "1");
return overrides;
}
@Override
protected boolean supportsUrlWithTime() {
return true;
}
}