diff --git a/pom.client.xml b/pom.client.xml index d6a535206645..ce6957bcaeed 100644 --- a/pom.client.xml +++ b/pom.client.xml @@ -750,6 +750,7 @@ ./sdk/tracing/tracing-opentelemetry ./sdk/identity/azure-identity ./sdk/storage/azure-storage-blob + ./sdk/storage/azure-storage-common ./sdk/storage/azure-storage-file ./sdk/storage/azure-storage-queue diff --git a/sdk/storage/azure-storage-blob/pom.xml b/sdk/storage/azure-storage-blob/pom.xml index 1aeace443be0..59e986439e6d 100644 --- a/sdk/storage/azure-storage-blob/pom.xml +++ b/sdk/storage/azure-storage-blob/pom.xml @@ -51,6 +51,11 @@ azure-core 1.0.0-preview.4 + + com.azure + azure-storage-common + 12.0.0-preview.3 + org.slf4j slf4j-api diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/AccountSASSignatureValues.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/AccountSASSignatureValues.java index 5d770a8112e9..50e00f0bfafc 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/AccountSASSignatureValues.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/AccountSASSignatureValues.java @@ -3,9 +3,12 @@ package com.azure.storage.blob; +import com.azure.storage.common.Constants; +import com.azure.storage.common.IPRange; +import com.azure.storage.common.SASProtocol; +import com.azure.storage.common.Utility; import com.azure.storage.common.credentials.SharedKeyCredential; -import java.security.InvalidKeyException; import java.time.OffsetDateTime; /** @@ -223,14 +226,7 @@ public SASQueryParameters generateSASQueryParameters(SharedKeyCredential sharedK Utility.assertNotNull("version", this.version); // Signature is generated on the un-url-encoded values. - final String stringToSign = stringToSign(sharedKeyCredentials); - - String signature; - try { - signature = sharedKeyCredentials.computeHmac256(stringToSign); - } catch (InvalidKeyException e) { - throw new RuntimeException(e); // The key should have been validated by now. If it is no longer valid here, we fail. - } + String signature = sharedKeyCredentials.computeHmac256(stringToSign(sharedKeyCredentials)); return new SASQueryParameters(this.version, this.services, resourceTypes, this.protocol, this.startTime, this.expiryTime, this.ipRange, null, @@ -243,10 +239,10 @@ private String stringToSign(final SharedKeyCredential sharedKeyCredentials) { AccountSASPermission.parse(this.permissions).toString(), // guarantees ordering this.services, resourceTypes, - this.startTime == null ? "" : Utility.ISO_8601_UTC_DATE_FORMATTER.format(this.startTime), + this.startTime == null ? Constants.EMPTY_STRING : Utility.ISO_8601_UTC_DATE_FORMATTER.format(this.startTime), Utility.ISO_8601_UTC_DATE_FORMATTER.format(this.expiryTime), - this.ipRange == null ? (new IPRange()).toString() : this.ipRange.toString(), - this.protocol == null ? "" : this.protocol.toString(), + this.ipRange == null ? Constants.EMPTY_STRING : this.ipRange.toString(), + this.protocol == null ? Constants.EMPTY_STRING : this.protocol.toString(), this.version, Constants.EMPTY_STRING // Account SAS requires an additional newline character ); diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/AppendBlobAsyncClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/AppendBlobAsyncClient.java index 4ddc1d9e4682..9cf6c224c0d3 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/AppendBlobAsyncClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/AppendBlobAsyncClient.java @@ -14,13 +14,14 @@ import com.azure.storage.blob.models.BlobRange; import com.azure.storage.blob.models.Metadata; import com.azure.storage.blob.models.SourceModifiedAccessConditions; +import com.azure.storage.common.Constants; import io.netty.buffer.ByteBuf; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.net.URL; -import static com.azure.storage.blob.Utility.postProcessResponse; +import static com.azure.storage.blob.PostProcessor.postProcessResponse; /** diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/AppendBlobClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/AppendBlobClient.java index 65468a2e42b5..cacb5e0c30ed 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/AppendBlobClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/AppendBlobClient.java @@ -11,6 +11,7 @@ import com.azure.storage.blob.models.BlobRange; import com.azure.storage.blob.models.Metadata; import com.azure.storage.blob.models.SourceModifiedAccessConditions; +import com.azure.storage.common.Utility; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import reactor.core.publisher.Flux; diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobAsyncClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobAsyncClient.java index 8f5a863ad38c..b4a5e38668c6 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobAsyncClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobAsyncClient.java @@ -25,6 +25,10 @@ import com.azure.storage.blob.models.SourceModifiedAccessConditions; import com.azure.storage.blob.models.StorageAccountInfo; import com.azure.storage.blob.models.UserDelegationKey; +import com.azure.storage.common.Constants; +import com.azure.storage.common.IPRange; +import com.azure.storage.common.SASProtocol; +import com.azure.storage.common.Utility; import com.azure.storage.common.credentials.SharedKeyCredential; import io.netty.buffer.ByteBuf; import reactor.core.publisher.Flux; @@ -44,7 +48,7 @@ import java.util.ArrayList; import java.util.List; -import static com.azure.storage.blob.Utility.postProcessResponse; +import static com.azure.storage.blob.PostProcessor.postProcessResponse; /** * Client to a blob of any type: block, append, or page. It may only be instantiated through a {@link BlobClientBuilder} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobClient.java index dfbadaa4ab79..91ff5eb97dbc 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobClient.java @@ -18,6 +18,9 @@ import com.azure.storage.blob.models.ReliableDownloadOptions; import com.azure.storage.blob.models.StorageAccountInfo; import com.azure.storage.blob.models.UserDelegationKey; +import com.azure.storage.common.IPRange; +import com.azure.storage.common.SASProtocol; +import com.azure.storage.common.Utility; import reactor.core.publisher.Mono; import java.io.IOException; diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobClientBuilder.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobClientBuilder.java index e58c5a7adeb8..3b3d24ba6078 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobClientBuilder.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobClientBuilder.java @@ -246,7 +246,7 @@ public BlobClientBuilder endpoint(String endpoint) { this.blobName = parts.blobName(); this.snapshot = parts.snapshot(); - this.sasTokenCredential = SASTokenCredential.fromQueryParameters(parts.sasQueryParameters()); + this.sasTokenCredential = SASTokenCredential.fromSASTokenString(parts.sasQueryParameters().encode()); if (this.sasTokenCredential != null) { this.tokenCredential = null; this.sharedKeyCredential = null; diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobConfiguration.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobConfiguration.java index cc53933f7930..66619bdd73fd 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobConfiguration.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobConfiguration.java @@ -4,5 +4,5 @@ class BlobConfiguration { static final String NAME = "azure-storage-blob"; - static final String VERSION = "12.0.0-preview.2"; + static final String VERSION = "12.0.0-preview.3"; } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobInputStream.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobInputStream.java index 1a990e179688..18ef656c3550 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobInputStream.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobInputStream.java @@ -4,6 +4,7 @@ import com.azure.storage.blob.models.BlobAccessConditions; import com.azure.storage.blob.models.BlobRange; +import com.azure.storage.common.Constants; import reactor.netty.ByteBufFlux; import java.io.IOException; diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobOutputStream.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobOutputStream.java index 668f7a2c2066..f002ee4d1bf9 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobOutputStream.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobOutputStream.java @@ -249,7 +249,7 @@ private Mono dispatchWrite(Flux bufferRef, int writeLength, lo return Mono.empty(); } - if (this.streamType == BlobType.PAGE_BLOB && (writeLength % Constants.PAGE_SIZE != 0)) { + if (this.streamType == BlobType.PAGE_BLOB && (writeLength % PageBlobAsyncClient.PAGE_BYTES != 0)) { return Mono.error(new IOException(String.format(SR.INVALID_NUMBER_OF_BYTES_IN_THE_BUFFER, writeLength))); } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceAsyncClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceAsyncClient.java index a8aa7f7b2640..e009c5631b77 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceAsyncClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceAsyncClient.java @@ -21,6 +21,9 @@ import com.azure.storage.blob.models.StorageServiceProperties; import com.azure.storage.blob.models.StorageServiceStats; import com.azure.storage.blob.models.UserDelegationKey; +import com.azure.storage.common.IPRange; +import com.azure.storage.common.SASProtocol; +import com.azure.storage.common.Utility; import com.azure.storage.common.credentials.SharedKeyCredential; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -29,7 +32,7 @@ import java.net.URL; import java.time.OffsetDateTime; -import static com.azure.storage.blob.Utility.postProcessResponse; +import static com.azure.storage.blob.PostProcessor.postProcessResponse; /** * Client to a storage account. It may only be instantiated through a {@link BlobServiceClientBuilder}. This class does not diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceClient.java index 782e766330ab..07fc7dc0c6bd 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceClient.java @@ -16,6 +16,9 @@ import com.azure.storage.blob.models.StorageServiceProperties; import com.azure.storage.blob.models.StorageServiceStats; import com.azure.storage.blob.models.UserDelegationKey; +import com.azure.storage.common.IPRange; +import com.azure.storage.common.SASProtocol; +import com.azure.storage.common.Utility; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceClientBuilder.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceClientBuilder.java index 220d4331c64f..49aeb7536468 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceClientBuilder.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceClientBuilder.java @@ -135,7 +135,7 @@ public BlobServiceClientBuilder endpoint(String endpoint) { URL url = new URL(endpoint); this.endpoint = url.getProtocol() + "://" + url.getAuthority(); - this.sasTokenCredential = SASTokenCredential.fromQueryParameters(URLParser.parse(url).sasQueryParameters()); + this.sasTokenCredential = SASTokenCredential.fromSASTokenString(URLParser.parse(url).sasQueryParameters().encode()); if (this.sasTokenCredential != null) { this.tokenCredential = null; this.sharedKeyCredential = null; diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobURLParts.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobURLParts.java index 5c629e719ca3..90a9c4892caa 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobURLParts.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobURLParts.java @@ -4,6 +4,8 @@ package com.azure.storage.blob; import com.azure.core.implementation.http.UrlBuilder; +import com.azure.storage.common.Constants; +import com.azure.storage.common.Utility; import java.net.MalformedURLException; import java.net.URL; @@ -189,7 +191,7 @@ public URL toURL() throws MalformedURLException { for (Map.Entry entry : this.unparsedParameters.entrySet()) { // The commas are intentionally encoded. url.setQueryParameter(entry.getKey(), - Utility.safeURLEncode(String.join(",", entry.getValue()))); + Utility.urlEncode(String.join(",", entry.getValue()))); } return url.toURL(); diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlockBlobAsyncClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlockBlobAsyncClient.java index 1cc6e71232b1..162bb1917b61 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlockBlobAsyncClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlockBlobAsyncClient.java @@ -20,6 +20,7 @@ import com.azure.storage.blob.models.LeaseAccessConditions; import com.azure.storage.blob.models.Metadata; import com.azure.storage.blob.models.SourceModifiedAccessConditions; +import com.azure.storage.common.Constants; import io.netty.buffer.ByteBuf; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -39,7 +40,7 @@ import java.util.TreeMap; import java.util.UUID; -import static com.azure.storage.blob.Utility.postProcessResponse; +import static com.azure.storage.blob.PostProcessor.postProcessResponse; /** * Client to a block blob. It may only be instantiated through a {@link BlobClientBuilder}, via diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlockBlobClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlockBlobClient.java index 69bad81c7ff2..5e2f7dbd346c 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlockBlobClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlockBlobClient.java @@ -14,6 +14,7 @@ import com.azure.storage.blob.models.LeaseAccessConditions; import com.azure.storage.blob.models.Metadata; import com.azure.storage.blob.models.SourceModifiedAccessConditions; +import com.azure.storage.common.Utility; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import reactor.core.publisher.Flux; diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/Constants.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/Constants.java deleted file mode 100644 index fa812df4b531..000000000000 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/Constants.java +++ /dev/null @@ -1,310 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.storage.blob; - -/** - * RESERVED FOR INTERNAL USE. Contains storage constants. - */ -final class Constants { - - /** - * The master Microsoft Azure Storage header prefix. - */ - static final String PREFIX_FOR_STORAGE_HEADER = "x-ms-"; - /** - * Constant representing a kilobyte (Non-SI version). - */ - static final int KB = 1024; - /** - * Constant representing a megabyte (Non-SI version). - */ - static final int MB = 1024 * KB; - /** - * An empty {@code String} to use for comparison. - */ - static final String EMPTY_STRING = ""; - /** - * Specifies HTTP. - */ - static final String HTTP = "http"; - /** - * Specifies HTTPS. - */ - static final String HTTPS = "https"; - /** - * Specifies both HTTPS and HTTP. - */ - static final String HTTPS_HTTP = "https,http"; - /** - * The default type for content-type and accept. - */ - static final String UTF8_CHARSET = "UTF-8"; - /** - * The query parameter for snapshots. - */ - static final String SNAPSHOT_QUERY_PARAMETER = "snapshot"; - /** - * The word redacted. - */ - static final String REDACTED = "REDACTED"; - /** - * The default amount of parallelism for TransferManager operations. - */ - // We chose this to match Go, which followed AWS' default. - static final int TRANSFER_MANAGER_DEFAULT_PARALLELISM = 5; - - /** - * The size of a page, in bytes, in a page blob. - */ - public static final int PAGE_SIZE = 512; - - - /** - * Private Default Ctor - */ - private Constants() { - // Private to prevent construction. - } - - /** - * Defines constants for use with HTTP headers. - */ - static final class HeaderConstants { - /** - * The Authorization header. - */ - static final String AUTHORIZATION = "Authorization"; - - /** - * The header that indicates the client request ID. - */ - static final String CLIENT_REQUEST_ID_HEADER = PREFIX_FOR_STORAGE_HEADER + "client-request-id"; - - /** - * The ContentEncoding header. - */ - static final String CONTENT_ENCODING = "Content-Encoding"; - - /** - * The ContentLangauge header. - */ - static final String CONTENT_LANGUAGE = "Content-Language"; - - /** - * The ContentLength header. - */ - static final String CONTENT_LENGTH = "Content-Length"; - - /** - * The ContentMD5 header. - */ - static final String CONTENT_MD5 = "Content-MD5"; - - /** - * The ContentType header. - */ - static final String CONTENT_TYPE = "Content-Type"; - - /** - * The header that specifies the date. - */ - static final String DATE = PREFIX_FOR_STORAGE_HEADER + "date"; - - /** - * The header that specifies the error code on unsuccessful responses. - */ - static final String ERROR_CODE = PREFIX_FOR_STORAGE_HEADER + "error-code"; - - /** - * The IfMatch header. - */ - static final String IF_MATCH = "If-Match"; - - /** - * The IfModifiedSince header. - */ - static final String IF_MODIFIED_SINCE = "If-Modified-Since"; - - /** - * The IfNoneMatch header. - */ - static final String IF_NONE_MATCH = "If-None-Match"; - - /** - * The IfUnmodifiedSince header. - */ - static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; - - /** - * The Range header. - */ - static final String RANGE = "Range"; - - /** - * The copy source header. - */ - static final String COPY_SOURCE = "x-ms-copy-source"; - - /** - * The version header. - */ - static final String VERSION = "x-ms-version"; - - /** - * The current storage version header value. - */ - static final String TARGET_STORAGE_VERSION = "2018-11-09"; - - /** - * The UserAgent header. - */ - static final String USER_AGENT = "User-Agent"; - - /** - * Specifies the value to use for UserAgent header. - */ - static final String USER_AGENT_PREFIX = "Azure-Storage"; - - /** - * Specifies the value to use for UserAgent header. - */ - static final String USER_AGENT_VERSION = "11.0.1"; - - private HeaderConstants() { - // Private to prevent construction. - } - } - - static final class UrlConstants { - - /** - * The SAS service version parameter. - */ - static final String SAS_SERVICE_VERSION = "sv"; - - /** - * The SAS services parameter. - */ - static final String SAS_SERVICES = "ss"; - - /** - * The SAS resource types parameter. - */ - static final String SAS_RESOURCES_TYPES = "srt"; - - /** - * The SAS protocol parameter. - */ - static final String SAS_PROTOCOL = "spr"; - - /** - * The SAS start time parameter. - */ - static final String SAS_START_TIME = "st"; - - /** - * The SAS expiration time parameter. - */ - static final String SAS_EXPIRY_TIME = "se"; - - /** - * The SAS IP range parameter. - */ - static final String SAS_IP_RANGE = "sip"; - - /** - * The SAS signed identifier parameter. - */ - static final String SAS_SIGNED_IDENTIFIER = "si"; - - /** - * The SAS signed resource parameter. - */ - static final String SAS_SIGNED_RESOURCE = "sr"; - - /** - * The SAS signed permissions parameter. - */ - static final String SAS_SIGNED_PERMISSIONS = "sp"; - - /** - * The SAS signature parameter. - */ - static final String SAS_SIGNATURE = "sig"; - - /** - * The SAS cache control parameter. - */ - static final String SAS_CACHE_CONTROL = "rscc"; - - /** - * The SAS content disposition parameter. - */ - static final String SAS_CONTENT_DISPOSITION = "rscd"; - - /** - * The SAS content encoding parameter. - */ - static final String SAS_CONTENT_ENCODING = "rsce"; - - /** - * The SAS content language parameter. - */ - static final String SAS_CONTENT_LANGUAGE = "rscl"; - - /** - * The SAS content type parameter. - */ - static final String SAS_CONTENT_TYPE = "rsct"; - - /** - * The SAS signed object id parameter for user delegation SAS. - */ - public static final String SAS_SIGNED_OBJECT_ID = "skoid"; - - /** - * The SAS signed tenant id parameter for user delegation SAS. - */ - public static final String SAS_SIGNED_TENANT_ID = "sktid"; - - /** - * The SAS signed key-start parameter for user delegation SAS. - */ - public static final String SAS_SIGNED_KEY_START = "skt"; - - /** - * The SAS signed key-expiry parameter for user delegation SAS. - */ - public static final String SAS_SIGNED_KEY_EXPIRY = "ske"; - - /** - * The SAS signed service parameter for user delegation SAS. - */ - public static final String SAS_SIGNED_KEY_SERVICE = "sks"; - - /** - * The SAS signed version parameter for user delegation SAS. - */ - public static final String SAS_SIGNED_KEY_VERSION = "skv"; - - /** - * The SAS blob constant. - */ - public static final String SAS_BLOB_CONSTANT = "b"; - - /** - * The SAS blob snapshot constant. - */ - public static final String SAS_BLOB_SNAPSHOT_CONSTANT = "bs"; - - /** - * The SAS blob snapshot constant. - */ - public static final String SAS_CONTAINER_CONSTANT = "c"; - - private UrlConstants() { - // Private to prevent construction. - } - } -} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ContainerAsyncClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ContainerAsyncClient.java index 155c0498f49f..99b1524507bf 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ContainerAsyncClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ContainerAsyncClient.java @@ -26,6 +26,10 @@ import com.azure.storage.blob.models.SignedIdentifier; import com.azure.storage.blob.models.StorageAccountInfo; import com.azure.storage.blob.models.UserDelegationKey; +import com.azure.storage.common.Constants; +import com.azure.storage.common.IPRange; +import com.azure.storage.common.SASProtocol; +import com.azure.storage.common.Utility; import com.azure.storage.common.credentials.SharedKeyCredential; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -37,7 +41,7 @@ import java.time.temporal.ChronoUnit; import java.util.List; -import static com.azure.storage.blob.Utility.postProcessResponse; +import static com.azure.storage.blob.PostProcessor.postProcessResponse; /** * Client to a container. It may only be instantiated through a {@link ContainerClientBuilder} or via the method {@link diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ContainerClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ContainerClient.java index c66b9d8c2373..566efd526a69 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ContainerClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ContainerClient.java @@ -16,6 +16,9 @@ import com.azure.storage.blob.models.SignedIdentifier; import com.azure.storage.blob.models.StorageAccountInfo; import com.azure.storage.blob.models.UserDelegationKey; +import com.azure.storage.common.IPRange; +import com.azure.storage.common.SASProtocol; +import com.azure.storage.common.Utility; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ContainerClientBuilder.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ContainerClientBuilder.java index 7ef873aac1b3..1c61dd618a46 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ContainerClientBuilder.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ContainerClientBuilder.java @@ -140,7 +140,7 @@ public ContainerClientBuilder endpoint(String endpoint) { this.endpoint = parts.scheme() + "://" + parts.host(); this.containerName = parts.containerName(); - this.sasTokenCredential = SASTokenCredential.fromQueryParameters(parts.sasQueryParameters()); + this.sasTokenCredential = SASTokenCredential.fromSASTokenString(parts.sasQueryParameters().encode()); if (this.sasTokenCredential != null) { this.tokenCredential = null; this.sharedKeyCredential = null; diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/DownloadAsyncResponse.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/DownloadAsyncResponse.java index 819b85891b10..1daacb95051d 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/DownloadAsyncResponse.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/DownloadAsyncResponse.java @@ -8,6 +8,7 @@ import com.azure.storage.blob.models.BlobDownloadHeaders; import com.azure.storage.blob.models.BlobRange; import com.azure.storage.blob.models.ReliableDownloadOptions; +import com.azure.storage.common.Utility; import io.netty.buffer.ByteBuf; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/HTTPGetterInfo.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/HTTPGetterInfo.java index 6393d0a1ebbb..64dd6b5d997d 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/HTTPGetterInfo.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/HTTPGetterInfo.java @@ -4,6 +4,7 @@ package com.azure.storage.blob; import com.azure.storage.blob.models.BlobAccessConditions; +import com.azure.storage.common.Utility; import java.time.Duration; diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/PageBlobAsyncClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/PageBlobAsyncClient.java index 8b1cbd0ca39d..944bb4ef8ff6 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/PageBlobAsyncClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/PageBlobAsyncClient.java @@ -20,6 +20,7 @@ import com.azure.storage.blob.models.PageRange; import com.azure.storage.blob.models.SequenceNumberActionType; import com.azure.storage.blob.models.SourceModifiedAccessConditions; +import com.azure.storage.common.Constants; import io.netty.buffer.ByteBuf; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,7 +28,7 @@ import java.net.MalformedURLException; import java.net.URL; -import static com.azure.storage.blob.Utility.postProcessResponse; +import static com.azure.storage.blob.PostProcessor.postProcessResponse; /** * Client to a page blob. It may only be instantiated through a {@link BlobClientBuilder}, via diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/PageBlobClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/PageBlobClient.java index 14fdb39d5702..9e683c7a8b8d 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/PageBlobClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/PageBlobClient.java @@ -16,6 +16,7 @@ import com.azure.storage.blob.models.PageRange; import com.azure.storage.blob.models.SequenceNumberActionType; import com.azure.storage.blob.models.SourceModifiedAccessConditions; +import com.azure.storage.common.Utility; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import reactor.core.publisher.Flux; diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/PostProcessor.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/PostProcessor.java new file mode 100644 index 000000000000..28e6e541cd79 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/PostProcessor.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob; + +import com.azure.storage.blob.models.StorageErrorException; +import com.azure.storage.common.Utility; +import reactor.core.publisher.Mono; + +final class PostProcessor { + static Mono postProcessResponse(Mono response) { + return Utility.postProcessResponse(response, (errorResponse) -> + errorResponse.onErrorResume(StorageErrorException.class, resume -> + resume.response() + .bodyAsString() + .switchIfEmpty(Mono.just("")) + .flatMap(body -> Mono.error(new StorageException(resume, body))) + )); + } + + private PostProcessor() { + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/SASQueryParameters.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/SASQueryParameters.java index 7dead915f796..139fd8179f93 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/SASQueryParameters.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/SASQueryParameters.java @@ -4,13 +4,15 @@ package com.azure.storage.blob; import com.azure.storage.blob.models.UserDelegationKey; +import com.azure.storage.common.Constants; +import com.azure.storage.common.IPRange; +import com.azure.storage.common.SASProtocol; +import com.azure.storage.common.Utility; import java.time.OffsetDateTime; import java.util.Map; import java.util.function.Function; -import static com.azure.storage.blob.Utility.safeURLEncode; - /** * Represents the components that make up an Azure Storage SAS' query parameters. This type is not constructed directly * by the user; it is only generated by the {@link AccountSASSignatureValues} and {@link ServiceSASSignatureValues} @@ -346,7 +348,7 @@ private void tryAppendQueryParameter(StringBuilder sb, String param, Object valu if (sb.length() != 0) { sb.append('&'); } - sb.append(safeURLEncode(param)).append('=').append(safeURLEncode(value.toString())); + sb.append(Utility.urlEncode(param)).append('=').append(Utility.urlEncode(value.toString())); } } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ServiceSASSignatureValues.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ServiceSASSignatureValues.java index 9fd8eaa2fc64..a62f85d78643 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ServiceSASSignatureValues.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/ServiceSASSignatureValues.java @@ -4,11 +4,14 @@ package com.azure.storage.blob; import com.azure.storage.blob.models.UserDelegationKey; +import com.azure.storage.common.Constants; +import com.azure.storage.common.IPRange; +import com.azure.storage.common.SASProtocol; +import com.azure.storage.common.Utility; import com.azure.storage.common.credentials.SharedKeyCredential; import java.net.MalformedURLException; import java.net.URL; -import java.security.InvalidKeyException; import java.time.OffsetDateTime; /** @@ -410,21 +413,15 @@ public ServiceSASSignatureValues contentType(String contentType) { * * @param sharedKeyCredentials A {@link SharedKeyCredential} object used to sign the SAS values. * @return {@link SASQueryParameters} - * @throws Error If the accountKey is not a valid Base64-encoded string. + * @throws IllegalStateException If the HMAC-SHA256 algorithm isn't supported, if the key isn't a valid Base64 + * encoded string, or the UTF-8 charset isn't supported. */ public SASQueryParameters generateSASQueryParameters(SharedKeyCredential sharedKeyCredentials) { Utility.assertNotNull("sharedKeyCredentials", sharedKeyCredentials); assertGenerateOK(false); // Signature is generated on the un-url-encoded values. - final String stringToSign = stringToSign(); - - String signature; - try { - signature = sharedKeyCredentials.computeHmac256(stringToSign); - } catch (InvalidKeyException e) { - throw new Error(e); // The key should have been validated by now. If it is no longer valid here, we fail. - } + String signature = sharedKeyCredentials.computeHmac256(stringToSign()); return new SASQueryParameters(this.version, null, null, this.protocol, this.startTime, this.expiryTime, this.ipRange, this.identifier, resource, @@ -437,21 +434,15 @@ public SASQueryParameters generateSASQueryParameters(SharedKeyCredential sharedK * * @param delegationKey A {@link UserDelegationKey} object used to sign the SAS values. * @return {@link SASQueryParameters} - * @throws Error If the accountKey is not a valid Base64-encoded string. + * @throws IllegalStateException If the HMAC-SHA256 algorithm isn't supported, if the key isn't a valid Base64 + * encoded string, or the UTF-8 charset isn't supported. */ public SASQueryParameters generateSASQueryParameters(UserDelegationKey delegationKey) { Utility.assertNotNull("delegationKey", delegationKey); assertGenerateOK(true); // Signature is generated on the un-url-encoded values. - final String stringToSign = stringToSign(delegationKey); - - String signature; - try { - signature = Utility.delegateComputeHmac256(delegationKey, stringToSign); - } catch (InvalidKeyException e) { - throw new Error(e); // The key should have been validated by now. If it is no longer valid here, we fail. - } + String signature = Utility.computeHMac256(delegationKey.value(), stringToSign(delegationKey)); return new SASQueryParameters(this.version, null, null, this.protocol, this.startTime, this.expiryTime, this.ipRange, null /* identifier */, resource, @@ -487,7 +478,7 @@ private String stringToSign() { this.expiryTime == null ? "" : Utility.ISO_8601_UTC_DATE_FORMATTER.format(this.expiryTime), this.canonicalName == null ? "" : this.canonicalName, this.identifier == null ? "" : this.identifier, - this.ipRange == null ? (new IPRange()).toString() : this.ipRange.toString(), + this.ipRange == null ? "" : this.ipRange.toString(), this.protocol == null ? "" : protocol.toString(), this.version == null ? "" : this.version, this.resource == null ? "" : this.resource, @@ -512,7 +503,7 @@ private String stringToSign(final UserDelegationKey key) { key.signedExpiry() == null ? "" : Utility.ISO_8601_UTC_DATE_FORMATTER.format(key.signedExpiry()), key.signedService() == null ? "" : key.signedService(), key.signedVersion() == null ? "" : key.signedVersion(), - this.ipRange == null ? new IPRange().toString() : this.ipRange.toString(), + this.ipRange == null ? "" : this.ipRange.toString(), this.protocol == null ? "" : this.protocol.toString(), this.version == null ? "" : this.version, this.resource == null ? "" : this.resource, diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/StorageException.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/StorageException.java index 8fb587be3611..cff087ff1b52 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/StorageException.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/StorageException.java @@ -6,6 +6,7 @@ import com.azure.core.exception.HttpResponseException; import com.azure.storage.blob.models.StorageErrorCode; import com.azure.storage.blob.models.StorageErrorException; +import com.azure.storage.common.Constants; /** * A {@code StorageException} is thrown whenever Azure Storage successfully returns an error code that is not 200-level. diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/URLParser.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/URLParser.java index afc8539341a5..4ec53cd475c4 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/URLParser.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/URLParser.java @@ -3,6 +3,9 @@ package com.azure.storage.blob; +import com.azure.core.implementation.util.ImplUtils; +import com.azure.storage.common.Utility; + import java.net.URL; import java.util.Comparator; import java.util.Locale; @@ -34,7 +37,7 @@ public static BlobURLParts parse(URL url) { // find the container & blob names (if any) String path = url.getPath(); - if (!Utility.isNullOrEmpty(path)) { + if (!ImplUtils.isNullOrEmpty(path)) { // if the path starts with a slash remove it if (path.charAt(0) == '/') { path = path.substring(1); @@ -88,7 +91,7 @@ public int compare(String s1, String s2) { } }); - if (Utility.isNullOrEmpty(queryParams)) { + if (ImplUtils.isNullOrEmpty(queryParams)) { return retVals; } @@ -99,8 +102,8 @@ public int compare(String s1, String s2) { for (int m = 0; m < valuePairs.length; m++) { // Getting key and value for a single query parameter final int equalDex = valuePairs[m].indexOf("="); - String key = Utility.safeURLDecode(valuePairs[m].substring(0, equalDex)).toLowerCase(Locale.ROOT); - String value = Utility.safeURLDecode(valuePairs[m].substring(equalDex + 1)); + String key = Utility.urlDecode(valuePairs[m].substring(0, equalDex)).toLowerCase(Locale.ROOT); + String value = Utility.urlDecode(valuePairs[m].substring(equalDex + 1)); // add to map String[] keyValues = retVals.get(key); diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/Utility.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/Utility.java deleted file mode 100644 index d22c2fc6dde2..000000000000 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/Utility.java +++ /dev/null @@ -1,413 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.storage.blob; - -import com.azure.core.http.HttpHeader; -import com.azure.core.http.HttpHeaders; -import com.azure.core.http.HttpPipeline; -import com.azure.core.http.policy.HttpPipelinePolicy; -import com.azure.core.implementation.http.UrlBuilder; -import com.azure.storage.blob.models.StorageErrorException; -import com.azure.storage.blob.models.UserDelegationKey; -import com.azure.storage.common.credentials.SharedKeyCredential; -import com.azure.storage.common.policy.SharedKeyCredentialPolicy; -import reactor.core.publisher.Mono; -import reactor.util.annotation.Nullable; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.io.UnsupportedEncodingException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.time.Duration; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.Base64; -import java.util.Locale; - -final class Utility { - - static final DateTimeFormatter RFC_1123_GMT_DATE_FORMATTER = - DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z", Locale.ROOT).withZone(ZoneId.of("GMT")); - - static final DateTimeFormatter ISO_8601_UTC_DATE_FORMATTER = - DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT).withZone(ZoneId.of("UTC")); - /** - * Stores a reference to the UTC time zone. - */ - static final ZoneId UTC_ZONE = ZoneId.of("UTC"); - /** - * Stores a reference to the date/time pattern with the greatest precision Java.util.Date is capable of expressing. - */ - private static final String MAX_PRECISION_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS"; - /** - * Stores a reference to the ISO8601 date/time pattern. - */ - private static final String ISO8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - /** - * Stores a reference to the ISO8601 date/time pattern. - */ - private static final String ISO8601_PATTERN_NO_SECONDS = "yyyy-MM-dd'T'HH:mm'Z'"; - /** - * The length of a datestring that matches the MAX_PRECISION_PATTERN. - */ - private static final int MAX_PRECISION_DATESTRING_LENGTH = MAX_PRECISION_PATTERN.replaceAll("'", "").length(); - - /** - * Asserts that a value is not null. - * - * @param param - * A {@code String} that represents the name of the parameter, which becomes the exception message - * text if the value parameter is null. - * @param value - * An Object object that represents the value of the specified parameter. This is the value - * being asserted as not null. - */ - static void assertNotNull(final String param, final Object value) { - if (value == null) { - throw new IllegalArgumentException(String.format(Locale.ROOT, SR.ARGUMENT_NULL_OR_EMPTY, param)); - } - } - - /** - * Returns a value that indicates whether the specified string is null or empty. - * - * @param value - * A {@code String} being examined for null or empty. - * - * @return true if the specified value is null or empty; otherwise, false - */ - static boolean isNullOrEmpty(final String value) { - return value == null || value.length() == 0; - } - - /** - * Performs safe decoding of the specified string, taking care to preserve each + character, rather - * than replacing it with a space character. - * - * @param stringToDecode - * A {@code String} that represents the string to decode. - * - * @return A {@code String} that represents the decoded string. - */ - static String safeURLDecode(final String stringToDecode) { - if (stringToDecode.length() == 0) { - return Constants.EMPTY_STRING; - } - - // '+' are decoded as ' ' so preserve before decoding - if (stringToDecode.contains("+")) { - final StringBuilder outBuilder = new StringBuilder(); - - int startDex = 0; - for (int m = 0; m < stringToDecode.length(); m++) { - if (stringToDecode.charAt(m) == '+') { - if (m > startDex) { - try { - outBuilder.append(URLDecoder.decode(stringToDecode.substring(startDex, m), - Constants.UTF8_CHARSET)); - } catch (UnsupportedEncodingException e) { - throw new Error(e); - } - } - - outBuilder.append("+"); - startDex = m + 1; - } - } - - if (startDex != stringToDecode.length()) { - try { - outBuilder.append(URLDecoder.decode(stringToDecode.substring(startDex, stringToDecode.length()), - Constants.UTF8_CHARSET)); - } catch (UnsupportedEncodingException e) { - throw new Error(e); - } - } - - return outBuilder.toString(); - } else { - try { - return URLDecoder.decode(stringToDecode, Constants.UTF8_CHARSET); - } catch (UnsupportedEncodingException e) { - throw new Error(e); - } - } - } - - /** - * Given a String representing a date in a form of the ISO8601 pattern, generates a Date representing it - * with up to millisecond precision. - * - * @param dateString - * the {@code String} to be interpreted as a Date - * - * @return the corresponding Date object - * @throws IllegalArgumentException If {@code dateString} doesn't match an ISO8601 pattern - */ - public static OffsetDateTime parseDate(String dateString) { - String pattern = MAX_PRECISION_PATTERN; - switch (dateString.length()) { - case 28: // "yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'"-> [2012-01-04T23:21:59.1234567Z] length = 28 - case 27: // "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"-> [2012-01-04T23:21:59.123456Z] length = 27 - case 26: // "yyyy-MM-dd'T'HH:mm:ss.SSSSS'Z'"-> [2012-01-04T23:21:59.12345Z] length = 26 - case 25: // "yyyy-MM-dd'T'HH:mm:ss.SSSS'Z'"-> [2012-01-04T23:21:59.1234Z] length = 25 - case 24: // "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"-> [2012-01-04T23:21:59.123Z] length = 24 - dateString = dateString.substring(0, MAX_PRECISION_DATESTRING_LENGTH); - break; - case 23: // "yyyy-MM-dd'T'HH:mm:ss.SS'Z'"-> [2012-01-04T23:21:59.12Z] length = 23 - // SS is assumed to be milliseconds, so a trailing 0 is necessary - dateString = dateString.replace("Z", "0"); - break; - case 22: // "yyyy-MM-dd'T'HH:mm:ss.S'Z'"-> [2012-01-04T23:21:59.1Z] length = 22 - // S is assumed to be milliseconds, so trailing 0's are necessary - dateString = dateString.replace("Z", "00"); - break; - case 20: // "yyyy-MM-dd'T'HH:mm:ss'Z'"-> [2012-01-04T23:21:59Z] length = 20 - pattern = Utility.ISO8601_PATTERN; - break; - case 17: // "yyyy-MM-dd'T'HH:mm'Z'"-> [2012-01-04T23:21Z] length = 17 - pattern = Utility.ISO8601_PATTERN_NO_SECONDS; - break; - default: - throw new IllegalArgumentException(String.format(Locale.ROOT, SR.INVALID_DATE_STRING, dateString)); - } - - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern, Locale.ROOT); - return LocalDateTime.parse(dateString, formatter).atZone(UTC_ZONE).toOffsetDateTime(); - } - - /** - * Asserts that the specified integer is in the valid range. - * - * @param param - * A String that represents the name of the parameter, which becomes the exception message - * text if the value parameter is out of bounds. - * @param value - * The value of the specified parameter. - * @param min - * The minimum value for the specified parameter. - * @param max - * The maximum value for the specified parameter. - * @throws IllegalArgumentException If {@code value} is less than {@code min} or greater than {@code max}. - */ - public static void assertInBounds(final String param, final long value, final long min, final long max) { - if (value < min || value > max) { - throw new IllegalArgumentException(String.format(Locale.ROOT, SR.PARAMETER_NOT_IN_RANGE, param, min, max)); - } - } - - /** - * Performs safe encoding of the specified string, taking care to insert %20 for each space character, - * instead of inserting the + character. - */ - static String safeURLEncode(final String stringToEncode) { - if (stringToEncode == null) { - return null; - } - if (stringToEncode.length() == 0) { - return Constants.EMPTY_STRING; - } - - try { - final String tString = URLEncoder.encode(stringToEncode, Constants.UTF8_CHARSET); - - if (stringToEncode.contains(" ")) { - final StringBuilder outBuilder = new StringBuilder(); - - int startDex = 0; - for (int m = 0; m < stringToEncode.length(); m++) { - if (stringToEncode.charAt(m) == ' ') { - if (m > startDex) { - outBuilder.append(URLEncoder.encode(stringToEncode.substring(startDex, m), - Constants.UTF8_CHARSET)); - } - - outBuilder.append("%20"); - startDex = m + 1; - } - } - - if (startDex != stringToEncode.length()) { - outBuilder.append(URLEncoder.encode(stringToEncode.substring(startDex, stringToEncode.length()), - Constants.UTF8_CHARSET)); - } - - return outBuilder.toString(); - } else { - return tString; - } - - } catch (final UnsupportedEncodingException e) { - throw new Error(e); // If we can't encode UTF-8, we fail. - } - } - - static Mono postProcessResponse(Mono s) { - s = addErrorWrappingToSingle(s); - s = scrubEtagHeaderInResponse(s); - return s; - } - - /* - We need to convert the generated StorageErrorException to StorageException, which has a cleaner interface and - methods to conveniently access important values. - */ - private static Mono addErrorWrappingToSingle(Mono s) { - return s.onErrorResume( - StorageErrorException.class, - e -> e.response() - .bodyAsString() - .switchIfEmpty(Mono.just("")) - .flatMap(body -> Mono.error(new StorageException(e, body)))); - } - - /* - The service is inconsistent in whether or not the etag header value has quotes. This method will check if the - response returns an etag value, and if it does, remove any quotes that may be present to give the user a more - predictable format to work with. - */ - private static Mono scrubEtagHeaderInResponse(Mono s) { - return s.map(response -> { - String etag = null; - try { - Object headers = response.getClass().getMethod("deserializedHeaders").invoke(response); - Method etagGetterMethod = headers.getClass().getMethod("eTag"); - etag = (String) etagGetterMethod.invoke(headers); - // CommitBlockListHeaders has an etag property, but it's only set if the blob has committed blocks. - if (etag == null) { - return response; - } - etag = etag.replace("\"", ""); // Etag headers without the quotes will be unaffected. - headers.getClass().getMethod("eTag", String.class).invoke(headers, etag); - } catch (NoSuchMethodException e) { - // Response did not return an eTag value. No change necessary. - } catch (IllegalAccessException | InvocationTargetException e) { - //TODO (unknown): validate this won't throw - } - try { - HttpHeaders rawHeaders = (HttpHeaders) response.getClass().getMethod("headers").invoke(response); - // - if (etag != null) { - rawHeaders.put("ETag", etag); - } else { - HttpHeader eTagHeader = rawHeaders.get("etag"); - if (eTagHeader != null && eTagHeader.value() != null) { - etag = eTagHeader.value().replace("\"", ""); - rawHeaders.put("ETag", etag); - } - } - } catch (NoSuchMethodException e) { - // Response did not return an eTag value. No change necessary. - } catch (IllegalAccessException | InvocationTargetException e) { - //TODO (unknown): validate this won't throw - } - return response; - }); - } - - /** - * Computes a signature for the specified string using the HMAC-SHA256 algorithm. - * - * @param delegate - * Key used to sign - * @param stringToSign - * The UTF-8-encoded string to sign. - * - * @return A {@code String} that contains the HMAC-SHA256-encoded signature. - * - * @throws InvalidKeyException - * If the accountKey is not a valid Base64-encoded string. - */ - static String delegateComputeHmac256(final UserDelegationKey delegate, String stringToSign) throws InvalidKeyException { - try { - byte[] key = Base64.getDecoder().decode(delegate.value()); - Mac hmacSha256 = Mac.getInstance("HmacSHA256"); - hmacSha256.init(new SecretKeySpec(key, "HmacSHA256")); - byte[] utf8Bytes = stringToSign.getBytes(StandardCharsets.UTF_8); - return Base64.getEncoder().encodeToString(hmacSha256.doFinal(utf8Bytes)); - } catch (final NoSuchAlgorithmException e) { - throw new Error(e); - } - } - - /** - * Appends a string to the end of a URL's path (prefixing the string with a '/' if required). - * - * @param baseURL - * The url to which the name should be appended. - * @param name - * The name to be appended. - * - * @return A url with the name appended. - * - * @throws RuntimeException - * Appending the specified name produced an invalid URL. - */ - static URL appendToURLPath(URL baseURL, String name) { - UrlBuilder url = UrlBuilder.parse(baseURL); - if (url.path() == null) { - url.path("/"); // .path() will return null if it is empty, so we have to process separately from below. - } else if (url.path().charAt(url.path().length() - 1) != '/') { - url.path(url.path() + '/'); - } - url.path(url.path() + name); - try { - return url.toURL(); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } - - static URL stripLastPathSegment(URL baseURL) { - UrlBuilder url = UrlBuilder.parse(baseURL); - if (url.path() == null || !url.path().contains("/")) { - throw new IllegalArgumentException(String.format("URL %s does not contain path segments", baseURL)); - } - - String newPath = url.path().substring(0, url.path().lastIndexOf('/')); - url.path(newPath); - try { - return url.toURL(); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } - - static T blockWithOptionalTimeout(Mono response, @Nullable Duration timeout) { - if (timeout == null) { - return response.block(); - } else { - return response.block(timeout); - } - } - - /** - * Gets the SharedKeyCredential from the HttpPipeline - * - * @param httpPipeline - * The {@code HttpPipeline} httpPipeline from which a sharedKeyCredential will be extracted - * - * @return The {@code SharedKeyCredential} sharedKeyCredential in the httpPipeline - */ - static SharedKeyCredential getSharedKeyCredential(HttpPipeline httpPipeline) { - for (int i = 0; i < httpPipeline.getPolicyCount(); i++) { - HttpPipelinePolicy httpPipelinePolicy = httpPipeline.getPolicy(i); - if (httpPipelinePolicy instanceof SharedKeyCredentialPolicy) { - SharedKeyCredentialPolicy sharedKeyCredentialPolicy = (SharedKeyCredentialPolicy) httpPipelinePolicy; - return sharedKeyCredentialPolicy.sharedKeyCredential(); - } - } - return null; - } -} diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/APISpec.groovy b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/APISpec.groovy index 1768e02fab74..20d03a45a55c 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/APISpec.groovy +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/APISpec.groovy @@ -5,17 +5,24 @@ package com.azure.storage.blob import com.azure.core.http.HttpClient import com.azure.core.http.HttpHeaders +import com.azure.core.http.HttpMethod import com.azure.core.http.HttpPipelineCallContext import com.azure.core.http.HttpPipelineNextPolicy import com.azure.core.http.HttpRequest import com.azure.core.http.HttpResponse +import com.azure.core.http.ProxyOptions import com.azure.core.http.policy.HttpLogDetailLevel import com.azure.core.http.policy.HttpPipelinePolicy -import com.azure.core.http.ProxyOptions import com.azure.core.http.rest.Response import com.azure.core.util.configuration.ConfigurationManager -import com.azure.identity.credential.EnvironmentCredential -import com.azure.storage.blob.models.* +import com.azure.identity.credential.EnvironmentCredentialBuilder +import com.azure.storage.blob.models.ContainerItem +import com.azure.storage.blob.models.CopyStatusType +import com.azure.storage.blob.models.LeaseStateType +import com.azure.storage.blob.models.Metadata +import com.azure.storage.blob.models.RetentionPolicy +import com.azure.storage.blob.models.StorageServiceProperties +import com.azure.storage.common.Constants import com.azure.storage.common.credentials.SharedKeyCredential import io.netty.buffer.ByteBuf import org.junit.Assume @@ -367,7 +374,7 @@ class APISpec extends Specification { def getMockRequest() { HttpHeaders headers = new HttpHeaders() - headers.set(Constants.HeaderConstants.CONTENT_ENCODING, "en-US") + headers.put(Constants.HeaderConstants.CONTENT_ENCODING, "en-US") URL url = new URL("http://devtest.blob.core.windows.net/test-container/test-blob") HttpRequest request = new HttpRequest(HttpMethod.POST, url, headers, null) return request @@ -567,7 +574,7 @@ class APISpec extends Specification { def getOAuthServiceURL() { return new BlobServiceClientBuilder() .endpoint(String.format("https://%s.blob.core.windows.net/", primaryCreds.accountName())) - .credential(new EnvironmentCredential()) // AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET + .credential(new EnvironmentCredentialBuilder().build()) // AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET .httpLogDetailLevel(HttpLogDetailLevel.BODY_AND_HEADERS) .buildClient() } diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobOutputStreamTest.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobOutputStreamTest.java index e7955c9b7be9..899d0f2b6595 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobOutputStreamTest.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobOutputStreamTest.java @@ -3,6 +3,7 @@ package com.azure.storage.blob; +import com.azure.storage.common.Constants; import com.azure.storage.common.credentials.SharedKeyCredential; import org.junit.Assert; import org.junit.BeforeClass; diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlockBlobInputOutputStreamTest.groovy b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlockBlobInputOutputStreamTest.groovy index 7e2e44ef221a..71396829fb10 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlockBlobInputOutputStreamTest.groovy +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlockBlobInputOutputStreamTest.groovy @@ -1,5 +1,7 @@ package com.azure.storage.blob +import com.azure.storage.common.Constants + class BlockBlobInputOutputStreamTest extends APISpec { BlockBlobClient bu diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/HelperTest.groovy b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/HelperTest.groovy index aa20ad8c4666..01000a486529 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/HelperTest.groovy +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/HelperTest.groovy @@ -7,6 +7,10 @@ import com.azure.core.http.rest.Response import com.azure.core.http.rest.VoidResponse import com.azure.storage.blob.models.BlobRange import com.azure.storage.blob.models.UserDelegationKey +import com.azure.storage.common.Constants +import com.azure.storage.common.IPRange +import com.azure.storage.common.SASProtocol +import com.azure.storage.common.Utility import com.azure.storage.common.credentials.SASTokenCredential import com.azure.storage.common.credentials.SharedKeyCredential import spock.lang.Unroll @@ -122,7 +126,7 @@ class HelperTest extends APISpec { parts.snapshot(snapshotId) bsu = new BlobClientBuilder() .endpoint(parts.toURL().toString()) - .credential(SASTokenCredential.fromQueryParameters(parts.sasQueryParameters())) + .credential(SASTokenCredential.fromSASTokenString(parts.sasQueryParameters().encode())) .buildAppendBlobClient() ByteArrayOutputStream data = new ByteArrayOutputStream() @@ -276,7 +280,7 @@ class HelperTest extends APISpec { expectedStringToSign = String.format(expectedStringToSign, Utility.ISO_8601_UTC_DATE_FORMATTER.format(v.expiryTime()), primaryCreds.accountName()) then: - token.signature() == Utility.delegateComputeHmac256(key, expectedStringToSign) + token.signature() == Utility.computeHMac256(key.value(), expectedStringToSign) /* We test string to sign functionality directly related to user delegation sas specific parameters @@ -732,6 +736,6 @@ class HelperTest extends APISpec { parts.sasQueryParameters().permissions() == "r" parts.sasQueryParameters().version() == Constants.HeaderConstants.TARGET_STORAGE_VERSION parts.sasQueryParameters().resource() == "c" - parts.sasQueryParameters().signature() == Utility.safeURLDecode("Ee%2BSodSXamKSzivSdRTqYGh7AeMVEk3wEoRZ1yzkpSc%3D") + parts.sasQueryParameters().signature() == Utility.urlDecode("Ee%2BSodSXamKSzivSdRTqYGh7AeMVEk3wEoRZ1yzkpSc%3D") } } diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SASTest.groovy b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SASTest.groovy index 45d44751d1a0..39f2ce4e5d11 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SASTest.groovy +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SASTest.groovy @@ -8,6 +8,10 @@ import com.azure.storage.blob.models.AccessPolicy import com.azure.storage.blob.models.BlobRange import com.azure.storage.blob.models.SignedIdentifier import com.azure.storage.blob.models.UserDelegationKey +import com.azure.storage.common.Constants +import com.azure.storage.common.IPRange +import com.azure.storage.common.SASProtocol +import com.azure.storage.common.Utility import com.azure.storage.common.credentials.SASTokenCredential import com.azure.storage.common.credentials.SharedKeyCredential import spock.lang.Unroll @@ -611,7 +615,7 @@ class SASTest extends APISpec { def token = v.generateSASQueryParameters(key) then: - token.signature() == Utility.delegateComputeHmac256(key, expectedStringToSign) + token.signature() == Utility.computeHMac256(key.value(), expectedStringToSign) /* We test string to sign functionality directly related to user delegation sas specific parameters @@ -849,11 +853,11 @@ class SASTest extends APISpec { where: usingUserDelegation | version | canonicalName | expiryTime | permissions | identifier | resource | snapshotId - false | null | null | null | null | null | null | null - false | Constants.HeaderConstants.TARGET_STORAGE_VERSION | null | null | null | null | null | null - false | Constants.HeaderConstants.TARGET_STORAGE_VERSION | "containerName/blobName" | null | null | null | null | null - false | Constants.HeaderConstants.TARGET_STORAGE_VERSION | "containerName/blobName" | OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC) | null | null | null | null - false | Constants.HeaderConstants.TARGET_STORAGE_VERSION | "containerName/blobName" | null | new BlobSASPermission().read(true).toString() | null | null | null + false | null | null | null | null | null | null | null + false | Constants.HeaderConstants.TARGET_STORAGE_VERSION | null | null | null | null | null | null + false | Constants.HeaderConstants.TARGET_STORAGE_VERSION | "containerName/blobName" | null | null | null | null | null + false | Constants.HeaderConstants.TARGET_STORAGE_VERSION | "containerName/blobName" | OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC) | null | null | null | null + false | Constants.HeaderConstants.TARGET_STORAGE_VERSION | "containerName/blobName" | null | new BlobSASPermission().read(true).toString() | null | null | null false | null | null | null | null | "0000" | "c" | "id" } @@ -1071,6 +1075,6 @@ class SASTest extends APISpec { parts.sasQueryParameters().permissions() == "r" parts.sasQueryParameters().version() == Constants.HeaderConstants.TARGET_STORAGE_VERSION parts.sasQueryParameters().resource() == "c" - parts.sasQueryParameters().signature() == Utility.safeURLDecode("Ee%2BSodSXamKSzivSdRTqYGh7AeMVEk3wEoRZ1yzkpSc%3D") + parts.sasQueryParameters().signature() == Utility.urlDecode("Ee%2BSodSXamKSzivSdRTqYGh7AeMVEk3wEoRZ1yzkpSc%3D") } } diff --git a/sdk/storage/azure-storage-common/pom.xml b/sdk/storage/azure-storage-common/pom.xml new file mode 100644 index 000000000000..e50ea1d13cd8 --- /dev/null +++ b/sdk/storage/azure-storage-common/pom.xml @@ -0,0 +1,84 @@ + + + + com.azure + azure-client-sdk-parent + 1.3.0 + ../../../pom.client.xml + + + 4.0.0 + + com.azure + azure-storage-common + 12.0.0-preview.3 + + azure-storage-common + https://github.com/Azure/azure-sdk-for-java + + + + azure-java-build-docs + ${site.url}/site/${project.artifactId} + + + + + scm:git:https://github.com/Azure/azure-sdk-for-java + scm:git:git@github.com:Azure/azure-sdk-for-java.git + HEAD + + + + + com.azure + azure-core + 1.0.0-preview.3 + + + org.slf4j + slf4j-api + + + + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + + + + com.azure + azure-core-test + 1.0.0-preview.3 + test + + + com.azure + azure-identity + 1.0.0-preview.2 + test + + + junit + junit + test + + + org.slf4j + slf4j-simple + test + + + io.projectreactor + reactor-test + test + + + + diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/Constants.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/Constants.java new file mode 100644 index 000000000000..3bd8920331ce --- /dev/null +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/Constants.java @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.common; + +public final class Constants { + private static final int KB = 1024; + + /** + * Constant representing a megabyte (Non-SI version). + */ + public static final int MB = 1024 * KB; + + /** + * An empty {@code String} to use for comparison. + */ + public static final String EMPTY_STRING = ""; + + /** + * The default type for content-type and accept. + */ + static final String UTF8_CHARSET = "UTF-8"; + + /** + * The query parameter for snapshots. + */ + public static final String SNAPSHOT_QUERY_PARAMETER = "snapshot"; + + static final String HTTPS = "https"; + static final String HTTPS_HTTP = "https,http"; + + private Constants() { + } + + /** + * Defines constants for use with connection strings. + */ + public static final class ConnectionStringConstants { + /** + * The AccountName key. + */ + public static final String ACCOUNT_NAME = "accountname"; + + /** + * The AccountKey key. + */ + public static final String ACCOUNT_KEY = "accountkey"; + + /** + * The DefaultEndpointProtocol key. + */ + public static final String ENDPOINT_PROTOCOL = "defaultendpointprotocol"; + + /** + * The EndpointSuffix key. + */ + public static final String ENDPOINT_SUFFIX = "endpointsuffix"; + + private ConnectionStringConstants() { + } + } + + /** + * Defines constants for use with HTTP headers. + */ + public static final class HeaderConstants { + + /** + * The current storage version header value. + */ + public static final String TARGET_STORAGE_VERSION = "2018-11-09"; + + /** + * Error code returned from the service. + */ + public static final String ERROR_CODE = "x-ms-error-code"; + + /** + * Compression type used on the body. + */ + public static final String CONTENT_ENCODING = "Content-Encoding"; + + private HeaderConstants() { + // Private to prevent construction. + } + } + + public static final class UrlConstants { + + /** + * The SAS service version parameter. + */ + public static final String SAS_SERVICE_VERSION = "sv"; + + /** + * The SAS services parameter. + */ + public static final String SAS_SERVICES = "ss"; + + /** + * The SAS resource types parameter. + */ + public static final String SAS_RESOURCES_TYPES = "srt"; + + /** + * The SAS protocol parameter. + */ + public static final String SAS_PROTOCOL = "spr"; + + /** + * The SAS start time parameter. + */ + public static final String SAS_START_TIME = "st"; + + /** + * The SAS expiration time parameter. + */ + public static final String SAS_EXPIRY_TIME = "se"; + + /** + * The SAS IP range parameter. + */ + public static final String SAS_IP_RANGE = "sip"; + + /** + * The SAS signed identifier parameter. + */ + public static final String SAS_SIGNED_IDENTIFIER = "si"; + + /** + * The SAS signed resource parameter. + */ + public static final String SAS_SIGNED_RESOURCE = "sr"; + + /** + * The SAS signed permissions parameter. + */ + public static final String SAS_SIGNED_PERMISSIONS = "sp"; + + /** + * The SAS signature parameter. + */ + public static final String SAS_SIGNATURE = "sig"; + + /** + * The SAS cache control parameter. + */ + public static final String SAS_CACHE_CONTROL = "rscc"; + + /** + * The SAS content disposition parameter. + */ + public static final String SAS_CONTENT_DISPOSITION = "rscd"; + + /** + * The SAS content encoding parameter. + */ + public static final String SAS_CONTENT_ENCODING = "rsce"; + + /** + * The SAS content language parameter. + */ + public static final String SAS_CONTENT_LANGUAGE = "rscl"; + + /** + * The SAS content type parameter. + */ + public static final String SAS_CONTENT_TYPE = "rsct"; + + /** + * The SAS signed object id parameter for user delegation SAS. + */ + public static final String SAS_SIGNED_OBJECT_ID = "skoid"; + + /** + * The SAS signed tenant id parameter for user delegation SAS. + */ + public static final String SAS_SIGNED_TENANT_ID = "sktid"; + + /** + * The SAS signed key-start parameter for user delegation SAS. + */ + public static final String SAS_SIGNED_KEY_START = "skt"; + + /** + * The SAS signed key-expiry parameter for user delegation SAS. + */ + public static final String SAS_SIGNED_KEY_EXPIRY = "ske"; + + /** + * The SAS signed service parameter for user delegation SAS. + */ + public static final String SAS_SIGNED_KEY_SERVICE = "sks"; + + /** + * The SAS signed version parameter for user delegation SAS. + */ + public static final String SAS_SIGNED_KEY_VERSION = "skv"; + + /** + * The SAS blob constant. + */ + public static final String SAS_BLOB_CONSTANT = "b"; + + /** + * The SAS blob snapshot constant. + */ + public static final String SAS_BLOB_SNAPSHOT_CONSTANT = "bs"; + + /** + * The SAS blob snapshot constant. + */ + public static final String SAS_CONTAINER_CONSTANT = "c"; + + private UrlConstants() { + // Private to prevent construction. + } + } + + static final class MessageConstants { + static final String ARGUMENT_NULL_OR_EMPTY = "The argument must not be null or an empty string. Argument name: %s."; + static final String PARAMETER_NOT_IN_RANGE = "The value of the parameter '%s' should be between %s and %s."; + static final String INVALID_DATE_STRING = "Invalid Date String: %s."; + static final String NO_PATH_SEGMENTS = "URL %s does not contain path segments."; + + private MessageConstants() { + } + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/IPRange.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/IPRange.java similarity index 64% rename from sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/IPRange.java rename to sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/IPRange.java index 9e66189fa904..ca92f4ef83a5 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/IPRange.java +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/IPRange.java @@ -1,21 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.storage.blob; +package com.azure.storage.common; /** * This type specifies a continuous range of IP addresses. It is used to limit permissions on SAS tokens. Null may be - * set if it is not desired to confine the sas permissions to an IP range. Please refer to - * {@link AccountSASSignatureValues} or {@link ServiceSASSignatureValues} for more information. + * set if it is not desired to confine the sas permissions to an IP range. */ public final class IPRange { - private String ipMin; - private String ipMax; /** - * Constructs an empty IPRange. + * Constructs an IPRange object. */ public IPRange() { } @@ -23,23 +20,22 @@ public IPRange() { /** * Creates a {@code IPRange} from the specified string. * - * @param rangeStr - * The {@code String} representation of the {@code IPRange}. - * + * @param rangeStr The {@code String} representation of the {@code IPRange}. * @return The {@code IPRange} generated from the {@code String}. */ public static IPRange parse(String rangeStr) { String[] addrs = rangeStr.split("-"); - IPRange range = new IPRange(); - range.ipMin = addrs[0]; + + IPRange range = new IPRange().ipMin(addrs[0]); if (addrs.length > 1) { - range.ipMax = addrs[1]; + range.ipMax(addrs[1]); } + return range; } /** - * @return the minimum IP address of the range. + * @return the minimum IP address of the range */ public String ipMin() { return ipMin; @@ -48,7 +44,7 @@ public String ipMin() { /** * Sets the minimum IP address of the range. * - * @param ipMin Minimum IP of the range + * @param ipMin IP address to set as the minimum * @return the updated IPRange object */ public IPRange ipMin(String ipMin) { @@ -57,8 +53,7 @@ public IPRange ipMin(String ipMin) { } /** - * - * @return the maximum IP address of the range. + * @return the maximum IP address of the range */ public String ipMax() { return ipMax; @@ -67,7 +62,7 @@ public String ipMax() { /** * Sets the maximum IP address of the range. * - * @param ipMax Maximum IP of the range + * @param ipMax IP address to set as the maximum * @return the updated IPRange object */ public IPRange ipMax(String ipMax) { @@ -84,14 +79,10 @@ public IPRange ipMax(String ipMax) { public String toString() { if (this.ipMin == null) { return ""; + } else if (this.ipMax == null) { + return this.ipMin; + } else { + return this.ipMin + "-" + this.ipMax; } - this.ipMax = this.ipMax == null ? this.ipMin : this.ipMax; - StringBuilder str = new StringBuilder(this.ipMin); - if (!this.ipMin.equals(this.ipMax)) { - str.append('-'); - str.append(this.ipMax); - } - - return str.toString(); } } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/SASProtocol.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/SASProtocol.java similarity index 82% rename from sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/SASProtocol.java rename to sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/SASProtocol.java index 475e6f4289f6..bd7120ebc93e 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/SASProtocol.java +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/SASProtocol.java @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.storage.blob; +package com.azure.storage.common; import java.util.Locale; /** - * Specifies the set of possible permissions for a shared access signature protocol. Values of this type can be used - * to set the fields on the {@link AccountSASSignatureValues} and {@link ServiceSASSignatureValues} types. + * Specifies the set of possible permissions for a shared access signature protocol. */ public enum SASProtocol { /** @@ -42,7 +41,7 @@ public static SASProtocol parse(String str) { return SASProtocol.HTTPS_HTTP; } throw new IllegalArgumentException(String.format(Locale.ROOT, - "%s could not be parsed into a SASProtocl value.", str)); + "%s could not be parsed into a SASProtocol value.", str)); } @Override diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/Utility.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/Utility.java new file mode 100644 index 000000000000..650db18fcbf3 --- /dev/null +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/Utility.java @@ -0,0 +1,440 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.common; + +import com.azure.core.http.HttpHeader; +import com.azure.core.http.HttpHeaders; +import com.azure.core.http.HttpPipeline; +import com.azure.core.http.policy.HttpPipelinePolicy; +import com.azure.core.implementation.http.UrlBuilder; +import com.azure.core.implementation.util.ImplUtils; +import com.azure.storage.common.credentials.SharedKeyCredential; +import com.azure.storage.common.policy.SharedKeyCredentialPolicy; +import reactor.core.publisher.Mono; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Base64; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Function; + +public final class Utility { + private static final String DESERIALIZED_HEADERS = "deserializedHeaders"; + private static final String ETAG = "eTag"; + + public static final DateTimeFormatter ISO_8601_UTC_DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT).withZone(ZoneId.of("UTC")); + /** + * Stores a reference to the date/time pattern with the greatest precision Java.util.Date is capable of expressing. + */ + private static final String MAX_PRECISION_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS"; + /** + * Stores a reference to the ISO8601 date/time pattern. + */ + private static final String ISO8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + /** + * Stores a reference to the ISO8601 date/time pattern. + */ + private static final String ISO8601_PATTERN_NO_SECONDS = "yyyy-MM-dd'T'HH:mm'Z'"; + /** + * The length of a datestring that matches the MAX_PRECISION_PATTERN. + */ + private static final int MAX_PRECISION_DATESTRING_LENGTH = MAX_PRECISION_PATTERN.replaceAll("'", "").length(); + + /** + *Parses the query string into a key-value pair map that maintains key, query parameter key, order. + * + * @param queryString Query string to parse + * @return a mapping of query string pieces as key-value pairs. + */ + public static TreeMap parseQueryString(final String queryString) { + TreeMap pieces = new TreeMap<>(String::compareTo); + + if (ImplUtils.isNullOrEmpty(queryString)) { + return pieces; + } + + for (String kvp : queryString.split("&")) { + int equalIndex = kvp.indexOf("="); + String key = urlDecode(kvp.substring(0, equalIndex)).toLowerCase(Locale.ROOT); + String value = urlDecode(kvp.substring(equalIndex + 1)); + + pieces.putIfAbsent(key, value); + } + + return pieces; + } + + /** + * Performs a safe decoding of the passed string, taking care to preserve each {@code +} character rather than + * replacing it with a space character. + * + * @param stringToDecode String value to decode + * @return the decoded string value + * @throws RuntimeException If the UTF-8 charset isn't supported + */ + public static String urlDecode(final String stringToDecode) { + if (ImplUtils.isNullOrEmpty(stringToDecode)) { + return ""; + } + + if (stringToDecode.contains("+")) { + StringBuilder outBuilder = new StringBuilder(); + + int startDex = 0; + for (int m = 0; m < stringToDecode.length(); m++) { + if (stringToDecode.charAt(m) == '+') { + if (m > startDex) { + outBuilder.append(decode(stringToDecode.substring(startDex, m))); + } + + outBuilder.append("+"); + startDex = m + 1; + } + } + + if (startDex != stringToDecode.length()) { + outBuilder.append(decode(stringToDecode.substring(startDex))); + } + + return outBuilder.toString(); + } else { + return decode(stringToDecode); + } + } + + /* + * Helper method to reduce duplicate calls of URLDecoder.decode + */ + private static String decode(final String stringToDecode) { + try { + return URLDecoder.decode(stringToDecode, Constants.UTF8_CHARSET); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Performs a safe encoding of the specified string, taking care to insert %20 for each space character instead of + * inserting the {@code +} character. + * + * @param stringToEncode String value to encode + * @return the encoded string value + * @throws RuntimeException If the UTF-8 charset ins't supported + */ + public static String urlEncode(final String stringToEncode) { + if (stringToEncode == null) { + return null; + } + + if (stringToEncode.length() == 0) { + return Constants.EMPTY_STRING; + } + + if (stringToEncode.contains(" ")) { + StringBuilder outBuilder = new StringBuilder(); + + int startDex = 0; + for (int m = 0; m < stringToEncode.length(); m++) { + if (stringToEncode.charAt(m) == ' ') { + if (m > startDex) { + outBuilder.append(encode(stringToEncode.substring(startDex, m))); + } + + outBuilder.append("%20"); + startDex = m + 1; + } + } + + if (startDex != stringToEncode.length()) { + outBuilder.append(encode(stringToEncode.substring(startDex))); + } + + return outBuilder.toString(); + } else { + return encode(stringToEncode); + } + } + + /* + * Helper method to reduce duplicate calls of URLEncoder.encode + */ + private static String encode(final String stringToEncode) { + try { + return URLEncoder.encode(stringToEncode, Constants.UTF8_CHARSET); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Parses the connection string into key-value pair map. + * + * @param connectionString Connection string to parse + * @return a mapping of connection string pieces as key-value pairs. + */ + public static Map parseConnectionString(final String connectionString) { + Map parts = new HashMap<>(); + + for (String part : connectionString.split(";")) { + String[] kvp = part.split("=", 2); + parts.put(kvp[0].toLowerCase(Locale.ROOT), kvp[1]); + } + + return parts; + } + + /** + * Blocks an asynchronous response with an optional timeout. + * + * @param response Asynchronous response to block + * @param timeout Optional timeout + * @param Return type of the asynchronous response + * @return the value of the asynchronous response + * @throws RuntimeException If the asynchronous response doesn't complete before the timeout expires. + */ + public static T blockWithOptionalTimeout(Mono response, Duration timeout) { + if (timeout == null) { + return response.block(); + } else { + return response.block(timeout); + } + } + + /** + * Asserts that a value is not {@code null}. + * + * @param param Name of the parameter + * @param value Value of the parameter + * @throws IllegalArgumentException If {@code value} is {@code null} + */ + public static void assertNotNull(final String param, final Object value) { + if (value == null) { + throw new IllegalArgumentException(String.format(Locale.ROOT, Constants.MessageConstants.ARGUMENT_NULL_OR_EMPTY, param)); + } + } + + /** + * Asserts that the specified number is in the valid range. The range is inclusive. + * + * @param param Name of the parameter + * @param value Value of the parameter + * @param min The minimum allowed value + * @param max The maximum allowed value + * @throws IllegalArgumentException If {@code value} is less than {@code min} or {@code value} is greater than + * {@code max}. + */ + public static void assertInBounds(final String param, final long value, final long min, final long max) { + if (value < min || value > max) { + throw new IllegalArgumentException(String.format(Locale.ROOT, Constants.MessageConstants.PARAMETER_NOT_IN_RANGE, param, min, max)); + } + } + + /** + * Given a String representing a date in a form of the ISO8601 pattern, generates a Date representing it with up to + * millisecond precision. + * + * @param dateString the {@code String} to be interpreted as a Date + * @return the corresponding Date object + * @throws IllegalArgumentException If {@code dateString} doesn't match an ISO8601 pattern + */ + public static OffsetDateTime parseDate(String dateString) { + String pattern = MAX_PRECISION_PATTERN; + switch (dateString.length()) { + case 28: // "yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'"-> [2012-01-04T23:21:59.1234567Z] length = 28 + case 27: // "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"-> [2012-01-04T23:21:59.123456Z] length = 27 + case 26: // "yyyy-MM-dd'T'HH:mm:ss.SSSSS'Z'"-> [2012-01-04T23:21:59.12345Z] length = 26 + case 25: // "yyyy-MM-dd'T'HH:mm:ss.SSSS'Z'"-> [2012-01-04T23:21:59.1234Z] length = 25 + case 24: // "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"-> [2012-01-04T23:21:59.123Z] length = 24 + dateString = dateString.substring(0, MAX_PRECISION_DATESTRING_LENGTH); + break; + case 23: // "yyyy-MM-dd'T'HH:mm:ss.SS'Z'"-> [2012-01-04T23:21:59.12Z] length = 23 + // SS is assumed to be milliseconds, so a trailing 0 is necessary + dateString = dateString.replace("Z", "0"); + break; + case 22: // "yyyy-MM-dd'T'HH:mm:ss.S'Z'"-> [2012-01-04T23:21:59.1Z] length = 22 + // S is assumed to be milliseconds, so trailing 0's are necessary + dateString = dateString.replace("Z", "00"); + break; + case 20: // "yyyy-MM-dd'T'HH:mm:ss'Z'"-> [2012-01-04T23:21:59Z] length = 20 + pattern = Utility.ISO8601_PATTERN; + break; + case 17: // "yyyy-MM-dd'T'HH:mm'Z'"-> [2012-01-04T23:21Z] length = 17 + pattern = Utility.ISO8601_PATTERN_NO_SECONDS; + break; + default: + throw new IllegalArgumentException(String.format(Locale.ROOT, Constants.MessageConstants.INVALID_DATE_STRING, dateString)); + } + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern, Locale.ROOT); + return LocalDateTime.parse(dateString, formatter).atZone(ZoneOffset.UTC).toOffsetDateTime(); + } + + /** + * Wraps any potential error responses from the service and applies post processing of the response's eTag header + * to standardize the value. + * + * @param response Response from a service call + * @param errorWrapper Error wrapping function that is applied to the response + * @param Value type of the response + * @return an updated response with post processing steps applied. + */ + public static Mono postProcessResponse(Mono response, Function, Mono> errorWrapper) { + return scrubETagHeader(errorWrapper.apply(response)); + } + + /* + The service is inconsistent in whether or not the etag header value has quotes. This method will check if the + response returns an etag value, and if it does, remove any quotes that may be present to give the user a more + predictable format to work with. + */ + private static Mono scrubETagHeader(Mono unprocessedResponse) { + return unprocessedResponse.map(response -> { + String eTag = null; + + try { + Object headers = response.getClass().getMethod(DESERIALIZED_HEADERS).invoke(response); + Method eTagGetterMethod = headers.getClass().getMethod(ETAG); + eTag = (String) eTagGetterMethod.invoke(headers); + + if (eTag == null) { + return response; + } + + eTag = eTag.replace("\"", ""); + headers.getClass().getMethod(ETAG, String.class).invoke(headers, eTag); + } catch (NoSuchMethodException ex) { + // Response did not return an eTag value. + } catch (IllegalAccessException | InvocationTargetException ex) { + // Unable to access the method or the invoked method threw an exception. + } + + try { + HttpHeaders rawHeaders = (HttpHeaders) response.getClass().getMethod("headers").invoke(response); + // + if (eTag != null) { + rawHeaders.put(ETAG, eTag); + } else { + HttpHeader eTagHeader = rawHeaders.get(ETAG); + if (eTagHeader != null && eTagHeader.value() != null) { + eTag = eTagHeader.value().replace("\"", ""); + rawHeaders.put(ETAG, eTag); + } + } + } catch (NoSuchMethodException e) { + // Response did not return an eTag value. No change necessary. + } catch (IllegalAccessException | InvocationTargetException e) { + // Unable to access the method or the invoked method threw an exception. + } + + return response; + }); + } + + /** + * Computes a signature for the specified string using the HMAC-SHA256 algorithm. + * + * @param base64Key Base64 encoded key used to sign the string + * @param stringToSign UTF-8 encoded string to sign + * @return the HMAC-SHA256 encoded signature + * @throws RuntimeException If the HMAC-SHA256 algorithm isn't support, if the key isn't a valid Base64 encoded + * string, or the UTF-8 charset isn't supported. + */ + public static String computeHMac256(final String base64Key, final String stringToSign) { + try { + byte[] key = Base64.getDecoder().decode(base64Key); + Mac hmacSHA256 = Mac.getInstance("HmacSHA256"); + hmacSHA256.init(new SecretKeySpec(key, "HmacSHA256")); + byte[] utf8Bytes = stringToSign.getBytes(StandardCharsets.UTF_8); + return Base64.getEncoder().encodeToString(hmacSHA256.doFinal(utf8Bytes)); + } catch (NoSuchAlgorithmException | InvalidKeyException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Appends a string to the end of the passed URL's path. + * + * @param baseURL URL having a path appended + * @param name Name of the path + * @return a URL with the path appended. + * @throws IllegalArgumentException If {@code name} causes the URL to become malformed. + */ + public static URL appendToURLPath(URL baseURL, String name) { + UrlBuilder builder = UrlBuilder.parse(baseURL); + + if (builder.path() == null) { + builder.path("/"); + } else if (!builder.path().endsWith("/")) { + builder.path(builder.path() + "/"); + } + + builder.path(builder.path() + name); + + try { + return builder.toURL(); + } catch (MalformedURLException ex) { + throw new IllegalArgumentException(ex); + } + } + + /** + * Strips the last path segment from the passed URL. + * + * @param baseURL URL having its last path segment stripped + * @return a URL with the path segment stripped. + * @throws IllegalArgumentException If stripping the last path segment causes the URL to become malformed or it + * doesn't contain any path segments. + */ + public static URL stripLastPathSegment(URL baseURL) { + UrlBuilder builder = UrlBuilder.parse(baseURL); + + if (builder.path() == null || !builder.path().contains("/")) { + throw new IllegalArgumentException(String.format(Locale.ROOT, Constants.MessageConstants.NO_PATH_SEGMENTS, baseURL)); + } + + builder.path(builder.path().substring(0, builder.path().lastIndexOf("/"))); + try { + return builder.toURL(); + } catch (MalformedURLException ex) { + throw new IllegalArgumentException(ex); + } + } + + /** + * Searches for a {@link SharedKeyCredential} in the passed {@link HttpPipeline}. + * + * @param httpPipeline Pipeline being searched + * @return a SharedKeyCredential if the pipeline contains one, otherwise null. + */ + public static SharedKeyCredential getSharedKeyCredential(HttpPipeline httpPipeline) { + for (int i = 0; i < httpPipeline.getPolicyCount(); i++) { + HttpPipelinePolicy httpPipelinePolicy = httpPipeline.getPolicy(i); + if (httpPipelinePolicy instanceof SharedKeyCredentialPolicy) { + SharedKeyCredentialPolicy sharedKeyCredentialPolicy = (SharedKeyCredentialPolicy) httpPipelinePolicy; + return sharedKeyCredentialPolicy.sharedKeyCredential(); + } + } + return null; + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java similarity index 61% rename from sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java rename to sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java index 8a18f449c8c9..fbf1bc0af180 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java @@ -4,12 +4,15 @@ package com.azure.storage.common.credentials; import com.azure.core.implementation.util.ImplUtils; -import com.azure.storage.blob.SASQueryParameters; + +import java.util.Map; /** * Holds a SAS token used for authenticating requests. */ public final class SASTokenCredential { + private static final String SIGNATURE = "sig"; + private final String sasToken; /** @@ -43,17 +46,26 @@ public static SASTokenCredential fromSASTokenString(String sasToken) { } /** - * Creates a SAS token credential from the passed {@link SASQueryParameters}. + * Creates a SAS token credential from the passed query string parameters. * - * @param queryParameters SAS token query parameters object + * @param queryParameters URL query parameters * @return a SAS token credential if {@code queryParameters} is not {@code null} and has - * {@link SASQueryParameters#signature() signature} set, otherwise returns {@code null}. + * the signature ("sig") query parameter, otherwise returns {@code null}. */ - public static SASTokenCredential fromQueryParameters(SASQueryParameters queryParameters) { - if (queryParameters == null || ImplUtils.isNullOrEmpty(queryParameters.signature())) { + public static SASTokenCredential fromQueryParameters(Map queryParameters) { + if (ImplUtils.isNullOrEmpty(queryParameters) || !queryParameters.containsKey(SIGNATURE)) { return null; } - return new SASTokenCredential(queryParameters.encode()); + StringBuilder sb = new StringBuilder(); + for (Map.Entry kvp : queryParameters.entrySet()) { + if (sb.length() != 0) { + sb.append("&"); + } + + sb.append(kvp.getKey()).append("=").append(kvp.getValue()); + } + + return new SASTokenCredential(sb.toString()); } } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/credentials/SharedKeyCredential.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/credentials/SharedKeyCredential.java similarity index 79% rename from sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/credentials/SharedKeyCredential.java rename to sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/credentials/SharedKeyCredential.java index 2f2b0111a91e..c71a22064016 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/credentials/SharedKeyCredential.java +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/credentials/SharedKeyCredential.java @@ -4,16 +4,11 @@ package com.azure.storage.common.credentials; import com.azure.core.implementation.util.ImplUtils; +import com.azure.storage.common.Utility; import io.netty.handler.codec.http.QueryStringDecoder; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; -import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -33,7 +28,7 @@ public final class SharedKeyCredential { private static final String ACCOUNT_KEY = "accountkey"; private final String accountName; - private final byte[] accountKey; + private final String accountKey; /** * Initializes a new instance of SharedKeyCredential contains an account's name and its primary or secondary @@ -46,7 +41,7 @@ public SharedKeyCredential(String accountName, String accountKey) { Objects.requireNonNull(accountName); Objects.requireNonNull(accountKey); this.accountName = accountName; - this.accountKey = Base64.getDecoder().decode(accountKey); + this.accountKey = accountKey; } /** @@ -89,7 +84,8 @@ public String accountName() { * @return the SharedKey authorization value */ public String generateAuthorizationHeader(URL requestURL, String httpMethod, Map headers) { - return computeHMACSHA256(buildStringToSign(requestURL, httpMethod, headers)); + String signature = Utility.computeHMac256(accountKey, buildStringToSign(requestURL, httpMethod, headers)); + return String.format(AUTHORIZATION_HEADER_FORMAT, accountName, signature); } /** @@ -98,23 +94,11 @@ public String generateAuthorizationHeader(URL requestURL, String httpMethod, Map * * @param stringToSign The UTF-8-encoded string to sign. * @return A {@code String} that contains the HMAC-SHA256-encoded signature. - * @throws InvalidKeyException If the accountKey is not a valid Base64-encoded string. - * @throws RuntimeException If the {@code HmacSHA256} algorithm isn't supported. + * @throws RuntimeException If the HMAC-SHA256 algorithm isn't support, if the key isn't a valid Base64 encoded + * string, or the UTF-8 charset isn't supported. */ - public String computeHmac256(final String stringToSign) throws InvalidKeyException { - try { - /* - We must get a new instance of the Mac calculator for each signature calculated because the instances are - not threadsafe and there is some suggestion online that they may not even be safe for reuse, so we use a - new one each time to be sure. - */ - Mac hmacSha256 = Mac.getInstance("HmacSHA256"); - hmacSha256.init(new SecretKeySpec(this.accountKey, "HmacSHA256")); - byte[] utf8Bytes = stringToSign.getBytes(StandardCharsets.UTF_8); - return Base64.getEncoder().encodeToString(hmacSha256.doFinal(utf8Bytes)); - } catch (final NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } + public String computeHmac256(final String stringToSign) { + return Utility.computeHMac256(accountKey, stringToSign); } private String buildStringToSign(URL requestURL, String httpMethod, Map headers) { @@ -216,16 +200,4 @@ private String getCanonicalizedResource(URL requestURL) { // append to main string builder the join of completed params with new line return canonicalizedResource.toString(); } - - private String computeHMACSHA256(String stringToSign) { - try { - Mac hmacSha256 = Mac.getInstance("HmacSHA256"); - hmacSha256.init(new SecretKeySpec(accountKey, "HmacSHA256")); - byte[] utf8Bytes = stringToSign.getBytes(StandardCharsets.UTF_8); - String signature = Base64.getEncoder().encodeToString(hmacSha256.doFinal(utf8Bytes)); - return String.format(AUTHORIZATION_HEADER_FORMAT, accountName, signature); - } catch (NoSuchAlgorithmException | InvalidKeyException ex) { - throw new Error(ex); - } - } } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/credentials/package-info.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/credentials/package-info.java similarity index 100% rename from sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/credentials/package-info.java rename to sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/credentials/package-info.java diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/RequestRetryOptions.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/RequestRetryOptions.java similarity index 90% rename from sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/RequestRetryOptions.java rename to sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/RequestRetryOptions.java index ae0519ee6a03..a99897511901 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/RequestRetryOptions.java +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/RequestRetryOptions.java @@ -16,8 +16,11 @@ public final class RequestRetryOptions { private final int tryTimeout; private final long retryDelayInMs; private final long maxRetryDelayInMs; - private final RetryPolicyType retryPolicyType; - private final String secondaryHost; + /** + * A {@link RetryPolicyType} telling the pipeline what kind of retry policy to use. + */ + private RetryPolicyType retryPolicyType; + private String secondaryHost; /** * Constructor with default retry values: Exponential backoff, maxTries=4, tryTimeout=30, retryDelayInMs=4000, @@ -54,11 +57,6 @@ public RequestRetryOptions() { * webpage * @throws IllegalArgumentException If {@code retryDelayInMs} and {@code maxRetryDelayInMs} are not both null or * non-null or {@code retryPolicyType} isn't {@link RetryPolicyType#EXPONENTIAL} or {@link RetryPolicyType#FIXED}. - * - *

Sample Code

- * - *

For more samples, please see the samples - * file

*/ public RequestRetryOptions(RetryPolicyType retryPolicyType, Integer maxTries, Integer tryTimeout, Long retryDelayInMs, Long maxRetryDelayInMs, String secondaryHost) { @@ -100,40 +98,40 @@ public RequestRetryOptions(RetryPolicyType retryPolicyType, Integer maxTries, In } this.maxRetryDelayInMs = TimeUnit.SECONDS.toMillis(120); } - this.secondaryHost = secondaryHost; } /** - * @return the maximum number attempts that will be retried before the operation finally fails. + * @return the maximum number of retries that will be attempted. */ public int maxTries() { return this.maxTries; } /** - * @return the timeout in seconds allowed for each retry operation. + * @return the maximum time, in seconds, allowed for a request until it is considered timed out. */ public int tryTimeout() { return this.tryTimeout; } /** - * @return the secondary host that retries could be attempted against. + * @return the URI of the secondary host where retries are attempted. If this is null then there is no secondary + * host and all retries are attempted against the original host. */ public String secondaryHost() { return this.secondaryHost; } /** - * @return the delay in milliseconds between retry attempts. + * @return the delay in milliseconds between each retry attempt. */ public long retryDelayInMs() { return retryDelayInMs; } /** - * @return the maximum delay in milliseconds between retry attempts. + * @return the maximum delay in milliseconds allowed between each retry. */ public long maxRetryDelayInMs() { return maxRetryDelayInMs; diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/RequestRetryPolicy.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/RequestRetryPolicy.java similarity index 93% rename from sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/RequestRetryPolicy.java rename to sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/RequestRetryPolicy.java index 83bef0b2b49f..22b62af89b04 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/RequestRetryPolicy.java +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/RequestRetryPolicy.java @@ -44,7 +44,7 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN boolean considerSecondary = (this.requestRetryOptions.secondaryHost() != null) && (HttpMethod.GET.equals(context.httpRequest().httpMethod()) || HttpMethod.HEAD.equals(context.httpRequest().httpMethod())); - return attemptAsync(context, next, context.httpRequest(), considerSecondary, 1, 1); + return this.attemptAsync(context, next, context.httpRequest(), considerSecondary, 1, 1); } /** @@ -58,15 +58,18 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN * secondary, ignore the retry count and wait (.1 second * random(0.8, 1.2)) * * @param context The request to try. - * @param next The next policy to apply to the request - * @param originalRequest The unmodified original request - * @param primaryTry This indicates how man tries we've attempted against the primary DC. + * @param next The next policy to apply to the request. + * @param originalRequest The unmodified original request. + * @param considerSecondary Before each try, we'll select either the primary or secondary URL if appropriate. + * @param primaryTry Number of attempts against the primary DC. * @param attempt This indicates the total number of attempts to send the request. * @return A single containing either the successful response or an error that was not retryable because either the * maxTries was exceeded or retries will not mitigate the issue. */ - private Mono attemptAsync(final HttpPipelineCallContext context, HttpPipelineNextPolicy next, final HttpRequest originalRequest, - boolean considerSecondary, final int primaryTry, final int attempt) { + private Mono attemptAsync(final HttpPipelineCallContext context, HttpPipelineNextPolicy next, + final HttpRequest originalRequest, final boolean considerSecondary, + final int primaryTry, final int attempt) { + // Determine which endpoint to try. It's primary if there is no secondary or if it is an odd number attempt. final boolean tryingPrimary = !considerSecondary || (attempt % 2 != 0); @@ -137,7 +140,6 @@ we do not consider the secondary at all (considerSecondary==false)). This will int newPrimaryTry = (!tryingPrimary || !considerSecondary) ? primaryTry + 1 : primaryTry; return attemptAsync(context, next, originalRequest, newConsiderSecondary, newPrimaryTry, attempt + 1); } - return Mono.just(response); }).onErrorResume(throwable -> { /* @@ -179,7 +181,6 @@ we do not consider the secondary at all (considerSecondary==false)). This will int newPrimaryTry = (!tryingPrimary || !considerSecondary) ? primaryTry + 1 : primaryTry; return attemptAsync(context, next, originalRequest, considerSecondary, newPrimaryTry, attempt + 1); } - return Mono.error(throwable); }); } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/RetryPolicyType.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/RetryPolicyType.java similarity index 100% rename from sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/RetryPolicyType.java rename to sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/RetryPolicyType.java diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/SASTokenCredentialPolicy.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/SASTokenCredentialPolicy.java similarity index 99% rename from sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/SASTokenCredentialPolicy.java rename to sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/SASTokenCredentialPolicy.java index ba2debfcd362..4af3edf7f229 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/SASTokenCredentialPolicy.java +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/SASTokenCredentialPolicy.java @@ -22,6 +22,7 @@ public final class SASTokenCredentialPolicy implements HttpPipelinePolicy { /** * Creates a SAS token credential policy that appends the SAS token to the request URL's query. + * * @param credential SAS token credential */ public SASTokenCredentialPolicy(SASTokenCredential credential) { diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/SharedKeyCredentialPolicy.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/SharedKeyCredentialPolicy.java similarity index 100% rename from sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/SharedKeyCredentialPolicy.java rename to sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/SharedKeyCredentialPolicy.java diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/package-info.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/package-info.java similarity index 100% rename from sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/common/policy/package-info.java rename to sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/package-info.java diff --git a/sdk/storage/ci.yml b/sdk/storage/ci.yml index a69f0e790a1f..4279cde3364c 100644 --- a/sdk/storage/ci.yml +++ b/sdk/storage/ci.yml @@ -34,9 +34,11 @@ stages: parameters: ServiceDirectory: storage Artifacts: + - name: azure-storage-common + safeName: azurestoragecommon - name: azure-storage-blob safeName: azurestorageblob - name: azure-storage-file safeName: azurestoragefile - name: azure-storage-queue - safeName: azurestoragequeue \ No newline at end of file + safeName: azurestoragequeue diff --git a/sdk/storage/pom.service.xml b/sdk/storage/pom.service.xml index d17c28020efb..6f995e563f24 100644 --- a/sdk/storage/pom.service.xml +++ b/sdk/storage/pom.service.xml @@ -13,6 +13,7 @@ ../core/azure-core ../core/azure-core-test ../identity/azure-identity + azure-storage-common azure-storage-blob azure-storage-file azure-storage-queue