From 327220404d4a377ae40375f9f88509853cb0b6c2 Mon Sep 17 00:00:00 2001 From: praful Date: Mon, 5 Nov 2018 17:35:00 +0530 Subject: [PATCH 1/8] #3140 feature added to change hostname of generated Url --- .../com/google/cloud/storage/Storage.java | 64 +++++++++++ .../com/google/cloud/storage/StorageImpl.java | 106 ++++++++++-------- 2 files changed, 122 insertions(+), 48 deletions(-) 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); + } } /** From 69a5a140c8647ac219cfeeceba660088c81da779 Mon Sep 17 00:00:00 2001 From: Praful Makani Date: Tue, 6 Nov 2018 17:35:59 +0530 Subject: [PATCH 2/8] add test case --- .../google/cloud/storage/StorageImplTest.java | 352 ++++++++++++++++-- 1 file changed, 322 insertions(+), 30 deletions(-) diff --git a/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java b/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java index 574c02ea001f..bb0ecc9a14bc 100644 --- a/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java +++ b/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java @@ -26,36 +26,6 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import com.google.api.client.googleapis.json.GoogleJsonError; -import com.google.api.core.ApiClock; -import com.google.api.gax.paging.Page; -import com.google.api.services.storage.model.Policy.Bindings; -import com.google.api.services.storage.model.StorageObject; -import com.google.api.services.storage.model.TestIamPermissionsResponse; -import com.google.auth.oauth2.ServiceAccountCredentials; -import com.google.cloud.Identity; -import com.google.cloud.Policy; -import com.google.cloud.ReadChannel; -import com.google.cloud.ServiceOptions; -import com.google.cloud.Tuple; -import com.google.cloud.WriteChannel; -import com.google.cloud.storage.Acl.Project; -import com.google.cloud.storage.Acl.Project.ProjectRole; -import com.google.cloud.storage.Acl.Role; -import com.google.cloud.storage.Acl.User; -import com.google.cloud.storage.Storage.BlobSourceOption; -import com.google.cloud.storage.Storage.BlobTargetOption; -import com.google.cloud.storage.Storage.BlobWriteOption; -import com.google.cloud.storage.Storage.BucketSourceOption; -import com.google.cloud.storage.Storage.CopyRequest; -import com.google.cloud.storage.spi.StorageRpcFactory; -import com.google.cloud.storage.spi.v1.RpcBatch; -import com.google.cloud.storage.spi.v1.StorageRpc; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; -import com.google.common.io.BaseEncoding; -import com.google.common.net.UrlEscapers; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -78,7 +48,9 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; + import javax.crypto.spec.SecretKeySpec; + import org.easymock.Capture; import org.easymock.EasyMock; import org.junit.After; @@ -88,6 +60,37 @@ import org.junit.Test; import org.junit.rules.ExpectedException; +import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.api.core.ApiClock; +import com.google.api.gax.paging.Page; +import com.google.api.services.storage.model.Policy.Bindings; +import com.google.api.services.storage.model.StorageObject; +import com.google.api.services.storage.model.TestIamPermissionsResponse; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.Identity; +import com.google.cloud.Policy; +import com.google.cloud.ReadChannel; +import com.google.cloud.ServiceOptions; +import com.google.cloud.Tuple; +import com.google.cloud.WriteChannel; +import com.google.cloud.storage.Acl.Project; +import com.google.cloud.storage.Acl.Project.ProjectRole; +import com.google.cloud.storage.Acl.Role; +import com.google.cloud.storage.Acl.User; +import com.google.cloud.storage.Storage.BlobSourceOption; +import com.google.cloud.storage.Storage.BlobTargetOption; +import com.google.cloud.storage.Storage.BlobWriteOption; +import com.google.cloud.storage.Storage.BucketSourceOption; +import com.google.cloud.storage.Storage.CopyRequest; +import com.google.cloud.storage.spi.StorageRpcFactory; +import com.google.cloud.storage.spi.v1.RpcBatch; +import com.google.cloud.storage.spi.v1.StorageRpc; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.io.BaseEncoding; +import com.google.common.net.UrlEscapers; + public class StorageImplTest { private static final String BUCKET_NAME1 = "b1"; @@ -1633,6 +1636,47 @@ public void testSignUrl() assertTrue( signer.verify(BaseEncoding.base64().decode(URLDecoder.decode(signature, UTF_8.name())))); } + + @Test + public void testSignUrlWithCustomUrl() + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, + UnsupportedEncodingException { + EasyMock.replay(storageRpcMock); + ServiceAccountCredentials credentials = + new ServiceAccountCredentials(null, ACCOUNT, privateKey, null, null); + storage = options.toBuilder().setCredentials(credentials).build().getService(); + URL url = storage.signUrl("https://custom.host.com",BLOB_INFO1, 14, TimeUnit.DAYS); + String stringUrl = url.toString(); + String expectedUrl = + new StringBuilder("https://custom.host.com/") + .append(BUCKET_NAME1) + .append('/') + .append(BLOB_NAME1) + .append("?GoogleAccessId=") + .append(ACCOUNT) + .append("&Expires=") + .append(42L + 1209600) + .append("&Signature=") + .toString(); + assertTrue(stringUrl.startsWith(expectedUrl)); + String signature = stringUrl.substring(expectedUrl.length()); + + StringBuilder signedMessageBuilder = new StringBuilder(); + signedMessageBuilder + .append(HttpMethod.GET) + .append("\n\n\n") + .append(42L + 1209600) + .append("\n/") + .append(BUCKET_NAME1) + .append('/') + .append(BLOB_NAME1); + + Signature signer = Signature.getInstance("SHA256withRSA"); + signer.initVerify(publicKey); + signer.update(signedMessageBuilder.toString().getBytes(UTF_8)); + assertTrue( + signer.verify(BaseEncoding.base64().decode(URLDecoder.decode(signature, UTF_8.name())))); + } @Test public void testSignUrlLeadingSlash() @@ -1675,6 +1719,48 @@ public void testSignUrlLeadingSlash() assertTrue( signer.verify(BaseEncoding.base64().decode(URLDecoder.decode(signature, UTF_8.name())))); } + + @Test + public void testSignUrlLeadingSlashWithCustomUrl() + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, + UnsupportedEncodingException { + String blobName = "/b1"; + EasyMock.replay(storageRpcMock); + ServiceAccountCredentials credentials = + new ServiceAccountCredentials(null, ACCOUNT, privateKey, null, null); + storage = options.toBuilder().setCredentials(credentials).build().getService(); + URL url = + storage.signUrl("https://custom.host.com",BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS); + String escapedBlobName = UrlEscapers.urlFragmentEscaper().escape(blobName); + String stringUrl = url.toString(); + String expectedUrl = + new StringBuilder("https://custom.host.com/") + .append(BUCKET_NAME1) + .append(escapedBlobName) + .append("?GoogleAccessId=") + .append(ACCOUNT) + .append("&Expires=") + .append(42L + 1209600) + .append("&Signature=") + .toString(); + assertTrue(stringUrl.startsWith(expectedUrl)); + String signature = stringUrl.substring(expectedUrl.length()); + + StringBuilder signedMessageBuilder = new StringBuilder(); + signedMessageBuilder + .append(HttpMethod.GET) + .append("\n\n\n") + .append(42L + 1209600) + .append("\n/") + .append(BUCKET_NAME1) + .append(escapedBlobName); + + Signature signer = Signature.getInstance("SHA256withRSA"); + signer.initVerify(publicKey); + signer.update(signedMessageBuilder.toString().getBytes(UTF_8)); + assertTrue( + signer.verify(BaseEncoding.base64().decode(URLDecoder.decode(signature, UTF_8.name())))); + } @Test public void testSignUrlWithOptions() @@ -1727,6 +1813,59 @@ public void testSignUrlWithOptions() assertTrue( signer.verify(BaseEncoding.base64().decode(URLDecoder.decode(signature, UTF_8.name())))); } + + @Test + public void testSignUrlWithOptionsAndCustomUrl() + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, + UnsupportedEncodingException { + EasyMock.replay(storageRpcMock); + ServiceAccountCredentials credentials = + new ServiceAccountCredentials(null, ACCOUNT, privateKey, null, null); + storage = options.toBuilder().setCredentials(credentials).build().getService(); + URL url = + storage.signUrl( + "https://custom.host.com", + BLOB_INFO1, + 14, + TimeUnit.DAYS, + Storage.SignUrlOption.httpMethod(HttpMethod.POST), + Storage.SignUrlOption.withContentType(), + Storage.SignUrlOption.withMd5()); + String stringUrl = url.toString(); + String expectedUrl = + new StringBuilder("https://custom.host.com/") + .append(BUCKET_NAME1) + .append('/') + .append(BLOB_NAME1) + .append("?GoogleAccessId=") + .append(ACCOUNT) + .append("&Expires=") + .append(42L + 1209600) + .append("&Signature=") + .toString(); + assertTrue(stringUrl.startsWith(expectedUrl)); + String signature = stringUrl.substring(expectedUrl.length()); + + StringBuilder signedMessageBuilder = new StringBuilder(); + signedMessageBuilder + .append(HttpMethod.POST) + .append('\n') + .append(BLOB_INFO1.getMd5()) + .append('\n') + .append(BLOB_INFO1.getContentType()) + .append('\n') + .append(42L + 1209600) + .append("\n/") + .append(BUCKET_NAME1) + .append('/') + .append(BLOB_NAME1); + + Signature signer = Signature.getInstance("SHA256withRSA"); + signer.initVerify(publicKey); + signer.update(signedMessageBuilder.toString().getBytes(UTF_8)); + assertTrue( + signer.verify(BaseEncoding.base64().decode(URLDecoder.decode(signature, UTF_8.name())))); + } @Test public void testSignUrlForBlobWithSpecialChars() @@ -1780,6 +1919,58 @@ public void testSignUrlForBlobWithSpecialChars() } } + @Test + public void testSignUrlForBlobWithSpecialCharsAndCustomUrl() + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, + UnsupportedEncodingException { + // List of chars under test were taken from + // https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding_reserved_characters + char[] specialChars = + new char[] { + '!', '#', '$', '&', '\'', '(', ')', '*', '+', ',', ':', ';', '=', '?', '@', '[', ']' + }; + EasyMock.replay(storageRpcMock); + ServiceAccountCredentials credentials = + new ServiceAccountCredentials(null, ACCOUNT, privateKey, null, null); + storage = options.toBuilder().setCredentials(credentials).build().getService(); + + for (char specialChar : specialChars) { + String blobName = "/a" + specialChar + "b"; + URL url = + storage.signUrl("https://custom.host.com",BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS); + String escapedBlobName = + UrlEscapers.urlFragmentEscaper().escape(blobName).replace("?", "%3F"); + String stringUrl = url.toString(); + String expectedUrl = + new StringBuilder("https://custom.host.com/") + .append(BUCKET_NAME1) + .append(escapedBlobName) + .append("?GoogleAccessId=") + .append(ACCOUNT) + .append("&Expires=") + .append(42L + 1209600) + .append("&Signature=") + .toString(); + assertTrue(stringUrl.startsWith(expectedUrl)); + String signature = stringUrl.substring(expectedUrl.length()); + + StringBuilder signedMessageBuilder = new StringBuilder(); + signedMessageBuilder + .append(HttpMethod.GET) + .append("\n\n\n") + .append(42L + 1209600) + .append("\n/") + .append(BUCKET_NAME1) + .append(escapedBlobName); + + Signature signer = Signature.getInstance("SHA256withRSA"); + signer.initVerify(publicKey); + signer.update(signedMessageBuilder.toString().getBytes(UTF_8)); + assertTrue( + signer.verify(BaseEncoding.base64().decode(URLDecoder.decode(signature, UTF_8.name())))); + } + } + @Test public void testSignUrlWithExtHeaders() throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, @@ -1836,7 +2027,65 @@ public void testSignUrlWithExtHeaders() assertTrue( signer.verify(BaseEncoding.base64().decode(URLDecoder.decode(signature, UTF_8.name())))); } + + @Test + public void testSignUrlWithExtHeadersAndCustomUrl() + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, + UnsupportedEncodingException { + EasyMock.replay(storageRpcMock); + ServiceAccountCredentials credentials = + new ServiceAccountCredentials(null, ACCOUNT, privateKey, null, null); + storage = options.toBuilder().setCredentials(credentials).build().getService(); + Map extHeaders = new HashMap(); + extHeaders.put("x-goog-acl", "public-read"); + extHeaders.put("x-goog-meta-owner", "myself"); + URL url = + storage.signUrl( + "https://custom.host.com", + BLOB_INFO1, + 14, + TimeUnit.DAYS, + Storage.SignUrlOption.httpMethod(HttpMethod.PUT), + Storage.SignUrlOption.withContentType(), + Storage.SignUrlOption.withExtHeaders(extHeaders)); + String stringUrl = url.toString(); + String expectedUrl = + new StringBuilder("https://custom.host.com/") + .append(BUCKET_NAME1) + .append('/') + .append(BLOB_NAME1) + .append("?GoogleAccessId=") + .append(ACCOUNT) + .append("&Expires=") + .append(42L + 1209600) + .append("&Signature=") + .toString(); + assertTrue(stringUrl.startsWith(expectedUrl)); + String signature = stringUrl.substring(expectedUrl.length()); + + StringBuilder signedMessageBuilder = new StringBuilder(); + signedMessageBuilder + .append(HttpMethod.PUT) + .append('\n') + .append('\n') + .append(BLOB_INFO1.getContentType()) + .append('\n') + .append(42L + 1209600) + .append('\n') + .append("x-goog-acl:public-read\n") + .append("x-goog-meta-owner:myself\n") + .append('/') + .append(BUCKET_NAME1) + .append('/') + .append(BLOB_NAME1); + Signature signer = Signature.getInstance("SHA256withRSA"); + signer.initVerify(publicKey); + signer.update(signedMessageBuilder.toString().getBytes(UTF_8)); + assertTrue( + signer.verify(BaseEncoding.base64().decode(URLDecoder.decode(signature, UTF_8.name())))); + } + @Test public void testSignUrlForBlobWithSlashes() throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, @@ -1879,6 +2128,49 @@ public void testSignUrlForBlobWithSlashes() assertTrue( signer.verify(BaseEncoding.base64().decode(URLDecoder.decode(signature, UTF_8.name())))); } + + @Test + public void testSignUrlForBlobWithSlashesAndCustomUrl() + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, + UnsupportedEncodingException { + EasyMock.replay(storageRpcMock); + ServiceAccountCredentials credentials = + new ServiceAccountCredentials(null, ACCOUNT, privateKey, null, null); + storage = options.toBuilder().setCredentials(credentials).build().getService(); + + String blobName = "/foo/bar/baz #%20other cool stuff.txt"; + URL url = + storage.signUrl("https://custom.host.com", BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS); + String escapedBlobName = UrlEscapers.urlFragmentEscaper().escape(blobName); + String stringUrl = url.toString(); + String expectedUrl = + new StringBuilder("https://custom.host.com/") + .append(BUCKET_NAME1) + .append(escapedBlobName) + .append("?GoogleAccessId=") + .append(ACCOUNT) + .append("&Expires=") + .append(42L + 1209600) + .append("&Signature=") + .toString(); + assertTrue(stringUrl.startsWith(expectedUrl)); + String signature = stringUrl.substring(expectedUrl.length()); + + StringBuilder signedMessageBuilder = new StringBuilder(); + signedMessageBuilder + .append(HttpMethod.GET) + .append("\n\n\n") + .append(42L + 1209600) + .append("\n/") + .append(BUCKET_NAME1) + .append(escapedBlobName); + + Signature signer = Signature.getInstance("SHA256withRSA"); + signer.initVerify(publicKey); + signer.update(signedMessageBuilder.toString().getBytes(UTF_8)); + assertTrue( + signer.verify(BaseEncoding.base64().decode(URLDecoder.decode(signature, UTF_8.name())))); + } @Test public void testGetAllArray() { From 696408c280f4659fa5207cef5829d3887c14391d Mon Sep 17 00:00:00 2001 From: Praful Makani Date: Mon, 12 Nov 2018 20:29:58 +0530 Subject: [PATCH 3/8] removed overload method and add method withExtHostName in SignUrlOption --- .../com/google/cloud/storage/Storage.java | 74 ++---------- .../com/google/cloud/storage/StorageImpl.java | 107 +++++++++--------- .../google/cloud/storage/StorageImplTest.java | 16 +-- 3 files changed, 70 insertions(+), 127 deletions(-) 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 f354072f9fd3..f670f513c8b9 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 @@ -892,7 +892,7 @@ class SignUrlOption implements Serializable { private final Object value; enum Option { - HTTP_METHOD, CONTENT_TYPE, MD5, EXT_HEADERS, SERVICE_ACCOUNT_CRED + HTTP_METHOD, CONTENT_TYPE, MD5, EXT_HEADERS, SERVICE_ACCOUNT_CRED,EXT_HOST } private SignUrlOption(Option option, Object value) { @@ -953,6 +953,13 @@ public static SignUrlOption withExtHeaders(Map extHeaders) { public static SignUrlOption signWith(ServiceAccountSigner signer) { return new SignUrlOption(Option.SERVICE_ACCOUNT_CRED, signer); } + + /** + * Provides a host name to sign the URL. If not provided than host name will be default + */ + public static SignUrlOption withExtHostName(String extHostName){ + return new SignUrlOption(Option.EXT_HOST, extHostName); + } } /** @@ -2107,6 +2114,7 @@ public static Builder newBuilder() { * 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 + * {@code SignUrlOption.withExtHostName()} option is used for external host name of signed url * @throws IllegalStateException if {@link SignUrlOption#signWith(ServiceAccountSigner)} was not * used and no implementation of {@link ServiceAccountSigner} was provided to * {@link StorageOptions} @@ -2119,70 +2127,6 @@ 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 9d3809de23c8..f359e3a7d5c6 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 @@ -33,6 +33,22 @@ import static com.google.common.base.Preconditions.checkState; import static java.nio.charset.StandardCharsets.UTF_8; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + import com.google.api.gax.paging.Page; import com.google.api.services.storage.model.BucketAccessControl; import com.google.api.services.storage.model.ObjectAccessControl; @@ -62,21 +78,6 @@ import com.google.common.io.BaseEncoding; import com.google.common.net.UrlEscapers; import com.google.common.primitives.Ints; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.net.URLEncoder; -import java.util.Arrays; -import java.util.Collections; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; final class StorageImpl extends BaseService implements Storage { @@ -500,52 +501,50 @@ private BlobWriteChannel writer(BlobInfo blobInfo, BlobTargetOption... options) @Override public URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options) { - 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); + 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(); + } - 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); - } + long expiration = TimeUnit.SECONDS.convert( + getOptions().getClock().millisTime() + unit.toMillis(duration), TimeUnit.MILLISECONDS); - String escapedName = UrlEscapers.urlFragmentEscaper().escape(blobInfo.getName()); - stPath.append(escapedName.replace("?", "%3F")); + 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); + } - URI path = URI.create(stPath.toString()); + String escapedName = UrlEscapers.urlFragmentEscaper().escape(blobInfo.getName()); + stPath.append(escapedName.replace("?", "%3F")); + + URI path = URI.create(stPath.toString()); - try { + try { SignatureInfo signatureInfo = buildSignatureInfo(optionMap, blobInfo, expiration, path); byte[] signatureBytes = credentials.sign(signatureInfo.constructUnsignedPayload().getBytes(UTF_8)); - StringBuilder stBuilder = new StringBuilder(url).append(path); + StringBuilder stBuilder = new StringBuilder(); + if(optionMap.get(SignUrlOption.Option.EXT_HOST) == null){ + stBuilder.append(DEFAULT_STORAGE_HOST).append(path); + } + else{ + stBuilder.append(optionMap.get(SignUrlOption.Option.EXT_HOST).toString()).append(path); + } + String signature = URLEncoder.encode(BaseEncoding.base64().encode(signatureBytes), UTF_8.name()); stBuilder.append("?GoogleAccessId=").append(credentials.getAccount()); @@ -556,7 +555,7 @@ private URL signUrlOptions(String url, BlobInfo blobInfo, long duration, TimeUni } catch (MalformedURLException | UnsupportedEncodingException ex) { throw new IllegalStateException(ex); - } + } } /** diff --git a/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java b/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java index bb0ecc9a14bc..3565df009e33 100644 --- a/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java +++ b/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java @@ -1645,7 +1645,7 @@ public void testSignUrlWithCustomUrl() ServiceAccountCredentials credentials = new ServiceAccountCredentials(null, ACCOUNT, privateKey, null, null); storage = options.toBuilder().setCredentials(credentials).build().getService(); - URL url = storage.signUrl("https://custom.host.com",BLOB_INFO1, 14, TimeUnit.DAYS); + URL url = storage.signUrl(BLOB_INFO1, 14, TimeUnit.DAYS, Storage.SignUrlOption.withExtHostName("https://custom.host.com")); String stringUrl = url.toString(); String expectedUrl = new StringBuilder("https://custom.host.com/") @@ -1730,7 +1730,7 @@ public void testSignUrlLeadingSlashWithCustomUrl() new ServiceAccountCredentials(null, ACCOUNT, privateKey, null, null); storage = options.toBuilder().setCredentials(credentials).build().getService(); URL url = - storage.signUrl("https://custom.host.com",BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS); + storage.signUrl(BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS, Storage.SignUrlOption.withExtHostName("https://custom.host.com")); String escapedBlobName = UrlEscapers.urlFragmentEscaper().escape(blobName); String stringUrl = url.toString(); String expectedUrl = @@ -1824,13 +1824,13 @@ public void testSignUrlWithOptionsAndCustomUrl() storage = options.toBuilder().setCredentials(credentials).build().getService(); URL url = storage.signUrl( - "https://custom.host.com", BLOB_INFO1, 14, TimeUnit.DAYS, Storage.SignUrlOption.httpMethod(HttpMethod.POST), Storage.SignUrlOption.withContentType(), - Storage.SignUrlOption.withMd5()); + Storage.SignUrlOption.withMd5(), + Storage.SignUrlOption.withExtHostName("https://custom.host.com")); String stringUrl = url.toString(); String expectedUrl = new StringBuilder("https://custom.host.com/") @@ -1937,7 +1937,7 @@ public void testSignUrlForBlobWithSpecialCharsAndCustomUrl() for (char specialChar : specialChars) { String blobName = "/a" + specialChar + "b"; URL url = - storage.signUrl("https://custom.host.com",BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS); + storage.signUrl(BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS, Storage.SignUrlOption.withExtHostName("https://custom.host.com")); String escapedBlobName = UrlEscapers.urlFragmentEscaper().escape(blobName).replace("?", "%3F"); String stringUrl = url.toString(); @@ -2041,13 +2041,13 @@ public void testSignUrlWithExtHeadersAndCustomUrl() extHeaders.put("x-goog-meta-owner", "myself"); URL url = storage.signUrl( - "https://custom.host.com", BLOB_INFO1, 14, TimeUnit.DAYS, Storage.SignUrlOption.httpMethod(HttpMethod.PUT), Storage.SignUrlOption.withContentType(), - Storage.SignUrlOption.withExtHeaders(extHeaders)); + Storage.SignUrlOption.withExtHeaders(extHeaders), + Storage.SignUrlOption.withExtHostName("https://custom.host.com")); String stringUrl = url.toString(); String expectedUrl = new StringBuilder("https://custom.host.com/") @@ -2140,7 +2140,7 @@ public void testSignUrlForBlobWithSlashesAndCustomUrl() String blobName = "/foo/bar/baz #%20other cool stuff.txt"; URL url = - storage.signUrl("https://custom.host.com", BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS); + storage.signUrl(BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS, Storage.SignUrlOption.withExtHostName("https://custom.host.com")); String escapedBlobName = UrlEscapers.urlFragmentEscaper().escape(blobName); String stringUrl = url.toString(); String expectedUrl = From 2b5439360dc3d5b9c4e1d555a1fe439bcc7249a0 Mon Sep 17 00:00:00 2001 From: Praful Makani Date: Tue, 13 Nov 2018 14:02:59 +0530 Subject: [PATCH 4/8] rename EX_HOST to SERVICE_ENDPOINT,rename withExtHostName to withHostName and used google-java-formatter --- .../com/google/cloud/storage/Storage.java | 6 +- .../com/google/cloud/storage/StorageImpl.java | 141 +++++++++--------- .../google/cloud/storage/StorageImplTest.java | 24 +-- 3 files changed, 85 insertions(+), 86 deletions(-) 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 f670f513c8b9..047b84c5aae7 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 @@ -892,7 +892,7 @@ class SignUrlOption implements Serializable { private final Object value; enum Option { - HTTP_METHOD, CONTENT_TYPE, MD5, EXT_HEADERS, SERVICE_ACCOUNT_CRED,EXT_HOST + HTTP_METHOD, CONTENT_TYPE, MD5, EXT_HEADERS, SERVICE_ACCOUNT_CRED,SERVICE_ENDPOINT } private SignUrlOption(Option option, Object value) { @@ -957,8 +957,8 @@ public static SignUrlOption signWith(ServiceAccountSigner signer) { /** * Provides a host name to sign the URL. If not provided than host name will be default */ - public static SignUrlOption withExtHostName(String extHostName){ - return new SignUrlOption(Option.EXT_HOST, extHostName); + public static SignUrlOption withHostName(String hostName){ + return new SignUrlOption(Option.SERVICE_ENDPOINT, hostName); } } 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 f359e3a7d5c6..d2ab2bbc43fb 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 @@ -33,22 +33,6 @@ import static com.google.common.base.Preconditions.checkState; import static java.nio.charset.StandardCharsets.UTF_8; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.net.URLEncoder; -import java.util.Arrays; -import java.util.Collections; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; - import com.google.api.gax.paging.Page; import com.google.api.services.storage.model.BucketAccessControl; import com.google.api.services.storage.model.ObjectAccessControl; @@ -78,6 +62,21 @@ import com.google.common.io.BaseEncoding; import com.google.common.net.UrlEscapers; import com.google.common.primitives.Ints; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; final class StorageImpl extends BaseService implements Storage { @@ -501,61 +500,61 @@ 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(); - if(optionMap.get(SignUrlOption.Option.EXT_HOST) == null){ - stBuilder.append(DEFAULT_STORAGE_HOST).append(path); - } - else{ - stBuilder.append(optionMap.get(SignUrlOption.Option.EXT_HOST).toString()).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); - } + 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(); + if (optionMap.get(SignUrlOption.Option.SERVICE_ENDPOINT) == null){ + stBuilder.append(DEFAULT_STORAGE_HOST).append(path); + } + else { + stBuilder.append(optionMap.get(SignUrlOption.Option.SERVICE_ENDPOINT)).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); + } } /** diff --git a/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java b/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java index 3565df009e33..426cc6e68495 100644 --- a/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java +++ b/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java @@ -1645,10 +1645,10 @@ public void testSignUrlWithCustomUrl() ServiceAccountCredentials credentials = new ServiceAccountCredentials(null, ACCOUNT, privateKey, null, null); storage = options.toBuilder().setCredentials(credentials).build().getService(); - URL url = storage.signUrl(BLOB_INFO1, 14, TimeUnit.DAYS, Storage.SignUrlOption.withExtHostName("https://custom.host.com")); + URL url = storage.signUrl(BLOB_INFO1, 14, TimeUnit.DAYS, Storage.SignUrlOption.withHostName("https://example.com")); String stringUrl = url.toString(); String expectedUrl = - new StringBuilder("https://custom.host.com/") + new StringBuilder("https://example.com/") .append(BUCKET_NAME1) .append('/') .append(BLOB_NAME1) @@ -1730,11 +1730,11 @@ public void testSignUrlLeadingSlashWithCustomUrl() new ServiceAccountCredentials(null, ACCOUNT, privateKey, null, null); storage = options.toBuilder().setCredentials(credentials).build().getService(); URL url = - storage.signUrl(BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS, Storage.SignUrlOption.withExtHostName("https://custom.host.com")); + storage.signUrl(BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS, Storage.SignUrlOption.withHostName("https://example.com")); String escapedBlobName = UrlEscapers.urlFragmentEscaper().escape(blobName); String stringUrl = url.toString(); String expectedUrl = - new StringBuilder("https://custom.host.com/") + new StringBuilder("https://example.com/") .append(BUCKET_NAME1) .append(escapedBlobName) .append("?GoogleAccessId=") @@ -1830,10 +1830,10 @@ public void testSignUrlWithOptionsAndCustomUrl() Storage.SignUrlOption.httpMethod(HttpMethod.POST), Storage.SignUrlOption.withContentType(), Storage.SignUrlOption.withMd5(), - Storage.SignUrlOption.withExtHostName("https://custom.host.com")); + Storage.SignUrlOption.withHostName("https://example.com")); String stringUrl = url.toString(); String expectedUrl = - new StringBuilder("https://custom.host.com/") + new StringBuilder("https://example.com/") .append(BUCKET_NAME1) .append('/') .append(BLOB_NAME1) @@ -1937,12 +1937,12 @@ public void testSignUrlForBlobWithSpecialCharsAndCustomUrl() for (char specialChar : specialChars) { String blobName = "/a" + specialChar + "b"; URL url = - storage.signUrl(BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS, Storage.SignUrlOption.withExtHostName("https://custom.host.com")); + storage.signUrl(BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS, Storage.SignUrlOption.withHostName("https://example.com")); String escapedBlobName = UrlEscapers.urlFragmentEscaper().escape(blobName).replace("?", "%3F"); String stringUrl = url.toString(); String expectedUrl = - new StringBuilder("https://custom.host.com/") + new StringBuilder("https://example.com/") .append(BUCKET_NAME1) .append(escapedBlobName) .append("?GoogleAccessId=") @@ -2047,10 +2047,10 @@ public void testSignUrlWithExtHeadersAndCustomUrl() Storage.SignUrlOption.httpMethod(HttpMethod.PUT), Storage.SignUrlOption.withContentType(), Storage.SignUrlOption.withExtHeaders(extHeaders), - Storage.SignUrlOption.withExtHostName("https://custom.host.com")); + Storage.SignUrlOption.withHostName("https://example.com")); String stringUrl = url.toString(); String expectedUrl = - new StringBuilder("https://custom.host.com/") + new StringBuilder("https://example.com/") .append(BUCKET_NAME1) .append('/') .append(BLOB_NAME1) @@ -2140,11 +2140,11 @@ public void testSignUrlForBlobWithSlashesAndCustomUrl() String blobName = "/foo/bar/baz #%20other cool stuff.txt"; URL url = - storage.signUrl(BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS, Storage.SignUrlOption.withExtHostName("https://custom.host.com")); + storage.signUrl(BlobInfo.newBuilder(BUCKET_NAME1, blobName).build(), 14, TimeUnit.DAYS, Storage.SignUrlOption.withHostName("https://example.com")); String escapedBlobName = UrlEscapers.urlFragmentEscaper().escape(blobName); String stringUrl = url.toString(); String expectedUrl = - new StringBuilder("https://custom.host.com/") + new StringBuilder("https://example.com/") .append(BUCKET_NAME1) .append(escapedBlobName) .append("?GoogleAccessId=") From 0913306ec244572635b70ece0031c0f5da4280db Mon Sep 17 00:00:00 2001 From: Praful Makani Date: Sat, 17 Nov 2018 18:37:06 +0530 Subject: [PATCH 5/8] update as per feedback --- .../com/google/cloud/storage/Storage.java | 8 +- .../com/google/cloud/storage/StorageImpl.java | 11 ++- .../google/cloud/storage/StorageImplTest.java | 74 +++++++++---------- 3 files changed, 48 insertions(+), 45 deletions(-) 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 047b84c5aae7..beff9ce4c59a 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 @@ -892,7 +892,7 @@ class SignUrlOption implements Serializable { private final Object value; enum Option { - HTTP_METHOD, CONTENT_TYPE, MD5, EXT_HEADERS, SERVICE_ACCOUNT_CRED,SERVICE_ENDPOINT + HTTP_METHOD, CONTENT_TYPE, MD5, EXT_HEADERS, SERVICE_ACCOUNT_CRED, HOST_NAME } private SignUrlOption(Option option, Object value) { @@ -955,10 +955,10 @@ public static SignUrlOption signWith(ServiceAccountSigner signer) { } /** - * Provides a host name to sign the URL. If not provided than host name will be default + * Provides a host name to sign the URL. If not provided than default host name will be used. */ public static SignUrlOption withHostName(String hostName){ - return new SignUrlOption(Option.SERVICE_ENDPOINT, hostName); + return new SignUrlOption(Option.HOST_NAME, hostName); } } @@ -2114,7 +2114,7 @@ public static Builder newBuilder() { * 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 - * {@code SignUrlOption.withExtHostName()} option is used for external host name of signed url + * {@code SignUrlOption.withHostName()} option to sign url with custom hostname. * @throws IllegalStateException if {@link SignUrlOption#signWith(ServiceAccountSigner)} was not * used and no implementation of {@link ServiceAccountSigner} was provided to * {@link StorageOptions} 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 d2ab2bbc43fb..6ff06b6b04ea 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,7 +84,10 @@ 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"; + /** + * SignedUrls uses GCS XML API endpoint. + */ + private static final String STORAGE_XML_HOST_NAME = "https://storage.googleapis.com"; private static final Function, Boolean> DELETE_FUNCTION = new Function, Boolean>() { @@ -537,11 +540,11 @@ public URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOptio byte[] signatureBytes = credentials.sign(signatureInfo.constructUnsignedPayload().getBytes(UTF_8)); StringBuilder stBuilder = new StringBuilder(); - if (optionMap.get(SignUrlOption.Option.SERVICE_ENDPOINT) == null){ - stBuilder.append(DEFAULT_STORAGE_HOST).append(path); + if (optionMap.get(SignUrlOption.Option.HOST_NAME) == null) { + stBuilder.append(STORAGE_XML_HOST_NAME).append(path); } else { - stBuilder.append(optionMap.get(SignUrlOption.Option.SERVICE_ENDPOINT)).append(path); + stBuilder.append(optionMap.get(SignUrlOption.Option.HOST_NAME)).append(path); } String signature = diff --git a/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java b/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java index 426cc6e68495..cea262cfce35 100644 --- a/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java +++ b/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java @@ -26,6 +26,37 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.api.core.ApiClock; +import com.google.api.gax.paging.Page; +import com.google.api.services.storage.model.Policy.Bindings; +import com.google.api.services.storage.model.StorageObject; +import com.google.api.services.storage.model.TestIamPermissionsResponse; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.Identity; +import com.google.cloud.Policy; +import com.google.cloud.ReadChannel; +import com.google.cloud.ServiceOptions; +import com.google.cloud.Tuple; +import com.google.cloud.WriteChannel; +import com.google.cloud.storage.Acl.Project; +import com.google.cloud.storage.Acl.Project.ProjectRole; +import com.google.cloud.storage.Acl.Role; +import com.google.cloud.storage.Acl.User; +import com.google.cloud.storage.Storage.BlobSourceOption; +import com.google.cloud.storage.Storage.BlobTargetOption; +import com.google.cloud.storage.Storage.BlobWriteOption; +import com.google.cloud.storage.Storage.BucketSourceOption; +import com.google.cloud.storage.Storage.CopyRequest; +import com.google.cloud.storage.spi.StorageRpcFactory; +import com.google.cloud.storage.spi.v1.RpcBatch; +import com.google.cloud.storage.spi.v1.StorageRpc; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.io.BaseEncoding; +import com.google.common.net.UrlEscapers; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -60,37 +91,6 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import com.google.api.client.googleapis.json.GoogleJsonError; -import com.google.api.core.ApiClock; -import com.google.api.gax.paging.Page; -import com.google.api.services.storage.model.Policy.Bindings; -import com.google.api.services.storage.model.StorageObject; -import com.google.api.services.storage.model.TestIamPermissionsResponse; -import com.google.auth.oauth2.ServiceAccountCredentials; -import com.google.cloud.Identity; -import com.google.cloud.Policy; -import com.google.cloud.ReadChannel; -import com.google.cloud.ServiceOptions; -import com.google.cloud.Tuple; -import com.google.cloud.WriteChannel; -import com.google.cloud.storage.Acl.Project; -import com.google.cloud.storage.Acl.Project.ProjectRole; -import com.google.cloud.storage.Acl.Role; -import com.google.cloud.storage.Acl.User; -import com.google.cloud.storage.Storage.BlobSourceOption; -import com.google.cloud.storage.Storage.BlobTargetOption; -import com.google.cloud.storage.Storage.BlobWriteOption; -import com.google.cloud.storage.Storage.BucketSourceOption; -import com.google.cloud.storage.Storage.CopyRequest; -import com.google.cloud.storage.spi.StorageRpcFactory; -import com.google.cloud.storage.spi.v1.RpcBatch; -import com.google.cloud.storage.spi.v1.StorageRpc; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; -import com.google.common.io.BaseEncoding; -import com.google.common.net.UrlEscapers; - public class StorageImplTest { private static final String BUCKET_NAME1 = "b1"; @@ -1638,7 +1638,7 @@ public void testSignUrl() } @Test - public void testSignUrlWithCustomUrl() + public void testSignUrlWithHostName() throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException { EasyMock.replay(storageRpcMock); @@ -1721,7 +1721,7 @@ public void testSignUrlLeadingSlash() } @Test - public void testSignUrlLeadingSlashWithCustomUrl() + public void testSignUrlLeadingSlashWithHostName() throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException { String blobName = "/b1"; @@ -1815,7 +1815,7 @@ public void testSignUrlWithOptions() } @Test - public void testSignUrlWithOptionsAndCustomUrl() + public void testSignUrlWithOptionsAndHostName() throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException { EasyMock.replay(storageRpcMock); @@ -1920,7 +1920,7 @@ public void testSignUrlForBlobWithSpecialChars() } @Test - public void testSignUrlForBlobWithSpecialCharsAndCustomUrl() + public void testSignUrlForBlobWithSpecialCharsAndHostName() throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException { // List of chars under test were taken from @@ -2029,7 +2029,7 @@ public void testSignUrlWithExtHeaders() } @Test - public void testSignUrlWithExtHeadersAndCustomUrl() + public void testSignUrlWithExtHeadersAndHostName() throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException { EasyMock.replay(storageRpcMock); @@ -2130,7 +2130,7 @@ public void testSignUrlForBlobWithSlashes() } @Test - public void testSignUrlForBlobWithSlashesAndCustomUrl() + public void testSignUrlForBlobWithSlashesAndHostName() throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException { EasyMock.replay(storageRpcMock); From f3c3bf75d3892828cbf94f196a360231cc25adb3 Mon Sep 17 00:00:00 2001 From: Praful Makani Date: Sat, 17 Nov 2018 18:43:23 +0530 Subject: [PATCH 6/8] fix formatting --- .../src/main/java/com/google/cloud/storage/StorageImpl.java | 4 ++-- .../test/java/com/google/cloud/storage/StorageImplTest.java | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) 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 6ff06b6b04ea..41fc0c3bbeb8 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 @@ -541,10 +541,10 @@ public URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOptio credentials.sign(signatureInfo.constructUnsignedPayload().getBytes(UTF_8)); StringBuilder stBuilder = new StringBuilder(); if (optionMap.get(SignUrlOption.Option.HOST_NAME) == null) { - stBuilder.append(STORAGE_XML_HOST_NAME).append(path); + stBuilder.append(STORAGE_XML_HOST_NAME).append(path); } else { - stBuilder.append(optionMap.get(SignUrlOption.Option.HOST_NAME)).append(path); + stBuilder.append(optionMap.get(SignUrlOption.Option.HOST_NAME)).append(path); } String signature = diff --git a/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java b/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java index cea262cfce35..18621928434d 100644 --- a/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java +++ b/google-cloud-clients/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java @@ -56,7 +56,6 @@ import com.google.common.collect.Iterables; import com.google.common.io.BaseEncoding; import com.google.common.net.UrlEscapers; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -79,9 +78,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; - import javax.crypto.spec.SecretKeySpec; - import org.easymock.Capture; import org.easymock.EasyMock; import org.junit.After; From 3194704b569f078f6eb70f95a5c81e720d2851c5 Mon Sep 17 00:00:00 2001 From: Praful Makani Date: Tue, 20 Nov 2018 19:13:21 +0530 Subject: [PATCH 7/8] update comments --- .../src/main/java/com/google/cloud/storage/Storage.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 beff9ce4c59a..8a05d024a930 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 @@ -955,7 +955,7 @@ public static SignUrlOption signWith(ServiceAccountSigner signer) { } /** - * Provides a host name to sign the URL. If not provided than default host name will be used. + * Use a different host name than the default host name 'storage.googleapis.com' */ public static SignUrlOption withHostName(String hostName){ return new SignUrlOption(Option.HOST_NAME, hostName); @@ -2114,7 +2114,8 @@ public static Builder newBuilder() { * 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 - * {@code SignUrlOption.withHostName()} option to sign url with custom hostname. + * {@code SignUrlOption.withHostName()} option to set a custom host name instead of using + * https://storage.googleapis.com. * @throws IllegalStateException if {@link SignUrlOption#signWith(ServiceAccountSigner)} was not * used and no implementation of {@link ServiceAccountSigner} was provided to * {@link StorageOptions} From 17c799fd9ba6a632dd73be1334929787e54d1ad7 Mon Sep 17 00:00:00 2001 From: Praful Makani Date: Tue, 20 Nov 2018 19:34:46 +0530 Subject: [PATCH 8/8] update Signed URLs Comment --- .../src/main/java/com/google/cloud/storage/StorageImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 41fc0c3bbeb8..0c7dd5f5e558 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 @@ -85,7 +85,7 @@ final class StorageImpl extends BaseService implements Storage { private static final String EMPTY_BYTE_ARRAY_CRC32C = "AAAAAA=="; private static final String PATH_DELIMITER = "/"; /** - * SignedUrls uses GCS XML API endpoint. + * Signed URLs are only supported through the GCS XML API endpoint. */ private static final String STORAGE_XML_HOST_NAME = "https://storage.googleapis.com";