diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/SignatureInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/SignatureInfo.java index f26157d9d0..03630c61a4 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/SignatureInfo.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/SignatureInfo.java @@ -169,7 +169,9 @@ private String constructV4CanonicalRequestHash() { canonicalRequest .append(serializer.serializeHeaderNames(canonicalizedExtensionHeaders)) .append(COMPONENT_SEPARATOR); - canonicalRequest.append("UNSIGNED-PAYLOAD"); + + String userProvidedHash = canonicalizedExtensionHeaders.get("X-Goog-Content-SHA256"); + canonicalRequest.append(userProvidedHash == null ? "UNSIGNED-PAYLOAD" : userProvidedHash); return Hashing.sha256() .hashString(canonicalRequest.toString(), StandardCharsets.UTF_8) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java index 77c88e97c6..98f304dd89 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java @@ -161,6 +161,21 @@ public String getSelector() { } } + enum UriScheme { + HTTP("http"), + HTTPS("https"); + + private final String scheme; + + UriScheme(String scheme) { + this.scheme = scheme; + } + + public String getScheme() { + return scheme; + } + } + /** Class for specifying bucket target options. */ class BucketTargetOption extends Option { @@ -1038,6 +1053,7 @@ enum Option { HOST_NAME, PATH_STYLE, VIRTUAL_HOSTED_STYLE, + BUCKET_BOUND_HOST_NAME, QUERY_PARAMS } @@ -1160,6 +1176,44 @@ public static SignUrlOption withPathStyle() { return new SignUrlOption(Option.PATH_STYLE, ""); } + /** + * Use a bucket-bound hostname, which replaces the storage.googleapis.com host with the name of + * a CNAME bucket, e.g. a bucket named 'gcs-subdomain.my.domain.tld', or a Google Cloud Load + * Balancer which routes to a bucket you own, e.g. 'my-load-balancer-domain.tld'. Note that this + * cannot be used alongside {@code withVirtualHostedStyle()} or {@code withPathStyle()}. This + * method signature uses HTTP for the URI scheme, and is equivalent to calling {@code + * withBucketBoundHostname("...", UriScheme.HTTP).} + * + * @see CNAME + * Redirects + * @see + * GCLB Redirects + */ + public static SignUrlOption withBucketBoundHostname(String bucketBoundHostname) { + return withBucketBoundHostname(bucketBoundHostname, UriScheme.HTTP); + } + + /** + * Use a bucket-bound hostname, which replaces the storage.googleapis.com host with the name of + * a CNAME bucket, e.g. a bucket named 'gcs-subdomain.my.domain.tld', or a Google Cloud Load + * Balancer which routes to a bucket you own, e.g. 'my-load-balancer-domain.tld'. Note that this + * cannot be used alongside {@code withVirtualHostedStyle()} or {@code withPathStyle()}. The + * bucket name itself should not include the URI scheme (http or https), so it is specified via + * a local enum. + * + * @see CNAME + * Redirects + * @see + * GCLB Redirects + */ + public static SignUrlOption withBucketBoundHostname( + String bucketBoundHostname, UriScheme uriScheme) { + return new SignUrlOption( + Option.BUCKET_BOUND_HOST_NAME, uriScheme.getScheme() + "://" + bucketBoundHostname); + } + /** * Use if the URL should contain additional query parameters. * diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java index 63c1985570..d76aa0d298 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java @@ -660,8 +660,10 @@ public URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOptio checkArgument( !(optionMap.containsKey(SignUrlOption.Option.VIRTUAL_HOSTED_STYLE) - && optionMap.containsKey(SignUrlOption.Option.PATH_STYLE)), - "Cannot specify both the VIRTUAL_HOSTED_STYLE and PATH_STYLE SignUrlOptions together."); + && optionMap.containsKey(SignUrlOption.Option.PATH_STYLE) + && optionMap.containsKey(SignUrlOption.Option.BUCKET_BOUND_HOST_NAME)), + "Only one of VIRTUAL_HOSTED_STYLE, PATH_STYLE, or BUCKET_BOUND_HOST_NAME SignUrlOptions can be" + + " specified."); String bucketName = slashlessBucketNameFromBlobInfo(blobInfo); String escapedBlobName = ""; @@ -676,6 +678,10 @@ public URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOptio ? STORAGE_XML_URI_SCHEME + "://" + getBaseStorageHostName(optionMap) : STORAGE_XML_URI_SCHEME + "://" + bucketName + "." + getBaseStorageHostName(optionMap); + if (optionMap.containsKey(SignUrlOption.Option.BUCKET_BOUND_HOST_NAME)) { + storageXmlHostName = (String) optionMap.get(SignUrlOption.Option.BUCKET_BOUND_HOST_NAME); + } + String stPath = usePathStyle ? constructResourceUriPath(bucketName, escapedBlobName, optionMap) @@ -753,9 +759,7 @@ private String constructResourceUriPath( } return pathBuilder.toString(); } - if (!escapedBlobName.startsWith(PATH_DELIMITER)) { - pathBuilder.append(PATH_DELIMITER); - } + pathBuilder.append(PATH_DELIMITER); pathBuilder.append(escapedBlobName); return pathBuilder.toString(); } @@ -776,7 +780,8 @@ private SignUrlOption.SignatureVersion getPreferredSignatureVersion( private boolean shouldUsePathStyleForSignedUrl(EnumMap optionMap) { // TODO(#6362): If we decide to change the default style used to generate URLs, switch this // logic to return false unless PATH_STYLE was explicitly specified. - if (optionMap.containsKey(SignUrlOption.Option.VIRTUAL_HOSTED_STYLE)) { + if (optionMap.containsKey(SignUrlOption.Option.VIRTUAL_HOSTED_STYLE) + || optionMap.containsKey(SignUrlOption.Option.BUCKET_BOUND_HOST_NAME)) { return false; } return true; @@ -836,7 +841,8 @@ private SignatureInfo buildSignatureInfo( extHeadersBuilder.put( "host", slashlessBucketNameFromBlobInfo(blobInfo) + "." + getBaseStorageHostName(optionMap)); - } else if (optionMap.containsKey(SignUrlOption.Option.HOST_NAME)) { + } else if (optionMap.containsKey(SignUrlOption.Option.HOST_NAME) + || optionMap.containsKey(SignUrlOption.Option.BUCKET_BOUND_HOST_NAME)) { extHeadersBuilder.put("host", getBaseStorageHostName(optionMap)); } } @@ -868,9 +874,14 @@ private String slashlessBucketNameFromBlobInfo(BlobInfo blobInfo) { /** Returns the hostname used to send requests to Cloud Storage, e.g. "storage.googleapis.com". */ private String getBaseStorageHostName(Map optionMap) { String specifiedBaseHostName = (String) optionMap.get(SignUrlOption.Option.HOST_NAME); + String bucketBoundHostName = + (String) optionMap.get(SignUrlOption.Option.BUCKET_BOUND_HOST_NAME); if (!Strings.isNullOrEmpty(specifiedBaseHostName)) { return specifiedBaseHostName.replaceFirst("http(s)?://", ""); } + if (!Strings.isNullOrEmpty(bucketBoundHostName)) { + return bucketBoundHostName.replaceFirst("http(s)?://", ""); + } return STORAGE_XML_URI_HOST_NAME; } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java index 34f2478d5e..59e1f8d2cf 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java @@ -1808,6 +1808,7 @@ public void testSignUrlLeadingSlash() String expectedUrl = new StringBuilder("https://storage.googleapis.com/") .append(BUCKET_NAME1) + .append("/") .append(expectedResourcePath) .append("?GoogleAccessId=") .append(ACCOUNT) @@ -1825,6 +1826,7 @@ public void testSignUrlLeadingSlash() .append(42L + 1209600) .append("\n/") .append(BUCKET_NAME1) + .append("/") .append(expectedResourcePath); Signature signer = Signature.getInstance("SHA256withRSA"); @@ -1857,6 +1859,7 @@ public void testSignUrlLeadingSlashWithHostName() String expectedUrl = new StringBuilder("https://example.com/") .append(BUCKET_NAME1) + .append("/") .append(escapedBlobName) .append("?GoogleAccessId=") .append(ACCOUNT) @@ -1874,6 +1877,7 @@ public void testSignUrlLeadingSlashWithHostName() .append(42L + 1209600) .append("\n/") .append(BUCKET_NAME1) + .append("/") .append(escapedBlobName); Signature signer = Signature.getInstance("SHA256withRSA"); @@ -2019,6 +2023,7 @@ public void testSignUrlForBlobWithSpecialChars() String expectedUrl = new StringBuilder("https://storage.googleapis.com/") .append(BUCKET_NAME1) + .append("/") .append(expectedBlobName) .append("?GoogleAccessId=") .append(ACCOUNT) @@ -2036,6 +2041,7 @@ public void testSignUrlForBlobWithSpecialChars() .append(42L + 1209600) .append("\n/") .append(BUCKET_NAME1) + .append("/") .append(expectedBlobName); Signature signer = Signature.getInstance("SHA256withRSA"); @@ -2075,6 +2081,7 @@ public void testSignUrlForBlobWithSpecialCharsAndHostName() String expectedUrl = new StringBuilder("https://example.com/") .append(BUCKET_NAME1) + .append("/") .append(expectedBlobName) .append("?GoogleAccessId=") .append(ACCOUNT) @@ -2092,6 +2099,7 @@ public void testSignUrlForBlobWithSpecialCharsAndHostName() .append(42L + 1209600) .append("\n/") .append(BUCKET_NAME1) + .append("/") .append(expectedBlobName); Signature signer = Signature.getInstance("SHA256withRSA"); @@ -2243,6 +2251,7 @@ public void testSignUrlForBlobWithSlashes() String expectedUrl = new StringBuilder("https://storage.googleapis.com/") .append(BUCKET_NAME1) + .append("/") .append(escapedBlobName) .append("?GoogleAccessId=") .append(ACCOUNT) @@ -2260,6 +2269,7 @@ public void testSignUrlForBlobWithSlashes() .append(42L + 1209600) .append("\n/") .append(BUCKET_NAME1) + .append("/") .append(escapedBlobName); Signature signer = Signature.getInstance("SHA256withRSA"); @@ -2293,6 +2303,7 @@ public void testSignUrlForBlobWithSlashesAndHostName() String expectedUrl = new StringBuilder("https://example.com/") .append(BUCKET_NAME1) + .append("/") .append(escapedBlobName) .append("?GoogleAccessId=") .append(ACCOUNT) @@ -2310,6 +2321,7 @@ public void testSignUrlForBlobWithSlashesAndHostName() .append(42L + 1209600) .append("\n/") .append(BUCKET_NAME1) + .append("/") .append(escapedBlobName); Signature signer = Signature.getInstance("SHA256withRSA"); diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/V4SigningTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/V4SigningTest.java index 8e1ad31045..d45cb6dc8c 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/V4SigningTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/V4SigningTest.java @@ -16,11 +16,8 @@ package com.google.cloud.storage; -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsNot.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assume.assumeThat; import com.google.api.core.ApiClock; import com.google.auth.oauth2.ServiceAccountCredentials; @@ -94,11 +91,6 @@ public V4SigningTest( @Test public void test() { - assumeThat( - "Test skipped until b/136171758 is resolved", - testName.getMethodName(), - is(not("test[Headers should be trimmed]"))); - Storage storage = RemoteStorageHelper.create() .getOptions() @@ -110,6 +102,19 @@ public void test() { BlobInfo blob = BlobInfo.newBuilder(testData.getBucket(), testData.getObject()).build(); + SignUrlOption style = SignUrlOption.withPathStyle(); + + if (testData.getUrlStyle().equals(SigningV4Test.UrlStyle.VIRTUAL_HOSTED_STYLE)) { + style = SignUrlOption.withVirtualHostedStyle(); + } else if (testData.getUrlStyle().equals(SigningV4Test.UrlStyle.PATH_STYLE)) { + style = SignUrlOption.withPathStyle(); + } else if (testData.getUrlStyle().equals(SigningV4Test.UrlStyle.BUCKET_BOUND_DOMAIN)) { + style = + SignUrlOption.withBucketBoundHostname( + testData.getBucketBoundDomain(), + Storage.UriScheme.valueOf(testData.getScheme().toUpperCase())); + } + final String signedUrl = storage .signUrl( @@ -118,7 +123,9 @@ public void test() { TimeUnit.SECONDS, SignUrlOption.httpMethod(HttpMethod.valueOf(testData.getMethod())), SignUrlOption.withExtHeaders(testData.getHeadersMap()), - SignUrlOption.withV4Signature()) + SignUrlOption.withV4Signature(), + SignUrlOption.withQueryParams(testData.getQueryParametersMap()), + style) .toString(); assertEquals(testData.getExpectedUrl(), signedUrl); } diff --git a/pom.xml b/pom.xml index 725ce94068..de0421be98 100644 --- a/pom.xml +++ b/pom.xml @@ -212,7 +212,7 @@ com.google.cloud google-cloud-conformance-tests - 0.0.4 + 0.0.5 test @@ -225,7 +225,10 @@ org.apache.maven.plugins maven-dependency-plugin - org.objenesis:objenesis + + org.hamcrest:hamcrest + org.objenesis:objenesis +