diff --git a/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java index 04ad43f25057..f354072f9fd3 100644 --- a/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java +++ b/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java @@ -2119,6 +2119,70 @@ public static Builder newBuilder() { */ URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... 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 also don't want to require users to explicitly log in. Signing a URL requires + * a service account signer. If an instance of {@link com.google.auth.ServiceAccountSigner} was + * passed to {@link StorageOptions}' builder via {@code setCredentials(Credentials)} or the + * default credentials are being used and the environment variable + * {@code GOOGLE_APPLICATION_CREDENTIALS} is set or your application is running in App Engine, + * then {@code signUrl} will use that credentials to sign the URL. If the credentials passed to + * {@link StorageOptions} do not implement {@link ServiceAccountSigner} (this is the case, for + * instance, for Google Cloud SDK credentials) then {@code signUrl} will throw an + * {@link IllegalStateException} unless an implementation of {@link ServiceAccountSigner} is + * passed using the {@link SignUrlOption#signWith(ServiceAccountSigner)} option. + * + *

A service account signer is looked for in the following order: + *

    + *
  1. The signer passed with the option {@link SignUrlOption#signWith(ServiceAccountSigner)} + *
  2. The credentials passed to {@link StorageOptions} + *
  3. The default credentials, if no credentials were passed to {@link StorageOptions} + *
+ * + *

Example of creating a signed URL that is valid for 2 weeks, using the default credentials + * for signing the URL. + *

 {@code
+   * String bucketName = "my_unique_bucket";
+   * String blobName = "my_blob_name";
+   * URL signedUrl = storage.signUrl(BlobInfo.newBuilder(bucketName, blobName).build(), 14,
+   *     TimeUnit.DAYS);
+   * }
+ * + *

Example of creating a signed URL passing the + * {@link SignUrlOption#signWith(ServiceAccountSigner)} option, that will be used for signing the + * URL. + *

 {@code
+   * String bucketName = "my_unique_bucket";
+   * String blobName = "my_blob_name";
+   * String keyPath = "/path/to/key.json";
+   * URL signedUrl = storage.signUrl(BlobInfo.newBuilder(bucketName, blobName).build(),
+   *     14, TimeUnit.DAYS, SignUrlOption.signWith(
+   *         ServiceAccountCredentials.fromStream(new FileInputStream(keyPath))));
+   * }
+ * + *

Note that the {@link ServiceAccountSigner} may require additional configuration to enable + * URL signing. See the documentation for the implementation for more details.

+ * + * @param url can be customize + * @param blobInfo the blob associated with the signed URL + * @param duration time until the signed URL expires, expressed in {@code unit}. The finest + * granularity supported is 1 second, finer granularities will be truncated + * @param unit time unit of the {@code duration} parameter + * @param options optional URL signing options + * @throws IllegalStateException if {@link SignUrlOption#signWith(ServiceAccountSigner)} was not + * used and no implementation of {@link ServiceAccountSigner} was provided to + * {@link StorageOptions} + * @throws IllegalArgumentException if {@code SignUrlOption.withMd5()} option is used and + * {@code blobInfo.md5()} is {@code null} + * @throws IllegalArgumentException if {@code SignUrlOption.withContentType()} option is used and + * {@code blobInfo.contentType()} is {@code null} + * @throws SigningException if the attempt to sign the URL failed + * @see Signed-URLs + */ + URL signUrl(String url, BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options); + /** * Gets the requested blobs. A batch request is used to perform this call. * diff --git a/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java b/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java index 787388006571..9d3809de23c8 100644 --- a/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java +++ b/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java @@ -84,6 +84,7 @@ final class StorageImpl extends BaseService implements Storage { private static final String EMPTY_BYTE_ARRAY_MD5 = "1B2M2Y8AsgTpgAmY7PhCfg=="; private static final String EMPTY_BYTE_ARRAY_CRC32C = "AAAAAA=="; private static final String PATH_DELIMITER = "/"; + private static final String DEFAULT_STORAGE_HOST = "https://storage.googleapis.com"; private static final Function, Boolean> DELETE_FUNCTION = new Function, Boolean>() { @@ -499,54 +500,63 @@ private BlobWriteChannel writer(BlobInfo blobInfo, BlobTargetOption... options) @Override public URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options) { - EnumMap optionMap = Maps.newEnumMap(SignUrlOption.Option.class); - for (SignUrlOption option : options) { - optionMap.put(option.getOption(), option.getValue()); - } - ServiceAccountSigner credentials = - (ServiceAccountSigner) optionMap.get(SignUrlOption.Option.SERVICE_ACCOUNT_CRED); - if (credentials == null) { - checkState(this.getOptions().getCredentials() instanceof ServiceAccountSigner, - "Signing key was not provided and could not be derived"); - credentials = (ServiceAccountSigner) this.getOptions().getCredentials(); - } - - long expiration = TimeUnit.SECONDS.convert( - getOptions().getClock().millisTime() + unit.toMillis(duration), TimeUnit.MILLISECONDS); - - StringBuilder stPath = new StringBuilder(); - if (!blobInfo.getBucket().startsWith(PATH_DELIMITER)) { - stPath.append(PATH_DELIMITER); - } - stPath.append(blobInfo.getBucket()); - if (!blobInfo.getBucket().endsWith(PATH_DELIMITER)) { - stPath.append(PATH_DELIMITER); - } - if (blobInfo.getName().startsWith(PATH_DELIMITER)) { - stPath.setLength(stPath.length() - 1); - } - - String escapedName = UrlEscapers.urlFragmentEscaper().escape(blobInfo.getName()); - stPath.append(escapedName.replace("?", "%3F")); - - URI path = URI.create(stPath.toString()); - - try { - SignatureInfo signatureInfo = buildSignatureInfo(optionMap, blobInfo, expiration, path); - byte[] signatureBytes = - credentials.sign(signatureInfo.constructUnsignedPayload().getBytes(UTF_8)); - StringBuilder stBuilder = new StringBuilder("https://storage.googleapis.com").append(path); - String signature = - URLEncoder.encode(BaseEncoding.base64().encode(signatureBytes), UTF_8.name()); - stBuilder.append("?GoogleAccessId=").append(credentials.getAccount()); - stBuilder.append("&Expires=").append(expiration); - stBuilder.append("&Signature=").append(signature); - - return new URL(stBuilder.toString()); - - } catch (MalformedURLException | UnsupportedEncodingException ex) { - throw new IllegalStateException(ex); - } + return signUrlOptions(DEFAULT_STORAGE_HOST, blobInfo, duration, unit, options); + } + + @Override + public URL signUrl(String url, BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options) { + return signUrlOptions(url, blobInfo, duration, unit, options); + } + + private URL signUrlOptions(String url, BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options){ + EnumMap optionMap = Maps.newEnumMap(SignUrlOption.Option.class); + for (SignUrlOption option : options) { + optionMap.put(option.getOption(), option.getValue()); + } + ServiceAccountSigner credentials = + (ServiceAccountSigner) optionMap.get(SignUrlOption.Option.SERVICE_ACCOUNT_CRED); + if (credentials == null) { + checkState(this.getOptions().getCredentials() instanceof ServiceAccountSigner, + "Signing key was not provided and could not be derived"); + credentials = (ServiceAccountSigner) this.getOptions().getCredentials(); + } + + long expiration = TimeUnit.SECONDS.convert( + getOptions().getClock().millisTime() + unit.toMillis(duration), TimeUnit.MILLISECONDS); + + StringBuilder stPath = new StringBuilder(); + if (!blobInfo.getBucket().startsWith(PATH_DELIMITER)) { + stPath.append(PATH_DELIMITER); + } + stPath.append(blobInfo.getBucket()); + if (!blobInfo.getBucket().endsWith(PATH_DELIMITER)) { + stPath.append(PATH_DELIMITER); + } + if (blobInfo.getName().startsWith(PATH_DELIMITER)) { + stPath.setLength(stPath.length() - 1); + } + + String escapedName = UrlEscapers.urlFragmentEscaper().escape(blobInfo.getName()); + stPath.append(escapedName.replace("?", "%3F")); + + URI path = URI.create(stPath.toString()); + + try { + SignatureInfo signatureInfo = buildSignatureInfo(optionMap, blobInfo, expiration, path); + byte[] signatureBytes = + credentials.sign(signatureInfo.constructUnsignedPayload().getBytes(UTF_8)); + StringBuilder stBuilder = new StringBuilder(url).append(path); + String signature = + URLEncoder.encode(BaseEncoding.base64().encode(signatureBytes), UTF_8.name()); + stBuilder.append("?GoogleAccessId=").append(credentials.getAccount()); + stBuilder.append("&Expires=").append(expiration); + stBuilder.append("&Signature=").append(signature); + + return new URL(stBuilder.toString()); + + } catch (MalformedURLException | UnsupportedEncodingException ex) { + throw new IllegalStateException(ex); + } } /**