diff --git a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/GoogleCloudStorageBlobRequestSigner.java b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/GoogleCloudStorageBlobRequestSigner.java new file mode 100644 index 0000000000..390ada14ea --- /dev/null +++ b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/GoogleCloudStorageBlobRequestSigner.java @@ -0,0 +1,153 @@ +/* + * 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.googlecloudstorage.blobstore; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.net.URI; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; + +import javax.annotation.Resource; +import javax.inject.Provider; + +import org.jclouds.Constants; +import org.jclouds.blobstore.BlobRequestSigner; +import org.jclouds.blobstore.domain.Blob; +import org.jclouds.blobstore.functions.BlobToHttpGetOptions; +import org.jclouds.date.TimeStamp; +import org.jclouds.domain.Credentials; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpUtils; +import org.jclouds.http.Uris; +import org.jclouds.http.options.GetOptions; +import org.jclouds.logging.Logger; +import org.jclouds.oauth.v2.config.Authorization; + +import com.google.common.base.Charsets; +import com.google.common.base.Strings; +import com.google.common.base.Supplier; +import com.google.common.io.BaseEncoding; +import com.google.common.net.HttpHeaders; +import com.google.inject.Inject; +import com.google.inject.name.Named; + +public final class GoogleCloudStorageBlobRequestSigner implements BlobRequestSigner { + private static final int DEFAULT_EXPIRY_SECONDS = 15 * 60; + private static final URI STORAGE_URL = URI.create("http://storage.googleapis.com/"); + + private final Supplier creds; + private final Supplier privateKey; + private final Provider timestamp; + private final HttpUtils utils; + + private final BlobToHttpGetOptions toGetOptions = new BlobToHttpGetOptions(); + + @Resource + @Named(Constants.LOGGER_SIGNATURE) + protected Logger signatureLog = Logger.NULL; + + @Inject + protected GoogleCloudStorageBlobRequestSigner(@org.jclouds.location.Provider Supplier creds, + @Authorization Supplier privateKey, @TimeStamp Provider timestamp, HttpUtils utils) { + this.creds = creds; + this.privateKey = privateKey; + this.timestamp = timestamp; + this.utils = utils; + } + + @Override + public HttpRequest signGetBlob(String container, String name) { + return signGetBlob(container, name, DEFAULT_EXPIRY_SECONDS); + } + + @Override + public HttpRequest signGetBlob(String container, String name, long timeInSeconds) { + return sign("GET", container, name, GetOptions.NONE, timestamp.get() + timeInSeconds, null); + } + + @Override + public HttpRequest signGetBlob(String container, String name, org.jclouds.blobstore.options.GetOptions options) { + return sign("GET", container, name, toGetOptions.apply(options), timestamp.get() + DEFAULT_EXPIRY_SECONDS, null); + } + + @Override + public HttpRequest signPutBlob(String container, Blob blob) { + return signPutBlob(container, blob, DEFAULT_EXPIRY_SECONDS); + } + + @Override + public HttpRequest signPutBlob(String container, Blob blob, long timeInSeconds) { + return sign("PUT", container, blob.getMetadata().getName(), GetOptions.NONE, timestamp.get() + timeInSeconds, null); + } + + @Deprecated + @Override + public HttpRequest signRemoveBlob(String container, String name) { + throw new UnsupportedOperationException(); + } + + private HttpRequest sign(String method, String container, String name, GetOptions options, long expires, String contentType) { + checkNotNull(container, "container"); + checkNotNull(name, "name"); + + HttpRequest.Builder request = HttpRequest.builder() + .method(method) + .endpoint(Uris.uriBuilder(STORAGE_URL).appendPath(container).appendPath(name).build()); + if (contentType != null) { + request.replaceHeader(HttpHeaders.CONTENT_TYPE, contentType); + } + + String stringToSign = createStringToSign(request.build(), expires); + byte[] rawSignature; + try { + Signature signer = Signature.getInstance("SHA256withRSA"); + signer.initSign(privateKey.get()); + signer.update(stringToSign.getBytes(Charsets.UTF_8)); + rawSignature = signer.sign(); + } catch (InvalidKeyException ike) { + throw new RuntimeException(ike); + } catch (NoSuchAlgorithmException nsae) { + throw new RuntimeException(nsae); + } catch (SignatureException se) { + throw new RuntimeException(se); + } + String signature = BaseEncoding.base64().encode(rawSignature); + + return (HttpRequest) request + .addQueryParam("Expires", String.valueOf(expires)) + .addQueryParam("GoogleAccessId", creds.get().identity) + .addQueryParam("Signature", signature) + .headers(options.buildRequestHeaders()) + .build(); + } + + private String createStringToSign(HttpRequest request, long expires) { + utils.logRequest(signatureLog, request, ">>"); + StringBuilder buffer = new StringBuilder(); + buffer.append(request.getMethod()).append("\n"); + buffer.append(Strings.nullToEmpty(request.getFirstHeaderOrNull(HttpHeaders.CONTENT_MD5))).append("\n"); + buffer.append(Strings.nullToEmpty(request.getFirstHeaderOrNull(HttpHeaders.CONTENT_TYPE))).append("\n"); + buffer.append(String.valueOf(expires)).append("\n"); + // TODO: extension headers + buffer.append(request.getEndpoint().getPath()); + return buffer.toString(); + } +} diff --git a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/config/GoogleCloudStorageBlobStoreContextModule.java b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/config/GoogleCloudStorageBlobStoreContextModule.java index 3d2bcd762c..1392d3fd91 100644 --- a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/config/GoogleCloudStorageBlobStoreContextModule.java +++ b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/config/GoogleCloudStorageBlobStoreContextModule.java @@ -16,11 +16,15 @@ */ package org.jclouds.googlecloudstorage.blobstore.config; +import org.jclouds.blobstore.BlobRequestSigner; import org.jclouds.blobstore.BlobStore; import org.jclouds.blobstore.attr.ConsistencyModel; +import org.jclouds.date.TimeStamp; +import org.jclouds.googlecloudstorage.blobstore.GoogleCloudStorageBlobRequestSigner; import org.jclouds.googlecloudstorage.blobstore.GoogleCloudStorageBlobStore; import com.google.inject.AbstractModule; +import com.google.inject.Provides; import com.google.inject.Scopes; public class GoogleCloudStorageBlobStoreContextModule extends AbstractModule { @@ -29,5 +33,12 @@ public class GoogleCloudStorageBlobStoreContextModule extends AbstractModule { protected void configure() { bind(ConsistencyModel.class).toInstance(ConsistencyModel.EVENTUAL); bind(BlobStore.class).to(GoogleCloudStorageBlobStore.class).in(Scopes.SINGLETON); + bind(BlobRequestSigner.class).to(GoogleCloudStorageBlobRequestSigner.class); + } + + @Provides + @TimeStamp + protected final Long unixEpochTimestamp() { + return System.currentTimeMillis() / 1000; } } diff --git a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/domain/GoogleCloudStorageObject.java b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/domain/GoogleCloudStorageObject.java index d63971097f..ce48e4f188 100644 --- a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/domain/GoogleCloudStorageObject.java +++ b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/domain/GoogleCloudStorageObject.java @@ -46,7 +46,7 @@ public abstract class GoogleCloudStorageObject { public abstract String bucket(); public abstract long generation(); public abstract long metageneration(); - public abstract String contentType(); + @Nullable public abstract String contentType(); public abstract Date updated(); @Nullable public abstract Date timeDeleted(); public abstract StorageClass storageClass(); diff --git a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/handlers/GoogleCloudStorageErrorHandler.java b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/handlers/GoogleCloudStorageErrorHandler.java index 9c6840e9c4..d330146673 100644 --- a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/handlers/GoogleCloudStorageErrorHandler.java +++ b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/handlers/GoogleCloudStorageErrorHandler.java @@ -48,6 +48,11 @@ public class GoogleCloudStorageErrorHandler implements HttpErrorHandler { switch (response.getStatusCode()) { case 308: return; + case 400: + if (message.indexOf("ExpiredToken") != -1) { + exception = new AuthorizationException(message, exception); + } + break; case 401: case 403: exception = new AuthorizationException(message, exception); diff --git a/providers/google-cloud-storage/src/test/java/org/jclouds/googlecloudstorage/blobstore/integration/GoogleCloudStorageBlobSignerLiveTest.java b/providers/google-cloud-storage/src/test/java/org/jclouds/googlecloudstorage/blobstore/integration/GoogleCloudStorageBlobSignerLiveTest.java index 7e7dd52d62..2f76490649 100644 --- a/providers/google-cloud-storage/src/test/java/org/jclouds/googlecloudstorage/blobstore/integration/GoogleCloudStorageBlobSignerLiveTest.java +++ b/providers/google-cloud-storage/src/test/java/org/jclouds/googlecloudstorage/blobstore/integration/GoogleCloudStorageBlobSignerLiveTest.java @@ -16,8 +16,6 @@ */ package org.jclouds.googlecloudstorage.blobstore.integration; -import java.io.IOException; - import org.jclouds.blobstore.integration.internal.BaseBlobSignerLiveTest; import org.testng.SkipException; import org.testng.annotations.Test; @@ -28,70 +26,6 @@ public class GoogleCloudStorageBlobSignerLiveTest extends BaseBlobSignerLiveTest provider = "google-cloud-storage"; } - @Test - public void testSignGetUrl() throws Exception { - try { - super.testSignGetUrl(); - } catch (UnsupportedOperationException uoe) { - throw new SkipException("not yet implemented in GCS", uoe); - } - } - - @Test - public void testSignGetUrlOptions() throws Exception { - try { - super.testSignGetUrlOptions(); - } catch (UnsupportedOperationException uoe) { - throw new SkipException("not yet implemented in GCS", uoe); - } - } - - @Test - public void testSignGetUrlWithTime() throws InterruptedException, IOException { - try { - super.testSignGetUrlWithTime(); - } catch (UnsupportedOperationException uoe) { - throw new SkipException("not yet implemented in GCS", uoe); - } - } - - @Test - public void testSignGetUrlWithTimeExpired() throws InterruptedException, IOException { - try { - super.testSignGetUrlWithTimeExpired(); - } catch (UnsupportedOperationException uoe) { - throw new SkipException("not yet implemented in GCS", uoe); - } - } - - @Test - public void testSignPutUrl() throws Exception { - try { - super.testSignPutUrl(); - } catch (UnsupportedOperationException uoe) { - throw new SkipException("not yet implemented in GCS", uoe); - } - } - - @Test - public void testSignPutUrlWithTime() throws Exception { - try { - super.testSignPutUrlWithTime(); - } catch (UnsupportedOperationException uoe) { - throw new SkipException("not yet implemented in GCS", uoe); - } - } - - @Test - public void testSignPutUrlWithTimeExpired() throws Exception { - try { - super.testSignPutUrlWithTimeExpired(); - } catch (UnsupportedOperationException uoe) { - throw new SkipException("not yet implemented in GCS", uoe); - } - } - - @Test public void testSignRemoveUrl() throws Exception { try { super.testSignRemoveUrl();