Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.azure.storage.common.implementation.SasImplUtils;
import com.azure.storage.common.sas.CommonSasQueryParameters;
import com.azure.storage.common.implementation.Constants;
import com.azure.storage.common.implementation.StorageImplUtils;

import java.net.MalformedURLException;
import java.net.URL;
Expand Down Expand Up @@ -451,17 +452,13 @@ private static void parseNonIpUrl(URL url, BlobUrlParts parts) {
String host = url.getHost();
parts.setHost(host);

//Parse host to get account name
// host will look like this : <accountname>.blob.core.windows.net
if (!CoreUtils.isNullOrEmpty(host)) {
int accountNameIndex = host.indexOf('.');
if (accountNameIndex == -1) {
// host only contains account name
parts.setAccountName(host);
} else {
// if host is separated by .
parts.setAccountName(host.substring(0, accountNameIndex));
}
// Parse host to get account name
if (url.toString().contains(Constants.UrlConstants.BLOB_URI_SUBDOMAIN)) {
parts.setAccountName(
StorageImplUtils.getAccountNameFromHost(host, Constants.UrlConstants.BLOB_URI_SUBDOMAIN));
} else {
parts.setAccountName(
StorageImplUtils.getAccountNameFromHost(host, Constants.UrlConstants.DFS_URI_SUBDOMAIN));
}

// find the container & blob names (if any)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
import reactor.test.StepVerifier;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -47,6 +49,7 @@
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -523,6 +526,54 @@ public void throwsOnAmbiguousCredentialsWithAzureSasCredential() {
.buildClient());
}

@ParameterizedTest
@MethodSource("blobAccountNameSupplier")
void secondaryIpv6Dualstack(String urlString, String expectedAccountName) throws MalformedURLException {
BlobUrlParts blobUrlParts = BlobUrlParts.parse(new URL(urlString));

assertEquals("https", blobUrlParts.getScheme());
assertEquals(expectedAccountName, blobUrlParts.getAccountName());
assertEquals("", blobUrlParts.getBlobContainerName());
assertNull(blobUrlParts.getSnapshot());
assertEquals("", blobUrlParts.getCommonSasQueryParameters().encode());
assertNull(blobUrlParts.getVersionId());

// Verify the endpoint can be used to reconstruct the original URL
String newUri = blobUrlParts.toUrl().toString();
assertEquals(urlString, newUri);
}

private static Stream<Arguments> blobAccountNameSupplier() {
return Stream.of(Arguments.of("https://myaccount.blob.core.windows.net/", "myaccount"),
Arguments.of("https://myaccount-secondary.blob.core.windows.net/", "myaccount"),
Arguments.of("https://myaccount-dualstack.blob.core.windows.net/", "myaccount"),
Arguments.of("https://myaccount-ipv6.blob.core.windows.net/", "myaccount"),
Arguments.of("https://myaccount-secondary-dualstack.blob.core.windows.net/", "myaccount"),
Arguments.of("https://myaccount-secondary-ipv6.blob.core.windows.net/", "myaccount"));
}

@ParameterizedTest
@MethodSource("blobManagedDiskAccountNameSupplier")
void ipv6InternalAccounts(String urlString, String expectedAccountName) throws MalformedURLException {
BlobUrlParts blobUrlParts = BlobUrlParts.parse(new URL(urlString));

assertEquals("https", blobUrlParts.getScheme());
assertEquals(expectedAccountName, blobUrlParts.getAccountName());
assertEquals("", blobUrlParts.getBlobContainerName());
assertNull(blobUrlParts.getSnapshot());
assertEquals("", blobUrlParts.getCommonSasQueryParameters().encode());
assertNull(blobUrlParts.getVersionId());

// Verify the endpoint can be used to reconstruct the original URL
String newUri = blobUrlParts.toUrl().toString();
assertEquals(urlString, newUri);
}

private static Stream<Arguments> blobManagedDiskAccountNameSupplier() {
return Stream.of(Arguments.of("https://md-d3rqxhqbxbwq.blob.core.windows.net/", "md-d3rqxhqbxbwq"),
Arguments.of("https://md-ssd-bndub02px100c21.blob.core.windows.net/", "md-ssd-bndub02px100c21"));
}

@Test
public void onlyOneRetryOptionsCanBeApplied() {
assertThrows(IllegalStateException.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,31 @@ private HeaderConstants() {
*/
public static final class UrlConstants {

/**
* DNS subdomain label for the Blob service ({@code account}.blob.{suffix}).
*/
public static final String BLOB_URI_SUBDOMAIN = "blob";

/**
* DNS subdomain label for the Azure Files service ({@code account}.file.{suffix}).
*/
public static final String FILE_URI_SUBDOMAIN = "file";

/**
* DNS subdomain label for the Queue service ({@code account}.queue.{suffix}).
*/
public static final String QUEUE_URI_SUBDOMAIN = "queue";

/**
* DNS subdomain label for the Table service ({@code account}.table.{suffix}).
*/
public static final String TABLE_URI_SUBDOMAIN = "table";

/**
* DNS subdomain label for Data Lake Storage ({@code account}.dfs.{suffix}).
*/
public static final String DFS_URI_SUBDOMAIN = "dfs";

/**
* The snapshot parameters.
*/
Expand Down Expand Up @@ -463,4 +488,5 @@ private UrlConstants() {
// Private to prevent construction.
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,58 @@ public static String getAccountName(URL url) {
return accountName;
}

public static String getAccountName(URL url, String serviceSubDomain) {
String host = url.getHost();
return getAccountNameFromHost(host, serviceSubDomain);
}

/**
* Gets the account name from a host string, stripping IPv6, dualstack, and secondary suffixes.
* <p>
* For IPv6/dualstack endpoints, hosts look like {@code accountname-ipv6.blob.core.windows.net} or
* {@code accountname-secondary-dualstack.blob.core.windows.net}. This method extracts the base account name
* by verifying the service subdomain is present, then stripping known suffixes.
* <p>
* Suffixes are stripped in order: first {@code -ipv6} or {@code -dualstack}, then {@code -secondary},
* to handle compound cases like {@code accountname-secondary-ipv6}.
*
* @param host The host string from a URL.
* @param serviceSubDomain The service subdomain (e.g., "blob", "file", "queue", "dfs").
* @return The account name, or {@code null} if it cannot be parsed.
*/
public static String getAccountNameFromHost(String host, String serviceSubDomain) {
if (CoreUtils.isNullOrEmpty(host)) {
return null;
}

int accountEndIndex = host.indexOf('.');
if (accountEndIndex >= 0) {
int serviceStartIndex = host.indexOf(serviceSubDomain, accountEndIndex);
if (serviceStartIndex > -1) {
String accountName = host.substring(0, accountEndIndex);

// Note: The suffixes are specifically checked/trimmed in this order to
// take into account of cases with both "-secondary" and "-ipv6"/"-dualstack"
// ie. "accountname-secondary-ipv6"

// Remove "-ipv6" or "-dualstack" from end if present
if (accountName.endsWith("-ipv6")) {
accountName = accountName.substring(0, accountName.length() - "-ipv6".length());
} else if (accountName.endsWith("-dualstack")) {
accountName = accountName.substring(0, accountName.length() - "-dualstack".length());
}

// Remove "-secondary" from end if present
if (accountName.endsWith("-secondary")) {
accountName = accountName.substring(0, accountName.length() - "-secondary".length());
}

return accountName;
}
}
return null;
}

/** Returns an empty string if value is {@code null}, otherwise returns value
* @param value The value to check and return.
* @return The value or empty string.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.azure.storage.common.implementation.SasImplUtils;
import org.junit.jupiter.api.Test;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
Expand Down Expand Up @@ -359,4 +361,19 @@ public void overrideDefaultProtocolToHttp() {
.equalsIgnoreCase(String.format("http://%s.blob.%s", // http protocol
ACCOUNT_NAME_VALUE, CHINA_CLOUD_ENDPOINT_SUFFIX)));
}

@Test
public void parseIPv6ConnectionString() throws MalformedURLException {
String accountName = "storagesample";
String blobEndpoint = "https://" + accountName + ".blob.core.windows.net";
String connectionString = String.format(
"DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=123=;BlobEndpoint=%s;EndpointSuffix=core.windows.net",
accountName, blobEndpoint);
StorageConnectionString storageConnectionString = StorageConnectionString.create(connectionString, LOGGER);

assertNotNull(storageConnectionString);
assertEquals(accountName, storageConnectionString.getAccountName());
assertEquals((new URL(blobEndpoint)).toString(), storageConnectionString.getBlobEndpoint().getPrimaryUri());

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
import com.azure.core.util.ClientOptions;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.logging.ClientLogger;
import com.azure.storage.blob.BlobUrlParts;
import com.azure.storage.common.StorageSharedKeyCredential;
import com.azure.storage.common.policy.RequestRetryOptions;
import com.azure.storage.common.policy.RetryPolicyType;
import com.azure.storage.file.datalake.implementation.util.BuilderHelper;
import com.azure.storage.file.datalake.implementation.util.DataLakeImplUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
Expand All @@ -30,6 +32,8 @@
import reactor.test.StepVerifier;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Map;
Expand All @@ -40,6 +44,7 @@
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -232,6 +237,53 @@ public void pathClientCustomApplicationIdInUAString(String logOptionsUA, String
.verifyComplete();
}

@ParameterizedTest
@MethodSource("dfsAccountNameSupplier")
void secondaryIpv6Dualstack(String urlString, String expectedAccountName) throws MalformedURLException {
BlobUrlParts blobUrlParts = BlobUrlParts.parse(new URL(urlString));

assertEquals("https", blobUrlParts.getScheme());
assertEquals(expectedAccountName, blobUrlParts.getAccountName());
assertEquals("", blobUrlParts.getBlobContainerName());
assertNull(blobUrlParts.getSnapshot());
assertEquals("", blobUrlParts.getCommonSasQueryParameters().encode());
assertNull(blobUrlParts.getVersionId());

String newUri = DataLakeImplUtils.endpointToDesiredEndpoint(blobUrlParts.toUrl().toString(), "dfs", "blob");
assertEquals(urlString, newUri);
}

private static Stream<Arguments> dfsAccountNameSupplier() {
return Stream.of(Arguments.of("https://myaccount.dfs.core.windows.net/", "myaccount"),
Arguments.of("https://myaccount-secondary.dfs.core.windows.net/", "myaccount"),
Arguments.of("https://myaccount-dualstack.dfs.core.windows.net/", "myaccount"),
Arguments.of("https://myaccount-ipv6.dfs.core.windows.net/", "myaccount"),
Arguments.of("https://myaccount-secondary-dualstack.dfs.core.windows.net/", "myaccount"),
Arguments.of("https://myaccount-secondary-ipv6.dfs.core.windows.net/", "myaccount"));
}

@ParameterizedTest
@MethodSource("dfsManagedDiskAccountNameSupplier")
void ipv6InternalAccounts(String urlString, String expectedAccountName) throws MalformedURLException {
String blobUrlString = DataLakeImplUtils.endpointToDesiredEndpoint(urlString, "blob", "dfs");
BlobUrlParts blobUrlParts = BlobUrlParts.parse(new URL(blobUrlString));

assertEquals("https", blobUrlParts.getScheme());
assertEquals(expectedAccountName, blobUrlParts.getAccountName());
assertEquals("", blobUrlParts.getBlobContainerName());
assertNull(blobUrlParts.getSnapshot());
assertEquals("", blobUrlParts.getCommonSasQueryParameters().encode());
assertNull(blobUrlParts.getVersionId());

String newUri = DataLakeImplUtils.endpointToDesiredEndpoint(blobUrlParts.toUrl().toString(), "dfs", "blob");
assertEquals(urlString, newUri);
}

private static Stream<Arguments> dfsManagedDiskAccountNameSupplier() {
return Stream.of(Arguments.of("https://md-d3rqxhqbxbwq.dfs.core.windows.net/", "md-d3rqxhqbxbwq"),
Arguments.of("https://md-ssd-bndub02px100c21.dfs.core.windows.net/", "md-ssd-bndub02px100c21"));
}

@Test
public void doesNotThrowOnAmbiguousCredentialsWithoutAzureSasCredential() {
assertDoesNotThrow(() -> new DataLakeFileSystemClientBuilder().endpoint(ENDPOINT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.azure.storage.common.implementation.BuilderUtils;
import com.azure.storage.common.implementation.Constants;
import com.azure.storage.common.implementation.SasImplUtils;
import com.azure.storage.common.implementation.StorageImplUtils;
import com.azure.storage.common.implementation.credentials.CredentialValidator;
import com.azure.storage.common.policy.MetadataValidationPolicy;
import com.azure.storage.common.policy.RequestRetryOptions;
Expand Down Expand Up @@ -205,19 +206,9 @@ public static String getAccountName(URL url) {
String[] pathPieces = path.split("/", 1);
return (pathPieces.length == 1) ? pathPieces[0] : null;
} else {
// URL is using a pattern of http://accountName.blob.core.windows.net
// URL is using a pattern of http://accountName.file.core.windows.net
String host = url.getHost();

if (CoreUtils.isNullOrEmpty(host)) {
return null;
}

int accountNameIndex = host.indexOf('.');
if (accountNameIndex == -1) {
return host;
} else {
return host.substring(0, accountNameIndex);
}
return StorageImplUtils.getAccountNameFromHost(host, Constants.UrlConstants.FILE_URI_SUBDOMAIN);
}
}

Expand Down Expand Up @@ -269,16 +260,8 @@ public static ShareUrlParts parseEndpoint(String endpoint, ClientLogger logger)
} else {
// URL is using a pattern of http://accountName.file.core.windows.net/shareName
String host = url.getHost();

String accountName = null;
if (!CoreUtils.isNullOrEmpty(host)) {
int accountNameIndex = host.indexOf('.');
if (accountNameIndex == -1) {
accountName = host;
} else {
accountName = host.substring(0, accountNameIndex);
}
}
String accountName
= StorageImplUtils.getAccountNameFromHost(host, Constants.UrlConstants.FILE_URI_SUBDOMAIN);

parts.setAccountName(accountName);

Expand Down
Loading
Loading