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() {