diff --git a/gcloud-java-core/pom.xml b/gcloud-java-core/pom.xml index 78baf824c080..fa2e1c18972f 100644 --- a/gcloud-java-core/pom.xml +++ b/gcloud-java-core/pom.xml @@ -28,13 +28,13 @@ com.google.http-client google-http-client - 1.19.0 + 1.20.0 compile com.google.oauth-client google-oauth-client - 1.19.0 + 1.20.0 compile diff --git a/gcloud-java-core/src/main/java/com/google/gcloud/AuthCredentials.java b/gcloud-java-core/src/main/java/com/google/gcloud/AuthCredentials.java index 839da54e62cf..6cdb737ddd91 100644 --- a/gcloud-java-core/src/main/java/com/google/gcloud/AuthCredentials.java +++ b/gcloud-java-core/src/main/java/com/google/gcloud/AuthCredentials.java @@ -62,7 +62,7 @@ private Object readResolve() throws ObjectStreamException { } } - private static class ServiceAccountAuthCredentials extends AuthCredentials { + public static class ServiceAccountAuthCredentials extends AuthCredentials { private static final long serialVersionUID = 8007708734318445901L; private final String account; @@ -94,6 +94,14 @@ protected HttpRequestInitializer httpRequestInitializer( return builder.build(); } + public String account() { + return account; + } + + public PrivateKey privateKey() { + return privateKey; + } + @Override public int hashCode() { return Objects.hash(account, privateKey); @@ -187,7 +195,7 @@ public static AuthCredentials createApplicationDefaults() throws IOException { return new ApplicationDefaultAuthCredentials(); } - public static AuthCredentials createFor(String account, PrivateKey privateKey) { + public static ServiceAccountAuthCredentials createFor(String account, PrivateKey privateKey) { return new ServiceAccountAuthCredentials(account, privateKey); } diff --git a/gcloud-java-examples/pom.xml b/gcloud-java-examples/pom.xml index 66d9fc9c93e3..1c0357d63635 100644 --- a/gcloud-java-examples/pom.xml +++ b/gcloud-java-examples/pom.xml @@ -21,4 +21,15 @@ ${project.version} + + + + org.codehaus.mojo + exec-maven-plugin + + false + + + + diff --git a/gcloud-java-examples/src/main/java/com/google/gcloud/examples/StorageExample.java b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/StorageExample.java index b0d44c292d2c..ccf2cd6f5b76 100644 --- a/gcloud-java-examples/src/main/java/com/google/gcloud/examples/StorageExample.java +++ b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/StorageExample.java @@ -16,6 +16,8 @@ package com.google.gcloud.examples; +import com.google.gcloud.AuthCredentials; +import com.google.gcloud.AuthCredentials.ServiceAccountAuthCredentials; import com.google.gcloud.RetryParams; import com.google.gcloud.spi.StorageRpc.Tuple; import com.google.gcloud.storage.BatchRequest; @@ -27,6 +29,7 @@ import com.google.gcloud.storage.StorageService; import com.google.gcloud.storage.StorageService.ComposeRequest; import com.google.gcloud.storage.StorageService.CopyRequest; +import com.google.gcloud.storage.StorageService.SignUrlOption; import com.google.gcloud.storage.StorageServiceFactory; import com.google.gcloud.storage.StorageServiceOptions; @@ -40,7 +43,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import java.util.Arrays; +import java.util.Calendar; import java.util.HashMap; import java.util.Map; @@ -58,7 +68,8 @@ * -Dexec.args="[] list []| info [ []]| * download [local_file]| upload []| * delete +| cp | - * compose + | update_metadata [key=value]*"} + * compose + | update_metadata [key=value]*| + * sign_url "} * * * @@ -75,7 +86,7 @@ private static abstract class StorageAction { abstract void run(StorageService storage, T request) throws Exception; - abstract T parse(String... args) throws IllegalArgumentException, IOException; + abstract T parse(String... args) throws Exception; protected String params() { return ""; @@ -424,7 +435,7 @@ public String params() { * * @see Objects: update */ - private static class UpdateMetadata extends StorageAction>> { + private static class UpdateMetadataAction extends StorageAction>> { @Override public void run(StorageService storage, Tuple> tuple) @@ -467,6 +478,52 @@ public String params() { } } + /** + * This class demonstrates how to sign a url. + * URL will be valid for 1 day. + * + * @see Signed URLs + */ + private static class SignUrlAction extends + StorageAction> { + + private static final char[] PASSWORD = "notasecret".toCharArray(); + + @Override + public void run(StorageService storage, Tuple tuple) + throws Exception { + run(storage, tuple.x(), tuple.y()); + } + + private void run(StorageService storage, ServiceAccountAuthCredentials cred, Blob blob) + throws IOException { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, 1); + long expiration = cal.getTimeInMillis() / 1000; + System.out.println("Signed URL: " + + storage.signUrl(blob, expiration, SignUrlOption.serviceAccount(cred))); + } + + @Override + Tuple parse(String... args) + throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException, + UnrecoverableKeyException { + if (args.length != 4) { + throw new IllegalArgumentException(); + } + KeyStore keystore = KeyStore.getInstance("PKCS12"); + keystore.load(Files.newInputStream(Paths.get(args[0])), PASSWORD); + PrivateKey privateKey = (PrivateKey) keystore.getKey("privatekey", PASSWORD); + ServiceAccountAuthCredentials cred = AuthCredentials.createFor(args[1], privateKey); + return Tuple.of(cred, Blob.of(args[2], args[3])); + } + + @Override + public String params() { + return " "; + } + } + static { ACTIONS.put("info", new InfoAction()); ACTIONS.put("delete", new DeleteAction()); @@ -475,7 +532,8 @@ public String params() { ACTIONS.put("download", new DownloadAction()); ACTIONS.put("cp", new CopyAction()); ACTIONS.put("compose", new ComposeAction()); - ACTIONS.put("update_metadata", new UpdateMetadata()); + ACTIONS.put("update_metadata", new UpdateMetadataAction()); + ACTIONS.put("sign_url", new SignUrlAction()); } public static void printUsage() { diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java index e27d837d7173..f63c57e3c784 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java @@ -82,9 +82,9 @@ public DefaultStorageRpc(StorageServiceOptions options) { HttpRequestInitializer initializer = options.httpRequestInitializer(); this.options = options; storage = new Storage.Builder(transport, new JacksonFactory(), initializer) + .setRootUrl(options.host()) .setApplicationName("gcloud-java") .build(); - // Todo: make sure nulls are being used as Data.asNull() } private static StorageServiceException translate(IOException exception) { diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/StorageRpc.java b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/StorageRpc.java index ab1e9affbbce..5a99cce69aa5 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/StorageRpc.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/StorageRpc.java @@ -1 +1 @@ -/* * Copyright 2015 Google Inc. All Rights Reserved. * * Licensed 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 com.google.gcloud.spi; import com.google.api.services.storage.model.Bucket; import com.google.api.services.storage.model.StorageObject; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.gcloud.storage.StorageServiceException; import java.util.List; import java.util.Map; public interface StorageRpc { enum Option { PREDEFINED_ACL("predefinedAcl"), PREDEFINED_DEFAULT_OBJECT_ACL("predefinedDefaultObjectAcl"), IF_METAGENERATION_MATCH("ifMetagenerationMatch"), IF_METAGENERATION_NOT_MATCH("ifMetagenerationNotMatch"), IF_GENERATION_NOT_MATCH("ifGenerationMatch"), IF_GENERATION_MATCH("ifGenerationNotMatch"), IF_SOURCE_METAGENERATION_MATCH("ifSourceMetagenerationMatch"), IF_SOURCE_METAGENERATION_NOT_MATCH("ifSourceMetagenerationNotMatch"), IF_SOURCE_GENERATION_MATCH("ifSourceGenerationMatch"), IF_SOURCE_GENERATION_NOT_MATCH("ifSourceGenerationNotMatch"), PREFIX("prefix"), MAX_RESULTS("maxResults"), PAGE_TOKEN("pageToken"), DELIMITER("delimiter"), VERSIONS("versions"); private final String value; Option(String value) { this.value = value; } public String value() { return value; } @SuppressWarnings("unchecked") T get(Map options) { return (T) options.get(this); } String getString(Map options) { return get(options); } Long getLong(Map options) { return get(options); } Boolean getBoolean(Map options) { return get(options); } } class Tuple { private final X x; private final Y y; private Tuple(X x, Y y) { this.x = x; this.y = y; } public static Tuple of(X x, Y y) { return new Tuple<>(x, y); } public X x() { return x; } public Y y() { return y; } } class BatchRequest { public final List>> toDelete; public final List>> toUpdate; public final List>> toGet; public BatchRequest(Iterable>> toDelete, Iterable>> toUpdate, Iterable>> toGet) { this.toDelete = ImmutableList.copyOf(toDelete); this.toUpdate = ImmutableList.copyOf(toUpdate); this.toGet = ImmutableList.copyOf(toGet); } } class BatchResponse { public final Map> deletes; public final Map> updates; public final Map> gets; public BatchResponse(Map> deletes, Map> updates, Map> gets) { this.deletes = ImmutableMap.copyOf(deletes); this.updates = ImmutableMap.copyOf(updates); this.gets = ImmutableMap.copyOf(gets); } } Bucket create(Bucket bucket, Map options) throws StorageServiceException; StorageObject create(StorageObject object, byte[] content, Map options) throws StorageServiceException; Tuple> list(Map options) throws StorageServiceException; Tuple> list(String bucket, Map options) throws StorageServiceException; Bucket get(Bucket bucket, Map options) throws StorageServiceException; StorageObject get(StorageObject object, Map options) throws StorageServiceException; Bucket patch(Bucket bucket, Map options) throws StorageServiceException; StorageObject patch(StorageObject storageObject, Map options) throws StorageServiceException; boolean delete(Bucket bucket, Map options) throws StorageServiceException; boolean delete(StorageObject object, Map options) throws StorageServiceException; BatchResponse batch(BatchRequest request) throws StorageServiceException; StorageObject compose(Iterable sources, StorageObject target, Map targetOptions) throws StorageServiceException; StorageObject copy(StorageObject source, Map sourceOptions, StorageObject target, Map targetOptions) throws StorageServiceException; byte[] load(StorageObject storageObject, Map options) throws StorageServiceException; byte[] read(StorageObject from, Map options, long position, int bytes) throws StorageServiceException; String open(StorageObject object, Map options) throws StorageServiceException; void write(String uploadId, byte[] toWrite, int toWriteOffset, StorageObject dest, long destOffset, int length, boolean last) throws StorageServiceException; } \ No newline at end of file +/* * Copyright 2015 Google Inc. All Rights Reserved. * * Licensed 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 com.google.gcloud.spi; import com.google.api.services.storage.model.Bucket; import com.google.api.services.storage.model.StorageObject; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.gcloud.storage.StorageServiceException; import java.util.List; import java.util.Map; public interface StorageRpc { // These options are part of the Google Cloud storage header options enum Option { PREDEFINED_ACL("predefinedAcl"), PREDEFINED_DEFAULT_OBJECT_ACL("predefinedDefaultObjectAcl"), IF_METAGENERATION_MATCH("ifMetagenerationMatch"), IF_METAGENERATION_NOT_MATCH("ifMetagenerationNotMatch"), IF_GENERATION_NOT_MATCH("ifGenerationMatch"), IF_GENERATION_MATCH("ifGenerationNotMatch"), IF_SOURCE_METAGENERATION_MATCH("ifSourceMetagenerationMatch"), IF_SOURCE_METAGENERATION_NOT_MATCH("ifSourceMetagenerationNotMatch"), IF_SOURCE_GENERATION_MATCH("ifSourceGenerationMatch"), IF_SOURCE_GENERATION_NOT_MATCH("ifSourceGenerationNotMatch"), PREFIX("prefix"), MAX_RESULTS("maxResults"), PAGE_TOKEN("pageToken"), DELIMITER("delimiter"), VERSIONS("versions"); private final String value; Option(String value) { this.value = value; } public String value() { return value; } @SuppressWarnings("unchecked") T get(Map options) { return (T) options.get(this); } String getString(Map options) { return get(options); } Long getLong(Map options) { return get(options); } Boolean getBoolean(Map options) { return get(options); } } class Tuple { private final X x; private final Y y; private Tuple(X x, Y y) { this.x = x; this.y = y; } public static Tuple of(X x, Y y) { return new Tuple<>(x, y); } public X x() { return x; } public Y y() { return y; } } class BatchRequest { public final List>> toDelete; public final List>> toUpdate; public final List>> toGet; public BatchRequest(Iterable>> toDelete, Iterable>> toUpdate, Iterable>> toGet) { this.toDelete = ImmutableList.copyOf(toDelete); this.toUpdate = ImmutableList.copyOf(toUpdate); this.toGet = ImmutableList.copyOf(toGet); } } class BatchResponse { public final Map> deletes; public final Map> updates; public final Map> gets; public BatchResponse(Map> deletes, Map> updates, Map> gets) { this.deletes = ImmutableMap.copyOf(deletes); this.updates = ImmutableMap.copyOf(updates); this.gets = ImmutableMap.copyOf(gets); } } Bucket create(Bucket bucket, Map options) throws StorageServiceException; StorageObject create(StorageObject object, byte[] content, Map options) throws StorageServiceException; Tuple> list(Map options) throws StorageServiceException; Tuple> list(String bucket, Map options) throws StorageServiceException; Bucket get(Bucket bucket, Map options) throws StorageServiceException; StorageObject get(StorageObject object, Map options) throws StorageServiceException; Bucket patch(Bucket bucket, Map options) throws StorageServiceException; StorageObject patch(StorageObject storageObject, Map options) throws StorageServiceException; boolean delete(Bucket bucket, Map options) throws StorageServiceException; boolean delete(StorageObject object, Map options) throws StorageServiceException; BatchResponse batch(BatchRequest request) throws StorageServiceException; StorageObject compose(Iterable sources, StorageObject target, Map targetOptions) throws StorageServiceException; StorageObject copy(StorageObject source, Map sourceOptions, StorageObject target, Map targetOptions) throws StorageServiceException; byte[] load(StorageObject storageObject, Map options) throws StorageServiceException; byte[] read(StorageObject from, Map options, long position, int bytes) throws StorageServiceException; String open(StorageObject object, Map options) throws StorageServiceException; void write(String uploadId, byte[] toWrite, int toWriteOffset, StorageObject dest, long destOffset, int length, boolean last) throws StorageServiceException; } \ No newline at end of file diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Cors.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Cors.java index b1953aa5e0e4..ce8cfb95b6e9 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Cors.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Cors.java @@ -53,14 +53,10 @@ public Bucket.Cors apply(Cors cors) { }; private final Integer maxAgeSeconds; - private final ImmutableList methods; + private final ImmutableList methods; private final ImmutableList origins; private final ImmutableList responseHeaders; - public enum Method { - ANY, GET, HEAD, PUT, POST, DELETE - } - public static final class Origin implements Serializable { private static final long serialVersionUID = -4447958124895577993L; @@ -118,7 +114,7 @@ public String value() { public static final class Builder { private Integer maxAgeSeconds; - private ImmutableList methods; + private ImmutableList methods; private ImmutableList origins; private ImmutableList responseHeaders; @@ -129,7 +125,7 @@ public Builder maxAgeSeconds(Integer maxAgeSeconds) { return this; } - public Builder methods(Iterable methods) { + public Builder methods(Iterable methods) { this.methods = methods != null ? ImmutableList.copyOf(methods) : null; return this; } @@ -160,7 +156,7 @@ public Integer maxAgeSeconds() { return maxAgeSeconds; } - public List methods() { + public List methods() { return methods; } @@ -217,10 +213,10 @@ Bucket.Cors toPb() { static Cors fromPb(Bucket.Cors cors) { Builder builder = builder().maxAgeSeconds(cors.getMaxAgeSeconds()); if (cors.getMethod() != null) { - builder.methods(transform(cors.getMethod(), new Function() { + builder.methods(transform(cors.getMethod(), new Function() { @Override - public Method apply(String name) { - return Method.valueOf(name.toUpperCase()); + public HttpMethod apply(String name) { + return HttpMethod.valueOf(name.toUpperCase()); } })); } diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/HttpMethod.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/HttpMethod.java new file mode 100644 index 000000000000..9d7944140915 --- /dev/null +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/HttpMethod.java @@ -0,0 +1,24 @@ +/* + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed 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 com.google.gcloud.storage; + +/** + * Http method supported by Storage service. + */ +public enum HttpMethod { + GET, HEAD, PUT, POST, DELETE +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageService.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageService.java index 6fefb3af3b16..edf12c9f8feb 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageService.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageService.java @@ -20,10 +20,12 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; +import com.google.gcloud.AuthCredentials.ServiceAccountAuthCredentials; import com.google.gcloud.Service; import com.google.gcloud.spi.StorageRpc; import java.io.Serializable; +import java.net.URL; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; @@ -213,13 +215,71 @@ public static BlobListOption recursive(boolean recursive) { } } + class SignUrlOption implements Serializable { + + private static final long serialVersionUID = 7850569877451099267L; + + private final Option option; + private final Object value; + + enum Option { + HTTP_METHOD, CONTENT_TYPE, MD5, SERVICE_ACCOUNT_CRED; + } + + private SignUrlOption(Option option, Object value) { + this.option = option; + this.value = value; + } + + Option option() { + return option; + } + + Object value() { + return value; + } + + /** + * The HTTP method to be used with the signed URL. + */ + public static SignUrlOption httpMethod(HttpMethod httpMethod) { + return new SignUrlOption(Option.HTTP_METHOD, httpMethod.name()); + } + + /** + * Use it if signature should include the blob's content-type. + * When used, users of the signed URL should include the blob's content-type with their request. + */ + public static SignUrlOption withContentType() { + return new SignUrlOption(Option.CONTENT_TYPE, true); + } + + /** + * Use it if signature should include the blob's md5. + * When used, users of the signed URL should include the blob's md5 with their request. + */ + public static SignUrlOption withMd5() { + return new SignUrlOption(Option.MD5, true); + } + + /** + * Service account credentials which are used for signing the URL. + * If not provided an attempt will be made to get it from the environment. + * + * @see Service account + */ + public static SignUrlOption serviceAccount(ServiceAccountAuthCredentials credentials) { + return new SignUrlOption(Option.SERVICE_ACCOUNT_CRED, credentials); + } + } + class ComposeRequest implements Serializable { private static final long serialVersionUID = -7385681353748590911L; private final List sourceBlobs; private final Blob target; - private final List targetOptions; + private final List targetOptions; public static class SourceBlob implements Serializable { @@ -528,4 +588,18 @@ public static Builder builder() { * @throws StorageServiceException upon failure */ BlobWriteChannel writer(Blob blob, BlobTargetOption... options); + + /** + * Generates a signed URL for a blob. + * If you have a blob that you want to allow access to for a fixed + * amount of time, you can use this method to generate a URL that + * is only valid within a certain time period. + * This is particularly useful if you don't want publicly + * accessible blobs, but don't want to require users to explicitly log in. + * + * @param blob the blob associated with the signed url + * @param expirationTimeInSeconds the signed URL expiration (using epoch time) + * @see Signed-URLs + */ + URL signUrl(Blob blob, long expirationTimeInSeconds, SignUrlOption... options); } diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageServiceImpl.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageServiceImpl.java index 95b95141be14..ecf7064a503c 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageServiceImpl.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageServiceImpl.java @@ -29,7 +29,7 @@ import static com.google.gcloud.spi.StorageRpc.Option.IF_SOURCE_METAGENERATION_MATCH; import static com.google.gcloud.spi.StorageRpc.Option.IF_SOURCE_METAGENERATION_NOT_MATCH; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; -import static java.util.concurrent.Executors.callable; +import static java.nio.charset.StandardCharsets.UTF_8; import com.google.api.services.storage.model.StorageObject; import com.google.common.base.Function; @@ -39,7 +39,9 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; +import com.google.common.io.BaseEncoding; import com.google.common.primitives.Ints; +import com.google.gcloud.AuthCredentials.ServiceAccountAuthCredentials; import com.google.gcloud.BaseService; import com.google.gcloud.ExceptionHandler; import com.google.gcloud.ExceptionHandler.Interceptor; @@ -47,7 +49,16 @@ import com.google.gcloud.spi.StorageRpc.Tuple; import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; import java.util.Arrays; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -82,10 +93,8 @@ public RetryResult beforeEval(Exception exception) { StorageServiceImpl(StorageServiceOptions options) { super(options); storageRpc = options.storageRpc(); - // todo: replace nulls with Value.asNull (per toPb) // todo: configure timeouts - https://developers.google.com/api-client-library/java/google-api-java-client/errors // todo: provide rewrite - https://cloud.google.com/storage/docs/json_api/v1/objects/rewrite - // todo: provide signed urls - https://cloud.google.com/storage/docs/access-control#Signed-URLs // todo: check if we need to expose https://cloud.google.com/storage/docs/json_api/v1/bucketAccessControls/insert vs using bucket update/patch } @@ -429,6 +438,69 @@ public BlobWriteChannel writer(Blob blob, BlobTargetOption... options) { return new BlobWriterChannelImpl(options(), blob, optionsMap); } + @Override + public URL signUrl(Blob blob, long expiration, SignUrlOption... options) { + EnumMap optionMap = Maps.newEnumMap(SignUrlOption.Option.class); + for (SignUrlOption option : options) { + optionMap.put(option.option(), option.value()); + } + ServiceAccountAuthCredentials cred = + (ServiceAccountAuthCredentials) optionMap.get(SignUrlOption.Option.SERVICE_ACCOUNT_CRED); + if (cred == null) { + checkArgument(options().authCredentials() instanceof ServiceAccountAuthCredentials, + "Signing key was not provided and could not be derived"); + cred = (ServiceAccountAuthCredentials) this.options().authCredentials(); + } + // construct signature data - see https://cloud.google.com/storage/docs/access-control#Signed-URLs + StringBuilder stBuilder = new StringBuilder(); + if (optionMap.containsKey(SignUrlOption.Option.HTTP_METHOD)) { + stBuilder.append(optionMap.get(SignUrlOption.Option.HTTP_METHOD)); + } else { + stBuilder.append(HttpMethod.GET); + } + stBuilder.append('\n'); + if (firstNonNull((Boolean) optionMap.get(SignUrlOption.Option.MD5) , false)) { + checkArgument(blob.md5() != null, "Blob is missing a value for md5"); + stBuilder.append(blob.md5()); + } + stBuilder.append('\n'); + if (firstNonNull((Boolean) optionMap.get(SignUrlOption.Option.CONTENT_TYPE) , false)) { + checkArgument(blob.contentType() != null, "Blob is missing a value for content-type"); + stBuilder.append(blob.contentType()); + } + stBuilder.append('\n'); + stBuilder.append(expiration).append('\n'); + StringBuilder path = new StringBuilder(); + if (!blob.bucket().startsWith("/")) { + path.append('/'); + } + path.append(blob.bucket()); + if (!blob.bucket().endsWith("/")) { + path.append('/'); + } + if (blob.name().startsWith("/")) { + path.setLength(stBuilder.length() - 1); + } + path.append(blob.name()); + stBuilder.append(path); + try { + Signature signer = Signature.getInstance("SHA256withRSA"); + signer.initSign(cred.privateKey()); + signer.update(stBuilder.toString().getBytes(UTF_8)); + String signature = + URLEncoder.encode(BaseEncoding.base64().encode(signer.sign()), UTF_8.name()); + stBuilder = new StringBuilder("https://storage.googleapis.com").append(path); + stBuilder.append("?GoogleAccessId=").append(cred.account()); + stBuilder.append("&Expires=").append(expiration); + stBuilder.append("&Signature=").append(signature); + return new URL(stBuilder.toString()); + } catch (MalformedURLException | NoSuchAlgorithmException | UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } catch (SignatureException | InvalidKeyException e) { + throw new IllegalArgumentException("Invalid service account private key"); + } + } + private Map optionMap(Long generation, Long metaGeneration, Iterable options) { return optionMap(generation, metaGeneration, options, false); diff --git a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CorsTest.java b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CorsTest.java index 8b0379f03583..f978cb87f3d1 100644 --- a/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CorsTest.java +++ b/gcloud-java-storage/src/test/java/com/google/gcloud/storage/CorsTest.java @@ -19,7 +19,6 @@ import static org.junit.Assert.assertEquals; import com.google.common.collect.ImmutableList; -import com.google.gcloud.storage.Cors.Method; import com.google.gcloud.storage.Cors.Origin; import org.junit.Test; @@ -39,7 +38,7 @@ public void testOrigin() { public void corsTest() { List origins = ImmutableList.of(Origin.any(), Origin.of("o")); List headers = ImmutableList.of("h1", "h2"); - List methods = ImmutableList.of(Method.ANY); + List methods = ImmutableList.of(HttpMethod.GET); Cors cors = Cors.builder() .maxAgeSeconds(100) .origins(origins) diff --git a/pom.xml b/pom.xml index e6fafbe5e7c2..8f56620542bc 100644 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,19 @@ + + + + org.codehaus.mojo + exec-maven-plugin + 1.3.2 + + true + java + + + + org.codehaus.mojo diff --git a/src/main/java/com/google/gcloud/storage/HttpMethod.java b/src/main/java/com/google/gcloud/storage/HttpMethod.java new file mode 100644 index 000000000000..f5889aedae90 --- /dev/null +++ b/src/main/java/com/google/gcloud/storage/HttpMethod.java @@ -0,0 +1,24 @@ +/* + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed 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 com.google.gcloud.storage; + +/** + * + */ +public enum HttpMethod { + GET, HEAD, PUT, POST, DELETE +}