Skip to content
Draft
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,14 @@ 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.Blob.URI_SUBDOMAIN)) {
parts.setAccountName(StorageImplUtils.getAccountNameFromHost(host, Constants.Blob.URI_SUBDOMAIN));
} else if (url.toString().contains(Constants.Dfs.URI_SUBDOMAIN)) {
parts.setAccountName(StorageImplUtils.getAccountNameFromHost(host, Constants.Dfs.URI_SUBDOMAIN));
} else {
throw LOGGER.logExceptionAsError(
new IllegalArgumentException("Host does not contain the expected subdomain. Host: " + host));
}

// find the container & blob names (if any)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.storage.blob;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

public class BlobUrlPartsTests {

@ParameterizedTest
@MethodSource("urlSupplier")
public void testParseUrl(String uriString) throws MalformedURLException {
// Arrange
URL originalUri = new URL(uriString);

// Act
BlobUrlParts blobUriBuilder = BlobUrlParts.parse(originalUri);
String newUri = blobUriBuilder.toUrl().toString();

// Assert
assertEquals("https", blobUriBuilder.getScheme());
assertEquals("myaccount", blobUriBuilder.getAccountName());
assertEquals("", blobUriBuilder.getBlobContainerName());
assertNull(blobUriBuilder.getBlobName());
assertNull(blobUriBuilder.getSnapshot());
assertEquals("", blobUriBuilder.getCommonSasQueryParameters().encode());
assertEquals(originalUri.toString(), newUri);
}

public static Stream<Arguments> urlSupplier() {
return Stream.of(Arguments.of("https://myaccount.blob.core.windows.net/"),
Arguments.of("https://myaccount-secondary.blob.core.windows.net/"),
Arguments.of("https://myaccount-dualstack.blob.core.windows.net/"),
Arguments.of("https://myaccount-ipv6.blob.core.windows.net/"),
Arguments.of("https://myaccount-secondary-dualstack.blob.core.windows.net/"),
Arguments.of("https://myaccount-secondary-ipv6.blob.core.windows.net/"));
}
}
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 @@ -463,4 +463,66 @@ private UrlConstants() {
// Private to prevent construction.
}
}

/**
* Constants for the Blob service subdomain.
*/
public static class Blob {
/**
* The URI subdomain for blob storage.
*/
public static final String URI_SUBDOMAIN = "blob";
}

/**
* Constants for the File service subdomain.
*/
public static final class File {
/**
* The URI subdomain for file storage.
*/
public static final String URI_SUBDOMAIN = "file";

private File() {
}
}

/**
* Constants for the Queue service subdomain.
*/
public static final class Queue {
/**
* The URI subdomain for queue storage.
*/
public static final String URI_SUBDOMAIN = "queue";

private Queue() {
}
}

/**
* Constants for the Table service subdomain.
*/
public static final class Table {
/**
* The URI subdomain for table storage.
*/
public static final String URI_SUBDOMAIN = "table";

private Table() {
}
}

/**
* Constants for the DFS (Data Lake) service subdomain.
*/
public static final class Dfs {
/**
* The URI subdomain for Data Lake Storage.
*/
public static final String URI_SUBDOMAIN = "dfs";

private Dfs() {
}
}
}
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
Loading
Loading