From 05d83431e2d6e3876b9da4e3af8a6407ad6b34e8 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Tue, 1 Dec 2020 09:41:18 -0600 Subject: [PATCH] Properly create secondary endpoint Uri for Azurite endpoints (#17246) fixes #17215 --- sdk/tables/Azure.Data.Tables/CHANGELOG.md | 3 ++ .../src/TableConnectionString.cs | 47 +++++++++++++++++-- .../src/TableServiceClient.cs | 18 +++---- .../tests/TableConnectionStringTests.cs | 34 +++++++++++++- 4 files changed, 89 insertions(+), 13 deletions(-) diff --git a/sdk/tables/Azure.Data.Tables/CHANGELOG.md b/sdk/tables/Azure.Data.Tables/CHANGELOG.md index 0948f69a75ad9..d36d5cf6fa37e 100644 --- a/sdk/tables/Azure.Data.Tables/CHANGELOG.md +++ b/sdk/tables/Azure.Data.Tables/CHANGELOG.md @@ -2,6 +2,9 @@ ## 3.0.0-beta.4 (Unreleased) +### Fixed + +- Properly create secondary endpoint Uri for Azurite endpoints ## 3.0.0-beta.3 (2020-11-12) diff --git a/sdk/tables/Azure.Data.Tables/src/TableConnectionString.cs b/sdk/tables/Azure.Data.Tables/src/TableConnectionString.cs index 6b80452fc4fc4..f33644d09c136 100644 --- a/sdk/tables/Azure.Data.Tables/src/TableConnectionString.cs +++ b/sdk/tables/Azure.Data.Tables/src/TableConnectionString.cs @@ -190,7 +190,7 @@ string settingOrDefault(string key) var matchesAutomaticEndpointsSpec = settings.TryGetSegmentValue(TableConstants.ConnectionStrings.AccountNameSetting, out var accountName) && settings.TryGetSegmentValue(TableConstants.ConnectionStrings.AccountKeySetting, out var accountKey) && - (settings.TryGetSegmentValue(TableConstants.ConnectionStrings.TableEndpointSetting, out accountName) || + (settings.TryGetSegmentValue(TableConstants.ConnectionStrings.TableEndpointSetting, out var primary) || settings.TryGetSegmentValue(TableConstants.ConnectionStrings.EndpointSuffixSetting, out var endpointSuffix)); if (matchesAutomaticEndpointsSpec || matchesExplicitEndpointsSpec) @@ -212,7 +212,10 @@ static bool IsValidEndpointPair(string primary, string secondary) => { if (!string.IsNullOrWhiteSpace(primary)) { - return (CreateUri(primary, sasToken), CreateUri(secondary, sasToken)); + Uri primaryUri = CreateUri(primary, sasToken); + Uri secondaryUri = CreateUri(secondary, sasToken) ?? GetSecondaryUriFromPrimary(primaryUri, accountName); + + return (primaryUri, secondaryUri); } else if (matchesAutomaticEndpointsSpec && factory != null) { @@ -266,6 +269,44 @@ static Uri CreateUri(string endpoint, string sasToken) return false; } + internal static Uri GetSecondaryUriFromPrimary(Uri primaryUri, string accountName = null) + { + var secondaryUriBuilder = new UriBuilder(primaryUri); + + if (!string.IsNullOrEmpty(accountName)) + { + // We've been provided the accountName, so just insert the '-secondary' suffix after it + var indexOfAccountName = secondaryUriBuilder.Host.IndexOf(accountName, StringComparison.OrdinalIgnoreCase); + secondaryUriBuilder.Host = secondaryUriBuilder.Host.Insert(indexOfAccountName + accountName.Length, TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix); + return secondaryUriBuilder.Uri; + } + + var indexOfDot = secondaryUriBuilder.Host.IndexOf('.'); + if (indexOfDot >= 0 && Uri.CheckHostName(primaryUri.Host) == UriHostNameType.Dns) + { + // This is a dns name such as contoso.core.windows.net + // Insert the '-secondary' suffix after the first part of the host name + secondaryUriBuilder.Host = secondaryUriBuilder.Host.Insert(indexOfDot, TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix); + } + else if (primaryUri.IsLoopback) + { + // This is most likely Azurite, which looks like this: https://127.0.0.1:10002/contoso/ + // Insert the '-secondary' suffix after the 2nd segment (the first segment is '/') + var segments = primaryUri.Segments; + var accountNameSegmentLength = segments[1].Length; + var insertIndex = segments[1].EndsWith("/", StringComparison.OrdinalIgnoreCase) ? accountNameSegmentLength - 1 : accountNameSegmentLength; + segments[1] = segments[1].Insert(insertIndex, TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix); + secondaryUriBuilder.Path = string.Join(string.Empty, segments); + } + else + { + // this is not a valid host name + return default; + } + + return secondaryUriBuilder.Uri; + } + /// /// Returns a with development storage credentials using the specified proxy Uri. /// @@ -403,7 +444,7 @@ private static (Uri, Uri) ConstructUris( } /// - /// Gets the default queue endpoint using the specified protocol and account name. + /// Gets the default table endpoint using the specified protocol and account name. /// /// The protocol to use. /// The name of the storage account. diff --git a/sdk/tables/Azure.Data.Tables/src/TableServiceClient.cs b/sdk/tables/Azure.Data.Tables/src/TableServiceClient.cs index ef0fe61aba471..888feb5d67b4f 100644 --- a/sdk/tables/Azure.Data.Tables/src/TableServiceClient.cs +++ b/sdk/tables/Azure.Data.Tables/src/TableServiceClient.cs @@ -21,7 +21,7 @@ public class TableServiceClient private readonly OdataMetadataFormat _format = OdataMetadataFormat.ApplicationJsonOdataMinimalmetadata; private readonly string _version; internal readonly bool _isPremiumEndpoint; - private readonly QueryOptions _defaultQueryOptions= new QueryOptions() { Format = OdataMetadataFormat.ApplicationJsonOdataMinimalmetadata}; + private readonly QueryOptions _defaultQueryOptions = new QueryOptions() { Format = OdataMetadataFormat.ApplicationJsonOdataMinimalmetadata }; /// /// Initializes a new instance of the using the specified containing a shared access signature (SAS) @@ -124,8 +124,8 @@ public TableServiceClient(string connectionString, TableClientOptions options = TableConnectionString connString = TableConnectionString.Parse(connectionString); options ??= new TableClientOptions(); - var endpointString = connString.TableStorageUri.PrimaryUri.ToString(); - var secondaryEndpoint = connString.TableStorageUri.PrimaryUri?.ToString() ?? endpointString.Insert(endpointString.IndexOf('.'), "-secondary"); + var endpointString = connString.TableStorageUri.PrimaryUri.AbsoluteUri; + var secondaryEndpoint = connString.TableStorageUri.SecondaryUri?.AbsoluteUri; TableSharedKeyPipelinePolicy policy = connString.Credentials switch { @@ -147,8 +147,8 @@ internal TableServiceClient(Uri endpoint, TableSharedKeyPipelinePolicy policy, T Argument.AssertNotNull(endpoint, nameof(endpoint)); options ??= new TableClientOptions(); - var endpointString = endpoint.ToString(); - var secondaryEndpoint = endpointString.Insert(endpointString.IndexOf('.'), "-secondary"); + var endpointString = endpoint.AbsoluteUri; + string secondaryEndpoint = TableConnectionString.GetSecondaryUriFromPrimary(endpoint)?.AbsoluteUri; HttpPipeline pipeline = HttpPipelineBuilder.Build(options, policy); _version = options.VersionString; @@ -314,7 +314,7 @@ public virtual Response CreateTable(string tableName, CancellationTok scope.Start(); try { - var response = _tableOperations.Create(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken); + var response = _tableOperations.Create(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken); return Response.FromValue(response.Value as TableItem, response.GetRawResponse()); } catch (Exception ex) @@ -337,7 +337,7 @@ public virtual async Task> CreateTableAsync(string tableName scope.Start(); try { - var response = await _tableOperations.CreateAsync(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + var response = await _tableOperations.CreateAsync(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken).ConfigureAwait(false); return Response.FromValue(response.Value as TableItem, response.GetRawResponse()); } catch (Exception ex) @@ -360,7 +360,7 @@ public virtual Response CreateTableIfNotExists(string tableName, Canc scope.Start(); try { - var response = _tableOperations.Create(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken); + var response = _tableOperations.Create(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken); return Response.FromValue(response.Value as TableItem, response.GetRawResponse()); } catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.Conflict) @@ -387,7 +387,7 @@ public virtual async Task> CreateTableIfNotExistsAsync(strin scope.Start(); try { - var response = await _tableOperations.CreateAsync(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + var response = await _tableOperations.CreateAsync(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken).ConfigureAwait(false); return Response.FromValue(response.Value as TableItem, response.GetRawResponse()); } catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.Conflict) diff --git a/sdk/tables/Azure.Data.Tables/tests/TableConnectionStringTests.cs b/sdk/tables/Azure.Data.Tables/tests/TableConnectionStringTests.cs index 292e6c223dde0..a9d56977f82a6 100644 --- a/sdk/tables/Azure.Data.Tables/tests/TableConnectionStringTests.cs +++ b/sdk/tables/Azure.Data.Tables/tests/TableConnectionStringTests.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; using Azure.Data.Tables; using NUnit.Framework; @@ -10,7 +12,7 @@ namespace Azure.Tables.Tests { public class TableConnectionStringTests { - private const string AccountName = "accountName"; + private const string AccountName = "accountname"; private const string SasToken = "sv=2019-12-12&ss=t&srt=s&sp=rwdlacu&se=2020-08-28T23:45:30Z&st=2020-08-26T15:45:30Z&spr=https&sig=mySig"; private const string Secret = "Kg=="; private readonly TableSharedKeyCredential _expectedCred = new TableSharedKeyCredential(AccountName, Secret); @@ -47,6 +49,7 @@ public void ParsesStorage(string connString) Assert.That(tcs.Credentials, Is.Not.Null); Assert.That(GetCredString(tcs.Credentials), Is.EqualTo(GetExpectedHash(_expectedCred)), "The Credentials should have matched."); Assert.That(tcs.TableStorageUri.PrimaryUri, Is.EqualTo(new Uri($"https://{AccountName}.table.core.windows.net/")), "The PrimaryUri should have matched."); + Assert.That(tcs.TableStorageUri.SecondaryUri, Is.EqualTo(new Uri($"https://{AccountName}{TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix}.table.core.windows.net/")), "The SecondaryUri should have matched."); } public static IEnumerable ValidCosmosConnStrings() @@ -66,6 +69,7 @@ public void ParsesCosmos(string connString) Assert.That(tcs.Credentials, Is.Not.Null); Assert.That(GetCredString(tcs.Credentials), Is.EqualTo(GetExpectedHash(_expectedCred)), "The Credentials should have matched."); Assert.That(tcs.TableStorageUri.PrimaryUri, Is.EqualTo(new Uri($"https://{AccountName}.table.cosmos.azure.com:443/")), "The PrimaryUri should have matched."); + Assert.That(tcs.TableStorageUri.SecondaryUri, Is.EqualTo(new Uri($"https://{AccountName}{TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix}.table.cosmos.azure.com:443/")), "The SecondaryUri should have matched."); } /// @@ -80,6 +84,7 @@ public void ParsesSaS() Assert.That(tcs.Credentials, Is.Not.Null); Assert.That(GetCredString(tcs.Credentials), Is.EqualTo(SasToken), "The Credentials should have matched."); Assert.That(tcs.TableStorageUri.PrimaryUri, Is.EqualTo(new Uri($"https://{AccountName}.table.core.windows.net/?{SasToken}")), "The PrimaryUri should have matched."); + Assert.That(tcs.TableStorageUri.SecondaryUri, Is.EqualTo(new Uri($"https://{AccountName}{TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix}.table.core.windows.net/?{SasToken}")), "The SecondaryUri should have matched."); } public static IEnumerable InvalidConnStrings() @@ -101,6 +106,33 @@ public void ParseFailsWithInvalidConnString(string connString) Assert.That(TableConnectionString.TryParse(connString, out TableConnectionString tcs), Is.False, "Parsing should not have been successful"); } + [Test] + public void GetSecondaryUriFromPrimaryCosmos() + { + Uri secondaryEndpoint = TableConnectionString.GetSecondaryUriFromPrimary(new Uri($"https://{AccountName}.table.cosmos.azure.com:443/")); + + Assert.That(secondaryEndpoint, Is.Not.Null.Or.Empty, "Secondary endpoint should not be null or empty"); + Assert.That(secondaryEndpoint.AbsoluteUri, Is.EqualTo(new Uri($"https://{AccountName}{TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix}.table.cosmos.azure.com:443/").AbsoluteUri)); + } + + [Test] + public void GetSecondaryUriFromPrimaryStorage() + { + Uri secondaryEndpoint = TableConnectionString.GetSecondaryUriFromPrimary(new Uri($"https://{AccountName}.table.core.windows.net/")); + + Assert.That(secondaryEndpoint, Is.Not.Null.Or.Empty, "Secondary endpoint should not be null or empty"); + Assert.That(secondaryEndpoint.AbsoluteUri, Is.EqualTo(new Uri($"https://{AccountName}{TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix}.table.core.windows.net/"))); + } + + [Test] + public void GetSecondaryUriFromPrimaryAzurite() + { + Uri secondaryEndpoint = TableConnectionString.GetSecondaryUriFromPrimary(new Uri($"https://127.0.0.1:10002/{AccountName}/")); + + Assert.That(secondaryEndpoint, Is.Not.Null.Or.Empty, "Secondary endpoint should not be null or empty"); + Assert.That(secondaryEndpoint.AbsoluteUri, Is.EqualTo(new Uri($"https://127.0.0.1:10002/{AccountName}{TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix}/"))); + } + private string GetExpectedHash(TableSharedKeyCredential cred) => cred.ComputeHMACSHA256("message"); private string GetCredString(object credential) => credential switch