From ed41a79d2524d0ac002be10ad08a89226af387d0 Mon Sep 17 00:00:00 2001 From: RTLCoil Date: Wed, 7 Jul 2021 16:32:00 +0300 Subject: [PATCH 1/5] Fixes format exception thrown when user agent header value is incorrect --- .../Asset/UrlBuilderTest.cs | 34 +++++++++++++++---- CloudinaryDotNet/ApiShared.Internal.cs | 24 ++++++++++--- CloudinaryDotNet/ApiShared.cs | 7 +--- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs b/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs index 6988060f..f4508b2f 100644 --- a/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs +++ b/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs @@ -475,23 +475,43 @@ public void TestExcludeEmptyTransformation() Assert.AreEqual(TestConstants.DefaultImageUpPath + "c_fill,x_100,y_100/test", uri); } - [Test] - public void TestAgentPlatformHeaders() + private HttpRequestMessage CreateRequest(string userPlatform) { var request = new HttpRequestMessage { RequestUri = new Uri("http://dummy.com") }; - m_api.UserPlatform = "Test/1.0"; + m_api.UserPlatform = userPlatform; m_api.PrepareRequestBody( request, HttpMethod.GET, new SortedDictionary(), new FileDescription("")); + return request; + } + + [Test] + public void TestAgentPlatformHeaders() + { + var httpRequestMessage = CreateRequest("UserPlatform"); //Can't test the result, so we just verify the UserAgent parameter is sent to the server - StringAssert.AreEqualIgnoringCase($"{m_api.UserPlatform} {ApiShared.USER_AGENT}", - request.Headers.UserAgent.ToString()); - StringAssert.IsMatch(@"Test\/1\.0 CloudinaryDotNet\/(\d+)\.(\d+)\.(\d+) \(.*\)", - request.Headers.UserAgent.ToString()); + StringAssert.IsMatch(@"CloudinaryDotNet\/(\d+)\.(\d+)\.(\d+) \(" + ApiShared.USER_AGENT.Replace("(", "").Replace(")", "") + @"\) \(UserPlatform\)", + httpRequestMessage.Headers.UserAgent.ToString()); + } + + [Test] + public void UnityUserAgentShouldNotThrow() + { + var userAgent = "Mono 5.11.0 ((HEAD/768f1b247c6)"; + var prevAgent = ApiShared.USER_AGENT; + ApiShared.USER_AGENT = userAgent; + try + { + var httpRequestMessage = CreateRequest("UserPlatform"); + } + finally + { + ApiShared.USER_AGENT = prevAgent; + } } [Test] diff --git a/CloudinaryDotNet/ApiShared.Internal.cs b/CloudinaryDotNet/ApiShared.Internal.cs index cdb5860f..46d11292 100644 --- a/CloudinaryDotNet/ApiShared.Internal.cs +++ b/CloudinaryDotNet/ApiShared.Internal.cs @@ -248,6 +248,21 @@ protected void HandleUnsignedParameters(IDictionary parameters) } } + private static void AddCommentToUserAgent( + HttpHeaderValueCollection userAgentHeader, + string comment) + { + if (string.IsNullOrEmpty(comment)) + { + return; + } + + var normalizedComment = comment + .Replace(")", string.Empty) + .Replace("(", string.Empty); + userAgentHeader.Add(new ProductInfoHeaderValue($"({normalizedComment})")); + } + private static SortedDictionary GetCallParams(HttpMethod method, BaseParams parameters) { parameters?.Check(); @@ -455,10 +470,11 @@ private void PrePrepareRequestBody( // Add platform information to the USER_AGENT header // This is intended for platform information and not individual applications! - var userPlatform = string.IsNullOrEmpty(UserPlatform) - ? USER_AGENT - : string.Format(CultureInfo.InvariantCulture, "{0} {1}", UserPlatform, USER_AGENT); - request.Headers.Add("User-Agent", userPlatform); + var userAgentHeader = request.Headers.UserAgent; + userAgentHeader.Add(new ProductInfoHeaderValue("CloudinaryDotNet", CloudinaryVersion.Full)); + + AddCommentToUserAgent(userAgentHeader, USER_AGENT); + AddCommentToUserAgent(userAgentHeader, UserPlatform); byte[] authBytes = Encoding.ASCII.GetBytes(GetApiCredentials()); request.Headers.Add("Authorization", string.Format(CultureInfo.InvariantCulture, "Basic {0}", Convert.ToBase64String(authBytes))); diff --git a/CloudinaryDotNet/ApiShared.cs b/CloudinaryDotNet/ApiShared.cs index b5900cae..d57cc73a 100644 --- a/CloudinaryDotNet/ApiShared.cs +++ b/CloudinaryDotNet/ApiShared.cs @@ -71,7 +71,7 @@ public partial class ApiShared : ISignProvider /// /// User agent for cloudinary API requests. /// - public static string USER_AGENT = BuildUserAgent(); + public static string USER_AGENT = RuntimeInformation.FrameworkDescription; /// /// Sends HTTP requests and receives HTTP responses. @@ -803,10 +803,5 @@ public string BuildUploadFormShared(string field, string resourceType, SortedDic return builder.ToString(); } - - private static string BuildUserAgent() - { - return $"CloudinaryDotNet/{CloudinaryVersion.Full} ({RuntimeInformation.FrameworkDescription})"; - } } } From 0b2e134d642b881e5e3c2e3e27e770422a19e61c Mon Sep 17 00:00:00 2001 From: RTLCoil Date: Wed, 28 Jul 2021 13:03:35 +0300 Subject: [PATCH 2/5] Change processing of User Platform header --- .../Asset/UrlBuilderTest.cs | 24 ++++++++--- CloudinaryDotNet/ApiShared.Internal.cs | 43 +++++++++++++++++-- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs b/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs index f4508b2f..1723f9f1 100644 --- a/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs +++ b/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs @@ -491,22 +491,36 @@ private HttpRequestMessage CreateRequest(string userPlatform) [Test] public void TestAgentPlatformHeaders() { - var httpRequestMessage = CreateRequest("UserPlatform"); + var httpRequestMessage = CreateRequest("UserPlatform/2.3"); //Can't test the result, so we just verify the UserAgent parameter is sent to the server - StringAssert.IsMatch(@"CloudinaryDotNet\/(\d+)\.(\d+)\.(\d+) \(" + ApiShared.USER_AGENT.Replace("(", "").Replace(")", "") + @"\) \(UserPlatform\)", + StringAssert.IsMatch(@"CloudinaryDotNet\/(\d+)\.(\d+)\.(\d+) \(" + ApiShared.USER_AGENT.Replace("(", "").Replace(")", "") + @"\) UserPlatform/2\.3", httpRequestMessage.Headers.UserAgent.ToString()); } [Test] - public void UnityUserAgentShouldNotThrow() + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + [TestCase("UserPlatform/")] + [TestCase("UserPlatform/ ")] + [TestCase("UserPlatform / 1.2")] + public void UnexpectedUserPlatformShouldNotThrow(string userPlatorm) + { + Assert.DoesNotThrow(() => CreateRequest(userPlatorm)); + } + + [Test] + [TestCase("Mono 5.11.0 ((HEAD/768f1b247c6)")] + [TestCase("(")] + [TestCase(")")] + public void MalformedUserAgentShouldNotThrow(string userAgent) { - var userAgent = "Mono 5.11.0 ((HEAD/768f1b247c6)"; var prevAgent = ApiShared.USER_AGENT; ApiShared.USER_AGENT = userAgent; try { - var httpRequestMessage = CreateRequest("UserPlatform"); + Assert.DoesNotThrow(() => CreateRequest("UserPlatform")); } finally { diff --git a/CloudinaryDotNet/ApiShared.Internal.cs b/CloudinaryDotNet/ApiShared.Internal.cs index 46d11292..82c420d7 100644 --- a/CloudinaryDotNet/ApiShared.Internal.cs +++ b/CloudinaryDotNet/ApiShared.Internal.cs @@ -257,12 +257,17 @@ private static void AddCommentToUserAgent( return; } - var normalizedComment = comment - .Replace(")", string.Empty) - .Replace("(", string.Empty); + var normalizedComment = RemoveBracketsFrom(comment); userAgentHeader.Add(new ProductInfoHeaderValue($"({normalizedComment})")); } + private static string RemoveBracketsFrom(string comment) + { + return comment + .Replace(")", string.Empty) + .Replace("(", string.Empty); + } + private static SortedDictionary GetCallParams(HttpMethod method, BaseParams parameters) { parameters?.Check(); @@ -474,7 +479,7 @@ private void PrePrepareRequestBody( userAgentHeader.Add(new ProductInfoHeaderValue("CloudinaryDotNet", CloudinaryVersion.Full)); AddCommentToUserAgent(userAgentHeader, USER_AGENT); - AddCommentToUserAgent(userAgentHeader, UserPlatform); + SetUserPlatform(userAgentHeader); byte[] authBytes = Encoding.ASCII.GetBytes(GetApiCredentials()); request.Headers.Add("Authorization", string.Format(CultureInfo.InvariantCulture, "Basic {0}", Convert.ToBase64String(authBytes))); @@ -494,6 +499,36 @@ private void PrePrepareRequestBody( } } + private void SetUserPlatform(HttpHeaderValueCollection userAgentHeader) + { + Console.WriteLine($"UserPlatform: [{UserPlatform}] ======"); + var up = UserPlatform?.Trim(); + if (string.IsNullOrEmpty(up)) + { + return; + } + + var upp = up.Split('/'); + var productName = GetElement(0); + if (string.IsNullOrEmpty(productName)) + { + return; + } + + var productVersion = GetElement(1); + if (string.IsNullOrEmpty(productVersion)) + { + productVersion = "0.1"; + } + + userAgentHeader.Add(new ProductInfoHeaderValue(productName, productVersion)); + + string GetElement(int index) + { + return upp.ElementAtOrDefault(index)?.Trim(); + } + } + private async Task PrepareRequestContentAsync( HttpRequestMessage request, SortedDictionary parameters, From a1e63286ba14101160b20fcf5f6943d6acaaab0b Mon Sep 17 00:00:00 2001 From: RTLCoil Date: Mon, 20 Sep 2021 23:13:25 +0300 Subject: [PATCH 3/5] Address review comments --- CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs | 10 +++++----- CloudinaryDotNet/ApiShared.Internal.cs | 5 +++-- CloudinaryDotNet/ApiShared.cs | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs b/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs index b8c98dcc..c83e619f 100644 --- a/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs +++ b/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs @@ -494,7 +494,7 @@ public void TestAgentPlatformHeaders() var httpRequestMessage = CreateRequest("UserPlatform/2.3"); //Can't test the result, so we just verify the UserAgent parameter is sent to the server - StringAssert.IsMatch(@"CloudinaryDotNet\/(\d+)\.(\d+)\.(\d+) \(" + ApiShared.USER_AGENT.Replace("(", "").Replace(")", "") + @"\) UserPlatform/2\.3", + StringAssert.IsMatch(@"CloudinaryDotNet\/(\d+)\.(\d+)\.(\d+) \(" + ApiShared.RUNTIME_INFORMATION.Replace("(", "").Replace(")", "") + @"\) UserPlatform/2\.3", httpRequestMessage.Headers.UserAgent.ToString()); } @@ -514,17 +514,17 @@ public void UnexpectedUserPlatformShouldNotThrow(string userPlatorm) [TestCase("Mono 5.11.0 ((HEAD/768f1b247c6)")] [TestCase("(")] [TestCase(")")] - public void MalformedUserAgentShouldNotThrow(string userAgent) + public void MalformedRuntimeInformationShouldNotThrow(string runtimeInformation) { - var prevAgent = ApiShared.USER_AGENT; - ApiShared.USER_AGENT = userAgent; + var prevAgent = ApiShared.RUNTIME_INFORMATION; + ApiShared.RUNTIME_INFORMATION = runtimeInformation; try { Assert.DoesNotThrow(() => CreateRequest("UserPlatform")); } finally { - ApiShared.USER_AGENT = prevAgent; + ApiShared.RUNTIME_INFORMATION = prevAgent; } } diff --git a/CloudinaryDotNet/ApiShared.Internal.cs b/CloudinaryDotNet/ApiShared.Internal.cs index 82c420d7..25e0f5ae 100644 --- a/CloudinaryDotNet/ApiShared.Internal.cs +++ b/CloudinaryDotNet/ApiShared.Internal.cs @@ -257,7 +257,9 @@ private static void AddCommentToUserAgent( return; } + // User-Agent's comment section is sensitive to brackets var normalizedComment = RemoveBracketsFrom(comment); + userAgentHeader.Add(new ProductInfoHeaderValue($"({normalizedComment})")); } @@ -478,7 +480,7 @@ private void PrePrepareRequestBody( var userAgentHeader = request.Headers.UserAgent; userAgentHeader.Add(new ProductInfoHeaderValue("CloudinaryDotNet", CloudinaryVersion.Full)); - AddCommentToUserAgent(userAgentHeader, USER_AGENT); + AddCommentToUserAgent(userAgentHeader, RUNTIME_INFORMATION); SetUserPlatform(userAgentHeader); byte[] authBytes = Encoding.ASCII.GetBytes(GetApiCredentials()); @@ -501,7 +503,6 @@ private void PrePrepareRequestBody( private void SetUserPlatform(HttpHeaderValueCollection userAgentHeader) { - Console.WriteLine($"UserPlatform: [{UserPlatform}] ======"); var up = UserPlatform?.Trim(); if (string.IsNullOrEmpty(up)) { diff --git a/CloudinaryDotNet/ApiShared.cs b/CloudinaryDotNet/ApiShared.cs index e6500907..c8fe8e60 100644 --- a/CloudinaryDotNet/ApiShared.cs +++ b/CloudinaryDotNet/ApiShared.cs @@ -69,9 +69,9 @@ public partial class ApiShared : ISignProvider public const string HTTP_BOUNDARY = "notrandomsequencetouseasboundary"; /// - /// User agent for cloudinary API requests. + /// Runtime information for cloudinary API requests. /// - public static string USER_AGENT = RuntimeInformation.FrameworkDescription; + public static string RUNTIME_INFORMATION = RuntimeInformation.FrameworkDescription; /// /// Whether to use a sub domain. From 387a4876f43a53708e0d1706903dd85092e24d32 Mon Sep 17 00:00:00 2001 From: RTLCoil Date: Tue, 19 Oct 2021 17:12:55 +0300 Subject: [PATCH 4/5] Cleans up code --- .../Asset/UrlBuilderTest.cs | 39 +++--------- CloudinaryDotNet/ApiShared.Internal.cs | 59 ++----------------- CloudinaryDotNet/ApiShared.cs | 8 +-- 3 files changed, 15 insertions(+), 91 deletions(-) diff --git a/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs b/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs index c83e619f..d2883776 100644 --- a/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs +++ b/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Net.Http; -using System.Linq; using System.Text.RegularExpressions; using CloudinaryDotNet.Actions; using NUnit.Framework; @@ -475,10 +474,10 @@ public void TestExcludeEmptyTransformation() Assert.AreEqual(TestConstants.DefaultImageUpPath + "c_fill,x_100,y_100/test", uri); } - private HttpRequestMessage CreateRequest(string userPlatform) + private HttpRequestMessage CreateRequest(string userPlatformProduct, string userPlatformVersion) { var request = new HttpRequestMessage { RequestUri = new Uri("http://dummy.com") }; - m_api.UserPlatform = userPlatform; + m_api.UserPlatform = new System.Net.Http.Headers.ProductHeaderValue(userPlatformProduct, userPlatformVersion); m_api.PrepareRequestBody( request, @@ -491,41 +490,19 @@ private HttpRequestMessage CreateRequest(string userPlatform) [Test] public void TestAgentPlatformHeaders() { - var httpRequestMessage = CreateRequest("UserPlatform/2.3"); + var httpRequestMessage = CreateRequest("UserPlatform", "2.3"); //Can't test the result, so we just verify the UserAgent parameter is sent to the server - StringAssert.IsMatch(@"CloudinaryDotNet\/(\d+)\.(\d+)\.(\d+) \(" + ApiShared.RUNTIME_INFORMATION.Replace("(", "").Replace(")", "") + @"\) UserPlatform/2\.3", + StringAssert.IsMatch(@"CloudinaryDotNet\/(\d+)\.(\d+)\.(\d+) UserPlatform/2\.3", httpRequestMessage.Headers.UserAgent.ToString()); } [Test] - [TestCase(null)] - [TestCase("")] - [TestCase(" ")] - [TestCase("UserPlatform/")] - [TestCase("UserPlatform/ ")] - [TestCase("UserPlatform / 1.2")] - public void UnexpectedUserPlatformShouldNotThrow(string userPlatorm) + [TestCase("UserPlatform", null)] + [TestCase("UserPlatform", "1.2")] + public void UnexpectedUserPlatformShouldNotThrow(string userPlatformProduct, string userPlatformVersion) { - Assert.DoesNotThrow(() => CreateRequest(userPlatorm)); - } - - [Test] - [TestCase("Mono 5.11.0 ((HEAD/768f1b247c6)")] - [TestCase("(")] - [TestCase(")")] - public void MalformedRuntimeInformationShouldNotThrow(string runtimeInformation) - { - var prevAgent = ApiShared.RUNTIME_INFORMATION; - ApiShared.RUNTIME_INFORMATION = runtimeInformation; - try - { - Assert.DoesNotThrow(() => CreateRequest("UserPlatform")); - } - finally - { - ApiShared.RUNTIME_INFORMATION = prevAgent; - } + Assert.DoesNotThrow(() => CreateRequest(userPlatformProduct, userPlatformVersion)); } [Test] diff --git a/CloudinaryDotNet/ApiShared.Internal.cs b/CloudinaryDotNet/ApiShared.Internal.cs index 25e0f5ae..c2ab7c9c 100644 --- a/CloudinaryDotNet/ApiShared.Internal.cs +++ b/CloudinaryDotNet/ApiShared.Internal.cs @@ -248,28 +248,6 @@ protected void HandleUnsignedParameters(IDictionary parameters) } } - private static void AddCommentToUserAgent( - HttpHeaderValueCollection userAgentHeader, - string comment) - { - if (string.IsNullOrEmpty(comment)) - { - return; - } - - // User-Agent's comment section is sensitive to brackets - var normalizedComment = RemoveBracketsFrom(comment); - - userAgentHeader.Add(new ProductInfoHeaderValue($"({normalizedComment})")); - } - - private static string RemoveBracketsFrom(string comment) - { - return comment - .Replace(")", string.Empty) - .Replace("(", string.Empty); - } - private static SortedDictionary GetCallParams(HttpMethod method, BaseParams parameters) { parameters?.Check(); @@ -480,10 +458,12 @@ private void PrePrepareRequestBody( var userAgentHeader = request.Headers.UserAgent; userAgentHeader.Add(new ProductInfoHeaderValue("CloudinaryDotNet", CloudinaryVersion.Full)); - AddCommentToUserAgent(userAgentHeader, RUNTIME_INFORMATION); - SetUserPlatform(userAgentHeader); + if (UserPlatform != null) + { + userAgentHeader.Add(new ProductInfoHeaderValue(UserPlatform)); + } - byte[] authBytes = Encoding.ASCII.GetBytes(GetApiCredentials()); + var authBytes = Encoding.ASCII.GetBytes(GetApiCredentials()); request.Headers.Add("Authorization", string.Format(CultureInfo.InvariantCulture, "Basic {0}", Convert.ToBase64String(authBytes))); if (extraHeaders != null) @@ -501,35 +481,6 @@ private void PrePrepareRequestBody( } } - private void SetUserPlatform(HttpHeaderValueCollection userAgentHeader) - { - var up = UserPlatform?.Trim(); - if (string.IsNullOrEmpty(up)) - { - return; - } - - var upp = up.Split('/'); - var productName = GetElement(0); - if (string.IsNullOrEmpty(productName)) - { - return; - } - - var productVersion = GetElement(1); - if (string.IsNullOrEmpty(productVersion)) - { - productVersion = "0.1"; - } - - userAgentHeader.Add(new ProductInfoHeaderValue(productName, productVersion)); - - string GetElement(int index) - { - return upp.ElementAtOrDefault(index)?.Trim(); - } - } - private async Task PrepareRequestContentAsync( HttpRequestMessage request, SortedDictionary parameters, diff --git a/CloudinaryDotNet/ApiShared.cs b/CloudinaryDotNet/ApiShared.cs index c8fe8e60..104fc405 100644 --- a/CloudinaryDotNet/ApiShared.cs +++ b/CloudinaryDotNet/ApiShared.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Linq; using System.Net.Http; + using System.Net.Http.Headers; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Serialization; @@ -68,11 +69,6 @@ public partial class ApiShared : ISignProvider /// public const string HTTP_BOUNDARY = "notrandomsequencetouseasboundary"; - /// - /// Runtime information for cloudinary API requests. - /// - public static string RUNTIME_INFORMATION = RuntimeInformation.FrameworkDescription; - /// /// Whether to use a sub domain. /// @@ -111,7 +107,7 @@ public partial class ApiShared : ISignProvider /// /// User platform information. /// - public string UserPlatform; + public ProductHeaderValue UserPlatform; /// /// Timeout for the API requests, milliseconds. From 31a9e26e8fd130c44d2c50fc432cde6e6cc24a60 Mon Sep 17 00:00:00 2001 From: RTLCoil Date: Sat, 23 Oct 2021 11:09:27 +0300 Subject: [PATCH 5/5] Add safe dotnet version to User Agent --- .../Asset/UrlBuilderTest.cs | 25 ++++++++++-- CloudinaryDotNet/ApiShared.Internal.cs | 1 + CloudinaryDotNet/ApiShared.cs | 39 ++++++++++++++++--- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs b/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs index d2883776..b884e648 100644 --- a/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs +++ b/CloudinaryDotNet.Tests/Asset/UrlBuilderTest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net.Http; +using System.Net.Http.Headers; using System.Text.RegularExpressions; using CloudinaryDotNet.Actions; using NUnit.Framework; @@ -474,10 +475,10 @@ public void TestExcludeEmptyTransformation() Assert.AreEqual(TestConstants.DefaultImageUpPath + "c_fill,x_100,y_100/test", uri); } - private HttpRequestMessage CreateRequest(string userPlatformProduct, string userPlatformVersion) + private HttpRequestMessage CreateRequest(string userPlatformProduct, string userPlatformVersion = null) { var request = new HttpRequestMessage { RequestUri = new Uri("http://dummy.com") }; - m_api.UserPlatform = new System.Net.Http.Headers.ProductHeaderValue(userPlatformProduct, userPlatformVersion); + m_api.UserPlatform = new ProductHeaderValue(userPlatformProduct, userPlatformVersion); m_api.PrepareRequestBody( request, @@ -493,14 +494,30 @@ public void TestAgentPlatformHeaders() var httpRequestMessage = CreateRequest("UserPlatform", "2.3"); //Can't test the result, so we just verify the UserAgent parameter is sent to the server - StringAssert.IsMatch(@"CloudinaryDotNet\/(\d+)\.(\d+)\.(\d+) UserPlatform/2\.3", + StringAssert.IsMatch(@"CloudinaryDotNet\/(\d+)\.(\d+)\.(\d+) \(.*\) UserPlatform/2\.3", httpRequestMessage.Headers.UserAgent.ToString()); } + [Test] + [TestCase("Mono 5.11.0 ((HEAD/768f1b247c6)")] + public void TestMalformedFrameworkVersion(string dotnetVersion) + { + var previousFramework = m_api.DotnetVersion; + try + { + m_api.DotnetVersion = dotnetVersion; + Assert.DoesNotThrow(() => CreateRequest("p")); + } + finally + { + m_api.DotnetVersion = previousFramework; + } + } + [Test] [TestCase("UserPlatform", null)] [TestCase("UserPlatform", "1.2")] - public void UnexpectedUserPlatformShouldNotThrow(string userPlatformProduct, string userPlatformVersion) + public void TestUserPlatformCombinations(string userPlatformProduct, string userPlatformVersion) { Assert.DoesNotThrow(() => CreateRequest(userPlatformProduct, userPlatformVersion)); } diff --git a/CloudinaryDotNet/ApiShared.Internal.cs b/CloudinaryDotNet/ApiShared.Internal.cs index c2ab7c9c..1b8186f3 100644 --- a/CloudinaryDotNet/ApiShared.Internal.cs +++ b/CloudinaryDotNet/ApiShared.Internal.cs @@ -457,6 +457,7 @@ private void PrePrepareRequestBody( // This is intended for platform information and not individual applications! var userAgentHeader = request.Headers.UserAgent; userAgentHeader.Add(new ProductInfoHeaderValue("CloudinaryDotNet", CloudinaryVersion.Full)); + userAgentHeader.Add(new ProductInfoHeaderValue($"({DotnetVersion})")); if (UserPlatform != null) { diff --git a/CloudinaryDotNet/ApiShared.cs b/CloudinaryDotNet/ApiShared.cs index 104fc405..b7b1ecbd 100644 --- a/CloudinaryDotNet/ApiShared.cs +++ b/CloudinaryDotNet/ApiShared.cs @@ -11,6 +11,7 @@ using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; + using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using CloudinaryDotNet.Actions; @@ -146,6 +147,8 @@ public partial class ApiShared : ISignProvider private readonly Func requestBuilder = (url) => new HttpRequestMessage { RequestUri = new Uri(url) }; + private string dotnetVersion; + /// /// Initializes a new instance of the class. /// Default parameterless constructor. @@ -378,6 +381,24 @@ public Url ApiUrlVideoUpV } } + /// + /// Gets or sets the .NET version compatible with Http headers comment specification. + /// + internal string DotnetVersion + { + get + { + if (dotnetVersion == null) + { + DotnetVersion = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription; + } + + return dotnetVersion; + } + + set => dotnetVersion = GetUserAgentCommentFriendlyValue(value); + } + /// /// Gets cloudinary parameter from enumeration. /// @@ -624,12 +645,12 @@ public string SignParameters(IDictionary parameters) StringBuilder signBase = new StringBuilder(string.Join("&", parameters. Where(pair => pair.Value != null && !excludedSignatureKeys.Any(s => pair.Key.Equals(s, StringComparison.Ordinal))) .Select(pair => - { - var value = pair.Value is IEnumerable - ? string.Join(",", ((IEnumerable)pair.Value).ToArray()) - : pair.Value.ToString(); - return string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Key, value); - }) + { + var value = pair.Value is IEnumerable + ? string.Join(",", ((IEnumerable)pair.Value).ToArray()) + : pair.Value.ToString(); + return string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Key, value); + }) .ToArray())); signBase.Append(Account.ApiSecret); @@ -794,5 +815,11 @@ public string BuildUploadFormShared(string field, string resourceType, SortedDic return builder.ToString(); } + + // Nonmatching braces are not accepted, so we just remove them all. + // See https://github.com/aspnet/HttpAbstractions/blob/master/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs#L250 + // for details. + private static string GetUserAgentCommentFriendlyValue(string value) + => Regex.Replace(value, "\\(|\\)", string.Empty); } }