Skip to content

Commit

Permalink
[Azure.Core] UrlEncode OS description in user-agent header value if i…
Browse files Browse the repository at this point in the history
…t contains non-ASCII characters (#44386)

* add html encode

* url encode if non ascii character is found

* tweak

* clean up usings

* feedback
  • Loading branch information
m-redding authored Jun 5, 2024
1 parent d7f4c01 commit c9ed525
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 4 deletions.
1 change: 1 addition & 0 deletions sdk/core/Azure.Core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Bugs Fixed

- Fixed User-Agent telemetry so that it properly escapes operating system information if it contains non-ascii characters.
- Fix for operation id not set of incomplete long-running operation.

### Other Changes
Expand Down
30 changes: 26 additions & 4 deletions sdk/core/Azure.Core/src/TelemetryDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Net;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using Azure.Core.Pipeline;

Expand Down Expand Up @@ -88,7 +86,19 @@ internal static string GenerateUserAgentString(Assembly clientAssembly, string?
version = version.Substring(0, hashSeparator);
}
runtimeInformation ??= new RuntimeInformationWrapper();
var platformInformation = EscapeProductInformation($"({runtimeInformation.FrameworkDescription}; {runtimeInformation.OSDescription})");

// RFC 9110 section 5.5 https://www.rfc-editor.org/rfc/rfc9110.txt#section-5.5 does not require any specific encoding : "Fields needing a greater range of characters
// can use an encoding, such as the one defined in RFC8187." RFC8187 is targeted at parameter values, almost always filename, so using url encoding here instead, which is
// more widely used. Since user-agent does not usually contain non-ascii, only encode when necessary.
// This was added to support operating systems with non-ascii characters in their release names.
string osDescription;
#if NET8_0_OR_GREATER
osDescription = Ascii.IsValid(runtimeInformation.OSDescription) ? runtimeInformation.OSDescription : WebUtility.UrlEncode(runtimeInformation.OSDescription);
#else
osDescription = ContainsNonAscii(runtimeInformation.OSDescription) ? WebUtility.UrlEncode(runtimeInformation.OSDescription) : runtimeInformation.OSDescription;
#endif

var platformInformation = EscapeProductInformation($"({runtimeInformation.FrameworkDescription}; {osDescription})");

return applicationId != null
? $"{applicationId} azsdk-net-{assemblyName}/{version} {platformInformation}"
Expand Down Expand Up @@ -161,5 +171,17 @@ private static string EscapeProductInformation(string productInfo)
sb.Append(')');
return sb.ToString();
}

private static bool ContainsNonAscii(string value)
{
foreach (char c in value)
{
if ((int)c > 0x7f)
{
return true;
}
}
return false;
}
}
}
47 changes: 47 additions & 0 deletions sdk/core/Azure.Core/tests/TelemetryDetailsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using Azure.Core.Pipeline;
Expand Down Expand Up @@ -97,6 +98,52 @@ public void ValidatesProperParenthesisMatching(string input, string output)
target.ToString());
}

[Test]
[TestCase("Win64; x64", "Win64; x64")]
[TestCase("Intel Mac OS X 10_15_7", "Intel Mac OS X 10_15_7")]
[TestCase("Android 10; SM-G973F", "Android 10; SM-G973F")]
[TestCase("Win64; x64; Xbox; Xbox One", "Win64; x64; Xbox; Xbox One")]
public void AsciiDoesNotEncode(string input, string output)
{
var mockRuntimeInformation = new MockRuntimeInformation { OSDescriptionMock = input, FrameworkDescriptionMock = RuntimeInformation.FrameworkDescription };
var assembly = Assembly.GetAssembly(GetType());
AssemblyInformationalVersionAttribute versionAttribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
string version = versionAttribute.InformationalVersion;
int hashSeparator = version.IndexOfOrdinal('+');
if (hashSeparator != -1)
{
version = version.Substring(0, hashSeparator);
}

var target = new TelemetryDetails(typeof(TelemetryDetailsTests).Assembly, default, mockRuntimeInformation);

Assert.AreEqual(
$"azsdk-net-Core.Tests/{version} ({mockRuntimeInformation.FrameworkDescription}; {output})",
target.ToString());
}

[Test]
[TestCase("»-Browser¢sample", "%C2%BB-Browser%C2%A2sample")]
[TestCase("NixOS 24.11 (Vicuña)", "NixOS+24.11+(Vicu%C3%B1a)")]
public void NonAsciiCharactersAreUrlEncoded(string input, string output)
{
var mockRuntimeInformation = new MockRuntimeInformation { OSDescriptionMock = input, FrameworkDescriptionMock = RuntimeInformation.FrameworkDescription };
var assembly = Assembly.GetAssembly(GetType());
AssemblyInformationalVersionAttribute versionAttribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
string version = versionAttribute.InformationalVersion;
int hashSeparator = version.IndexOfOrdinal('+');
if (hashSeparator != -1)
{
version = version.Substring(0, hashSeparator);
}

var target = new TelemetryDetails(typeof(TelemetryDetailsTests).Assembly, default, mockRuntimeInformation);

Assert.AreEqual(
$"azsdk-net-Core.Tests/{version} ({mockRuntimeInformation.FrameworkDescription}; {output})",
target.ToString());
}

private class MockRuntimeInformation : RuntimeInformationWrapper
{
public string FrameworkDescriptionMock { get; set; }
Expand Down

0 comments on commit c9ed525

Please sign in to comment.