From 099476d6f3175d8624bcf57c064b11085ed329ef Mon Sep 17 00:00:00 2001 From: Jesse Squire Date: Wed, 15 May 2024 11:18:41 -0700 Subject: [PATCH] [Messaging] Relax emulator endpoint restrictions (#44052) The focus of these changes is to remove the restriction that the endpoint used in connection strings must resolve to a variant of `localhost` when using the development emulator. A fix to parsing was also made to be resilient to an endpoint with no scheme that specifies a custom port. Previously, this would parse incorrectly and be rejected. The scenario is now detected and parsed. Also included is a package bump for the AMQP transport library and release prep for the Event Hubs core package. --- eng/Packages.Data.props | 2 +- .../src/Resources.Designer.cs | 12 --- .../src/Resources.resx | 3 - .../Azure.Messaging.EventHubs/CHANGELOG.md | 12 +-- .../src/Azure.Messaging.EventHubs.csproj | 2 +- .../EventHubsConnectionStringProperties.cs | 44 ++++++++-- ...ventHubsConnectionStringPropertiesTests.cs | 84 ++++++++++++------- .../Azure.Messaging.ServiceBus/CHANGELOG.md | 4 + .../ServiceBusConnectionStringProperties.cs | 37 +++++++- ...rviceBusConnectionStringPropertiesTests.cs | 81 ++++++++++++------ 10 files changed, 191 insertions(+), 90 deletions(-) diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props index 525bb344c4ed..3212df5094fd 100644 --- a/eng/Packages.Data.props +++ b/eng/Packages.Data.props @@ -151,7 +151,7 @@ - + diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.Designer.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.Designer.cs index 600764973f2e..889f5675c93d 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.Designer.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.Designer.cs @@ -242,18 +242,6 @@ internal static string InvalidConnectionString } } - /// - /// Looks up a localized string similar to The Event Hubs emulator is only available locally. The endpoint must reference to the local host.. - /// - internal static string InvalidEmulatorEndpoint - { - get - { - return ResourceManager.GetString("InvalidEmulatorEndpoint", resourceCulture); - } - } - - /// /// Looks up a localized string similar to The string has an invalid encoding format.. /// diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.resx b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.resx index 4cdb0914155e..11f936463320 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.resx +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Resources.resx @@ -342,9 +342,6 @@ For troubleshooting information, see https://aka.ms/azsdk/net/eventhubs/exceptions/troubleshoot - - The Event Hubs emulator is only available locally. The endpoint must reference to the local host. - The buffered producer took too long to start. diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/CHANGELOG.md b/sdk/eventhub/Azure.Messaging.EventHubs/CHANGELOG.md index e681c113dc96..7f3b273077be 100755 --- a/sdk/eventhub/Azure.Messaging.EventHubs/CHANGELOG.md +++ b/sdk/eventhub/Azure.Messaging.EventHubs/CHANGELOG.md @@ -1,16 +1,16 @@ # Release History -## 5.12.0-beta.1 (Unreleased) - -### Features Added - -### Breaking Changes +## 5.11.3 (2024-05-15) ### Bugs Fixed +- Fixed an error that caused connection strings using host names without a scheme to fail parsing and be considered invalid. + ### Other Changes -- Updated the `Microsoft.Azure.Amqp` dependency to 2.6.6, which includes a bug fix for an internal `NullReferenceException` that would sometimes impact creating new links. _(see: [#258](https://github.com/azure/azure-amqp/issues/258))_ +- Removed the restriction that endpoints used with the development emulator had to resolve to a `localhost` variant. + +- Updated the `Microsoft.Azure.Amqp` dependency to 2.6.7, which contains several bug fixes, including for an internal `NullReferenceException` that would sometimes impact creating new links. _(see: [#258](https://github.com/azure/azure-amqp/issues/258))_ ## 5.11.2 (2024-04-10) diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/src/Azure.Messaging.EventHubs.csproj b/sdk/eventhub/Azure.Messaging.EventHubs/src/Azure.Messaging.EventHubs.csproj index 3d57a798efff..5adf68ae3a2f 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/src/Azure.Messaging.EventHubs.csproj +++ b/sdk/eventhub/Azure.Messaging.EventHubs/src/Azure.Messaging.EventHubs.csproj @@ -1,7 +1,7 @@ Azure Event Hubs is a highly scalable publish-subscribe service that can ingest millions of events per second and stream them to multiple consumers. This client library allows for both publishing and consuming events using Azure Event Hubs. For more information about Event Hubs, see https://azure.microsoft.com/en-us/services/event-hubs/ - 5.12.0-beta.1 + 5.11.3 5.11.2 Azure;Event Hubs;EventHubs;.NET;AMQP;IoT;$(PackageCommonTags) diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubsConnectionStringProperties.cs b/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubsConnectionStringProperties.cs index 388c74788caf..0c5482decd9a 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubsConnectionStringProperties.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/src/EventHubsConnectionStringProperties.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Text; using Azure.Core; @@ -244,13 +245,6 @@ internal void Validate(string explicitEventHubName, { throw new ArgumentException(Resources.MissingConnectionInformation, connectionStringArgumentName); } - - // Ensure that the namespace reflects the local host when the development emulator is being used. - - if ((UseDevelopmentEmulator) && (!Endpoint.IsLoopback)) - { - throw new ArgumentException(Resources.InvalidEmulatorEndpoint, connectionStringArgumentName); - } } /// @@ -334,6 +328,17 @@ public static EventHubsConnectionStringProperties Parse(string connectionString) { endpointUri = null; } + else if (string.IsNullOrEmpty(endpointUri.Host) && (CountChar(':', value.AsSpan()) == 1)) + { + // If the host was empty after parsing and the value has a single port/scheme separator, + // then the parsing likely failed to recognize the host due to the lack of a scheme. Add + // an artificial scheme and try to parse again. + + if (!Uri.TryCreate(string.Concat(EventHubsEndpointScheme, value), UriKind.Absolute, out endpointUri)) + { + endpointUri = null; + } + } var endpointBuilder = endpointUri switch { @@ -400,5 +405,30 @@ public static EventHubsConnectionStringProperties Parse(string connectionString) return parsedValues; } + + /// + /// Counts the number of times a character occurs in a given span. + /// + /// + /// The span to evaluate. + /// The character to count. + /// + /// The number of times the occurs in . + /// + private static int CountChar(char value, + ReadOnlySpan span) + { + var count = 0; + + foreach (var character in span) + { + if (character == value) + { + ++count; + } + } + + return count; + } } } diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Core/EventHubsConnectionStringPropertiesTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Core/EventHubsConnectionStringPropertiesTests.cs index d3364a312481..d4746f12b5a7 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs/tests/Core/EventHubsConnectionStringPropertiesTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/tests/Core/EventHubsConnectionStringPropertiesTests.cs @@ -347,7 +347,7 @@ public void ParseIgnoresUnknownTokens() [TestCase("amqp://test.endpoint.com")] [TestCase("http://test.endpoint.com")] [TestCase("https://test.endpoint.com:8443")] - public void ParseDoesAcceptsHostNamesAndUrisForTheEndpoint(string endpointValue) + public void ParseAcceptsHostNamesAndUrisForTheEndpoint(string endpointValue) { var connectionString = $"Endpoint={ endpointValue };EntityPath=dummy"; var parsed = EventHubsConnectionStringProperties.Parse(connectionString); @@ -371,7 +371,7 @@ public void ParseDoesAcceptsHostNamesAndUrisForTheEndpoint(string endpointValue) /// /// [Test] - [TestCase("test.endpoint.com:443")] + [TestCase("test-endpoint|com:443")] [TestCase("notvalid=[broke]")] public void ParseDoesNotAllowAnInvalidEndpointFormat(string endpointValue) { @@ -402,6 +402,7 @@ public void ParseConsidersMissingValuesAsMalformed(string connectionString) /// method. /// /// + [Test] public void ParseDetectsDevelopmentEmulatorUse() { var connectionString = "Endpoint=localhost:1234;SharedAccessKeyName=[name];SharedAccessKey=[value];UseDevelopmentEmulator=true"; @@ -416,6 +417,7 @@ public void ParseDetectsDevelopmentEmulatorUse() /// method. /// /// + [Test] public void ParseRespectsDevelopmentEmulatorValue() { var connectionString = "Endpoint=localhost:1234;SharedAccessKeyName=[name];SharedAccessKey=[value];UseDevelopmentEmulator=false"; @@ -425,6 +427,57 @@ public void ParseRespectsDevelopmentEmulatorValue() Assert.That(parsed.UseDevelopmentEmulator, Is.False, "The development emulator flag should have been unset."); } + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCase("localhost")] + [TestCase("localhost:9084")] + [TestCase("127.0.0.1")] + [TestCase("local.docker.com")] + [TestCase("local.docker.com:8080")] + [TestCase("www.fake.com")] + [TestCase("www.fake.com:443")] + public void ParseRespectsTheEndpointForDevelopmentEmulatorValue(string host) + { + var connectionString = $"Endpoint={ host };SharedAccessKeyName=[name];SharedAccessKey=[value];UseDevelopmentEmulator=true"; + var endpoint = new Uri(string.Concat(GetEventHubsEndpointScheme(), host)); + var parsed = EventHubsConnectionStringProperties.Parse(connectionString); + + Assert.That(parsed.Endpoint.Host, Is.EqualTo(endpoint.Host), "The endpoint hosts should match."); + Assert.That(parsed.Endpoint.Port, Is.EqualTo(endpoint.Port), "The endpoint ports should match."); + Assert.That(parsed.UseDevelopmentEmulator, Is.True, "The development emulator flag should have been set."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCase("sb://localhost")] + [TestCase("http://localhost:9084")] + [TestCase("sb://local.docker.com")] + [TestCase("amqps://local.docker.com:8080")] + [TestCase("sb://www.fake.com")] + [TestCase("amqp://www.fake.com:443")] + public void ParseRespectsTheUrlFormatEndpointForDevelopmentEmulatorValue(string host) + { + var endpoint = new UriBuilder(host) + { + Scheme = GetEventHubsEndpointScheme() + }; + + var connectionString = $"Endpoint={ host };SharedAccessKeyName=[name];SharedAccessKey=[value];UseDevelopmentEmulator=true"; + var parsed = EventHubsConnectionStringProperties.Parse(connectionString); + + Assert.That(parsed.Endpoint.Host, Is.EqualTo(endpoint.Host), "The endpoint hosts should match."); + Assert.That(parsed.Endpoint.Port, Is.EqualTo(endpoint.Port), "The endpoint ports should match."); + Assert.That(parsed.UseDevelopmentEmulator, Is.True, "The development emulator flag should have been set."); + } + /// /// Verifies functionality of the /// method. @@ -657,33 +710,6 @@ public void ValidateAllowsSharedAccessSignatureAuthorization() Assert.That(() => properties.Validate(eventHubName, "dummy"), Throws.Nothing, "Validation should accept the shared access signature authorization."); } - /// - /// Verifies functionality of the - /// method. - /// - /// - [Test] - [TestCase("localhost", true)] - [TestCase("127.0.0.1", true)] - [TestCase("www.microsoft.com", false)] - [TestCase("fake.servicebus.windows.net", false)] - [TestCase("weirdname:8080", false)] - public void ValidateRequiresLocalEndpointForDevelopmentEmulator(string endpoint, - bool isValid) - { - var fakeConnection = $"Endpoint=sb://{ endpoint };SharedAccessSignature=[not_real];UseDevelopmentEmulator=true"; - var properties = EventHubsConnectionStringProperties.Parse(fakeConnection); - - if (isValid) - { - Assert.That(() => properties.Validate("fake", "dummy"), Throws.Nothing, "Validation should allow a local endpoint."); - } - else - { - Assert.That(() => properties.Validate("fake", "dummy"), Throws.ArgumentException.And.Message.StartsWith(Resources.InvalidEmulatorEndpoint), "Validation should enforce that the endpoint is a local address."); - } - } - /// /// Compares two instances for /// structural equality. diff --git a/sdk/servicebus/Azure.Messaging.ServiceBus/CHANGELOG.md b/sdk/servicebus/Azure.Messaging.ServiceBus/CHANGELOG.md index 3e1d861433d1..b82ea620b4eb 100644 --- a/sdk/servicebus/Azure.Messaging.ServiceBus/CHANGELOG.md +++ b/sdk/servicebus/Azure.Messaging.ServiceBus/CHANGELOG.md @@ -8,8 +8,12 @@ ### Bugs Fixed +- Fixed an error that caused connection strings using host names without a scheme to fail parsing and be considered invalid. + ### Other Changes +- Updated the `Microsoft.Azure.Amqp` dependency to 2.6.7, which contains a fix for decoding messages with a null format code as the body. + ## 7.18.0-beta.1 (2024-05-08) ### Features Added diff --git a/sdk/servicebus/Azure.Messaging.ServiceBus/src/Primitives/ServiceBusConnectionStringProperties.cs b/sdk/servicebus/Azure.Messaging.ServiceBus/src/Primitives/ServiceBusConnectionStringProperties.cs index 1b56fe418e66..33c6ed1e7529 100644 --- a/sdk/servicebus/Azure.Messaging.ServiceBus/src/Primitives/ServiceBusConnectionStringProperties.cs +++ b/sdk/servicebus/Azure.Messaging.ServiceBus/src/Primitives/ServiceBusConnectionStringProperties.cs @@ -307,6 +307,17 @@ public static ServiceBusConnectionStringProperties Parse(string connectionString { endpointUri = null; } + else if (string.IsNullOrEmpty(endpointUri.Host) && (CountChar(':', value.AsSpan()) == 1)) + { + // If the host was empty after parsing and the value has a single port/scheme separator, + // then the parsing likely failed to recognize the host due to the lack of a scheme. Add + // an artificial scheme and try to parse again. + + if (!Uri.TryCreate($"{ServiceBusEndpointSchemeName}://{value}", UriKind.Absolute, out endpointUri)) + { + endpointUri = null; + } + } var endpointBuilder = endpointUri switch { @@ -371,14 +382,32 @@ public static ServiceBusConnectionStringProperties Parse(string connectionString lastPosition = currentPosition; } - // Enforce that the development emulator can only be used for local development. + return parsedValues; + } - if ((parsedValues.UseDevelopmentEmulator) && (!parsedValues.Endpoint.IsLoopback)) + /// + /// Counts the number of times a character occurs in a given span. + /// + /// + /// The span to evaluate. + /// The character to count. + /// + /// The number of times the occurs in . + /// + private static int CountChar(char value, + ReadOnlySpan span) + { + var count = 0; + + foreach (var character in span) { - throw new ArgumentException("The Service Bus emulator is only available locally. The endpoint must reference to the local host.", connectionString); + if (character == value) + { + ++count; + } } - return parsedValues; + return count; } } } diff --git a/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Primitives/ServiceBusConnectionStringPropertiesTests.cs b/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Primitives/ServiceBusConnectionStringPropertiesTests.cs index ed3c3b410be2..2674b3eb8bfa 100644 --- a/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Primitives/ServiceBusConnectionStringPropertiesTests.cs +++ b/sdk/servicebus/Azure.Messaging.ServiceBus/tests/Primitives/ServiceBusConnectionStringPropertiesTests.cs @@ -363,7 +363,7 @@ public void ParseDoesAcceptsHostNamesAndUrisForTheEndpoint(string endpointValue) /// /// [Test] - [TestCase("test.endpoint.com:443")] + [TestCase("test-endpoint|com:443")] [TestCase("notvalid=[broke]")] public void ParseDoesNotAllowAnInvalidEndpointFormat(string endpointValue) { @@ -394,6 +394,7 @@ public void ParseConsidersMissingValuesAsMalformed(string connectionString) /// method. /// /// + [Test] public void ParseDetectsDevelopmentEmulatorUse() { var connectionString = "Endpoint=localhost:1234;SharedAccessKeyName=[name];SharedAccessKey=[value];UseDevelopmentEmulator=true"; @@ -408,6 +409,7 @@ public void ParseDetectsDevelopmentEmulatorUse() /// method. /// /// + [Test] public void ParseRespectsDevelopmentEmulatorValue() { var connectionString = "Endpoint=localhost:1234;SharedAccessKeyName=[name];SharedAccessKey=[value];UseDevelopmentEmulator=false"; @@ -417,6 +419,57 @@ public void ParseRespectsDevelopmentEmulatorValue() Assert.That(parsed.UseDevelopmentEmulator, Is.False, "The development emulator flag should have been unset."); } + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCase("localhost")] + [TestCase("localhost:9084")] + [TestCase("127.0.0.1")] + [TestCase("local.docker.com")] + [TestCase("local.docker.com:8080")] + [TestCase("www.fake.com")] + [TestCase("www.fake.com:443")] + public void ParseRespectsTheEndpointForDevelopmentEmulatorValue(string host) + { + var connectionString = $"Endpoint={ host };SharedAccessKeyName=[name];SharedAccessKey=[value];UseDevelopmentEmulator=true"; + var endpoint = new Uri(string.Concat(GetServiceBusEndpointScheme(), host)); + var parsed = ServiceBusConnectionStringProperties.Parse(connectionString); + + Assert.That(parsed.Endpoint.Host, Is.EqualTo(endpoint.Host), "The endpoint hosts should match."); + Assert.That(parsed.Endpoint.Port, Is.EqualTo(endpoint.Port), "The endpoint ports should match."); + Assert.That(parsed.UseDevelopmentEmulator, Is.True, "The development emulator flag should have been set."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCase("sb://localhost")] + [TestCase("http://localhost:9084")] + [TestCase("sb://local.docker.com")] + [TestCase("amqps://local.docker.com:8080")] + [TestCase("sb://www.fake.com")] + [TestCase("amqp://www.fake.com:443")] + public void ParseRespectsTheUrlFormatEndpointForDevelopmentEmulatorValue(string host) + { + var endpoint = new UriBuilder(host) + { + Scheme = GetServiceBusEndpointScheme() + }; + + var connectionString = $"Endpoint={ host };SharedAccessKeyName=[name];SharedAccessKey=[value];UseDevelopmentEmulator=true"; + var parsed = ServiceBusConnectionStringProperties.Parse(connectionString); + + Assert.That(parsed.Endpoint.Host, Is.EqualTo(endpoint.Host), "The endpoint hosts should match."); + Assert.That(parsed.Endpoint.Port, Is.EqualTo(endpoint.Port), "The endpoint ports should match."); + Assert.That(parsed.UseDevelopmentEmulator, Is.True, "The development emulator flag should have been set."); + } + /// /// Verifies functionality of the /// method. @@ -583,32 +636,6 @@ public void ToConnectionStringAllowsSharedAccessSignatureAuthorization() Assert.That(() => properties.ToConnectionString(), Throws.Nothing, "Validation should accept the shared access signature authorization."); } - /// - /// Verifies functionality of the - /// method. - /// - /// - [Test] - [TestCase("localhost", true)] - [TestCase("127.0.0.1", true)] - [TestCase("www.microsoft.com", false)] - [TestCase("fake.servicebus.windows.net", false)] - [TestCase("weirdname:8080", false)] - public void ValidateRequiresLocalEndpointForDevelopmentEmulator(string endpoint, - bool isValid) - { - var fakeConnection = $"Endpoint=sb://{ endpoint };SharedAccessSignature=[not_real];UseDevelopmentEmulator=true"; - - if (isValid) - { - Assert.That(() => ServiceBusConnectionStringProperties.Parse(fakeConnection), Throws.Nothing, "Validation should allow a local endpoint."); - } - else - { - Assert.That(() => ServiceBusConnectionStringProperties.Parse(fakeConnection), Throws.ArgumentException.And.Message.Contains("local"), "Parse should enforce that the endpoint is a local address."); - } - } - /// /// Compares two instances for /// structural equality.