From 445cdd17ab52f5f8e949cae17520c2cba9527ea7 Mon Sep 17 00:00:00 2001 From: inikulshin Date: Mon, 22 Jan 2024 20:17:58 +0200 Subject: [PATCH] LeaveDotsAndSlashesEscaped for .NET Framework (#314) LeaveDotsAndSlashesEscaped for .NET Framework --- .../EasyNetQ.Management.Client.Tests.csproj | 2 +- .../ManagementClientConstructorTests.cs | 68 +++++++++++++---- .../LegacyEndpointBuilder.cs | 8 +- .../ManagementClient.cs | 74 ++++++++++++++++++- 4 files changed, 130 insertions(+), 22 deletions(-) diff --git a/Source/EasyNetQ.Management.Client.Tests/EasyNetQ.Management.Client.Tests.csproj b/Source/EasyNetQ.Management.Client.Tests/EasyNetQ.Management.Client.Tests.csproj index 50a38d29..122103ae 100644 --- a/Source/EasyNetQ.Management.Client.Tests/EasyNetQ.Management.Client.Tests.csproj +++ b/Source/EasyNetQ.Management.Client.Tests/EasyNetQ.Management.Client.Tests.csproj @@ -1,7 +1,7 @@ - net7 + net48;net7.0 latest enable diff --git a/Source/EasyNetQ.Management.Client.Tests/ManagementClientConstructorTests.cs b/Source/EasyNetQ.Management.Client.Tests/ManagementClientConstructorTests.cs index 2148078b..5ca35ce2 100644 --- a/Source/EasyNetQ.Management.Client.Tests/ManagementClientConstructorTests.cs +++ b/Source/EasyNetQ.Management.Client.Tests/ManagementClientConstructorTests.cs @@ -1,4 +1,9 @@ -namespace EasyNetQ.Management.Client.Tests; +#if NETFRAMEWORK +using System; +using System.Reflection; +#endif + +namespace EasyNetQ.Management.Client.Tests; public class ManagementClientConstructorTests { @@ -23,27 +28,60 @@ public void Host_should_have_correct_url_for_ssl() } [Theory] - [InlineData("localhost", true)] - [InlineData("127.0.0.1", true)] - [InlineData("::1", false)] - [InlineData("2001:db8:1111::50", false)] - [InlineData("[::1]", true)] - [InlineData("[2001:db8:1111::50]", true)] - [InlineData("[[2001:db8:1111::50]]", false)] - public void Host_url_should_be_legal(string url, bool isValid) + [InlineData("localhost")] + [InlineData("127.0.0.1")] + [InlineData("[::1]")] + [InlineData("[2001:db8:1111::50]")] + public void Host_url_should_be_legal(string url) { #pragma warning disable CS0618 - var exception = Record.Exception(() => new ManagementClient(url, "user", "password")); + using var client = new ManagementClient(url, "user", "password"); #pragma warning restore CS0618 + } - if (isValid) + [Theory] + [InlineData("::1")] + [InlineData("2001:db8:1111::50")] + [InlineData("[[2001:db8:1111::50]]")] + public void Host_url_should_be_illegal(string url) + { +#pragma warning disable CS0618 + var exception = Assert.Throws(() => new ManagementClient(url, "user", "password")); +#pragma warning restore CS0618 + + exception.Message.Should().Be("hostUrl is illegal"); + } + +#if NETFRAMEWORK + [Fact] + public void UriParser_quirks_should_be_fixed() + { + // Enable legacy quirks for https. Must be done before first creation of ManagementClient with https. + string scheme = Uri.UriSchemeHttps; + + // https://mikehadlow.blogspot.com/2011/08/how-to-stop-systemuri-un-escaping.html + var getSyntaxMethod = + typeof(UriParser).GetMethod("GetSyntax", BindingFlags.Static | BindingFlags.NonPublic); + if (getSyntaxMethod == null) { - exception.Should().BeNull(); + throw new MissingMethodException("UriParser", "GetSyntax"); } - else + var uriParser = getSyntaxMethod.Invoke(null, new object[] { scheme })!; + + var setUpdatableFlagsMethod = + uriParser.GetType().GetMethod("SetUpdatableFlags", BindingFlags.Instance | BindingFlags.NonPublic); + if (setUpdatableFlagsMethod == null) { - exception.Should().BeOfType(); - exception.Message.Should().Be("hostUrl is illegal"); + throw new MissingMethodException("UriParser", "SetUpdatableFlags"); } + setUpdatableFlagsMethod.Invoke(uriParser, new object[] { 0x2000000 }); + + string uriWithEscapedDotsAndSlashes = $"{scheme}://localhost/{Uri.EscapeDataString("/.")}"; + new Uri(uriWithEscapedDotsAndSlashes).ToString().Should().NotBe(uriWithEscapedDotsAndSlashes); + + new ManagementClient(new Uri($"{scheme.ToUpper()}://localhost:15672"), "guest", "guest"); + + new Uri(uriWithEscapedDotsAndSlashes).ToString().Should().Be(uriWithEscapedDotsAndSlashes); } +#endif } diff --git a/Source/EasyNetQ.Management.Client/LegacyEndpointBuilder.cs b/Source/EasyNetQ.Management.Client/LegacyEndpointBuilder.cs index 8ee78b98..adb159a2 100644 --- a/Source/EasyNetQ.Management.Client/LegacyEndpointBuilder.cs +++ b/Source/EasyNetQ.Management.Client/LegacyEndpointBuilder.cs @@ -14,13 +14,13 @@ public static Uri Build(string hostUrl, int portNumber, bool ssl) if (ssl) { - if (hostUrl.Contains("http://")) throw new ArgumentException("hostUrl is illegal"); - hostUrl = hostUrl.Contains("https://") ? hostUrl : "https://" + hostUrl; + if (hostUrl.StartsWith("http://")) throw new ArgumentException("hostUrl is illegal"); + hostUrl = hostUrl.StartsWith("https://") ? hostUrl : "https://" + hostUrl; } else { - if (hostUrl.Contains("https://")) throw new ArgumentException("hostUrl is illegal"); - hostUrl = hostUrl.Contains("http://") ? hostUrl : "http://" + hostUrl; + if (hostUrl.StartsWith("https://")) throw new ArgumentException("hostUrl is illegal"); + hostUrl = hostUrl.StartsWith("http://") ? hostUrl : "http://" + hostUrl; } if (!UrlRegex.IsMatch(hostUrl)) throw new ArgumentException("hostUrl is illegal"); diff --git a/Source/EasyNetQ.Management.Client/ManagementClient.cs b/Source/EasyNetQ.Management.Client/ManagementClient.cs index 7561abda..7a666d74 100644 --- a/Source/EasyNetQ.Management.Client/ManagementClient.cs +++ b/Source/EasyNetQ.Management.Client/ManagementClient.cs @@ -11,6 +11,9 @@ #if NET6_0 using HttpHandler = System.Net.Http.SocketsHttpHandler; #else +using System.Collections.Concurrent; +using System.Reflection; + using HttpHandler = System.Net.Http.HttpClientHandler; #endif @@ -70,6 +73,49 @@ static ManagementClient() }; } +#if NETSTANDARD2_0 + private static bool DisableUriParserLegacyQuirks(string scheme) + { + string uriWithEscapedDotsAndSlashes = $"{scheme}://localhost/{Uri.EscapeDataString("/.")}"; + string uriParsed = new Uri(uriWithEscapedDotsAndSlashes).ToString(); + if (uriParsed == uriWithEscapedDotsAndSlashes) + { + return false; + } + + // https://mikehadlow.blogspot.com/2011/08/how-to-stop-systemuri-un-escaping.html + var getSyntaxMethod = + typeof(UriParser).GetMethod("GetSyntax", BindingFlags.Static | BindingFlags.NonPublic); + if (getSyntaxMethod == null) + { + throw new MissingMethodException("UriParser", "GetSyntax"); + } + var uriParser = getSyntaxMethod.Invoke(null, new object[] { scheme })!; + + var setUpdatableFlagsMethod = + uriParser.GetType().GetMethod("SetUpdatableFlags", BindingFlags.Instance | BindingFlags.NonPublic); + if (setUpdatableFlagsMethod == null) + { + throw new MissingMethodException("UriParser", "SetUpdatableFlags"); + } + setUpdatableFlagsMethod.Invoke(uriParser, new object[] { 0 }); + + uriParsed = new Uri(uriWithEscapedDotsAndSlashes).ToString(); + if (uriParsed != uriWithEscapedDotsAndSlashes) + { + throw new NotImplementedException($"Uri..ctor() can't preserve slashes and dots escaped. Expected={uriWithEscapedDotsAndSlashes}, actual={uriParsed}"); + } + + return true; + } + + private static ConcurrentDictionary s_fixedSchemes = new ConcurrentDictionary(); + private static void LeaveDotsAndSlashesEscaped(string scheme) + { + s_fixedSchemes.GetOrAdd(scheme, DisableUriParserLegacyQuirks); + } +#endif + [Obsolete("Please use another constructor")] public ManagementClient( string hostUrl, @@ -109,6 +155,10 @@ public ManagementClient( basicAuthHeader = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}"))); httpClient = new HttpClient(httpHandler) { Timeout = timeout ?? DefaultTimeout, BaseAddress = endpoint }; disposeHttpClient = true; + +#if NETSTANDARD2_0 + LeaveDotsAndSlashesEscaped(Endpoint.Scheme); +#endif } public ManagementClient(HttpClient httpClient, string username, string password) @@ -123,6 +173,10 @@ public ManagementClient(HttpClient httpClient, string username, string password) basicAuthHeader = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}"))); configureHttpRequestMessage = null; disposeHttpClient = false; + +#if NETSTANDARD2_0 + LeaveDotsAndSlashesEscaped(Endpoint.Scheme); +#endif } public Uri Endpoint => httpClient.BaseAddress!; @@ -722,7 +776,15 @@ private async Task GetAsync( response.EnsureExpectedStatusCode(statusCode => statusCode == HttpStatusCode.OK); - return (await response.Content.ReadFromJsonAsync(SerializerOptions, cancellationToken).ConfigureAwait(false))!; + try + { + return (await response.Content.ReadFromJsonAsync(SerializerOptions, cancellationToken).ConfigureAwait(false))!; + } + catch (Exception e) when (e is ArgumentNullException || e is JsonException) + { + string stringContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new JsonException($"Response content doesn't conform to the JSON schema: {stringContent}", e); + } } private async Task PostAsync( @@ -737,7 +799,15 @@ private async Task PostAsync( response.EnsureExpectedStatusCode(statusCode => statusCode is HttpStatusCode.OK or HttpStatusCode.Created or HttpStatusCode.NoContent); - return (await response.Content.ReadFromJsonAsync(SerializerOptions, cancellationToken).ConfigureAwait(false))!; + try + { + return (await response.Content.ReadFromJsonAsync(SerializerOptions, cancellationToken).ConfigureAwait(false))!; + } + catch (Exception e) when (e is ArgumentNullException || e is JsonException) + { + string stringContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new JsonException($"Response content doesn't conform to the JSON schema: {stringContent}", e); + } } private async Task PostAsync(