Skip to content

Commit

Permalink
Add Support for Symweb Authentication to PerfView (#2039)
Browse files Browse the repository at this point in the history
* Add Symweb authentication support.

Fix loading of dependencies for authentication and add special handling
for symweb.

* Update SymbolReader to only download headers before starting to display
download progress.
  • Loading branch information
brianrob authored May 30, 2024
1 parent a662474 commit c805b11
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 7 deletions.
3 changes: 3 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<MicroBuildCoreVersion>0.3.1</MicroBuildCoreVersion>
<MicrosoftIdentityClientVersion>4.60.1</MicrosoftIdentityClientVersion>
<MicrosoftIdentityClientExtensionsMsalVersion>4.60.1</MicrosoftIdentityClientExtensionsMsalVersion>
<MicrosoftIdentityModelAbstractionsVersion>7.1.2</MicrosoftIdentityModelAbstractionsVersion>
<MicrosoftIdentityModelTokensVersion>7.1.2</MicrosoftIdentityModelTokensVersion>
<MicrosoftIdentityModelJsonWebTokensVersion>7.1.2</MicrosoftIdentityModelJsonWebTokensVersion>
<MicrosoftSourceLinkGitHubVersion>8.0.0</MicrosoftSourceLinkGitHubVersion>
Expand All @@ -58,6 +59,8 @@
<SystemReflectionTypeExtensionsVersion>4.7.0</SystemReflectionTypeExtensionsVersion>
<SystemRuntimeCompilerServicesUnsafeVersion>6.0.0</SystemRuntimeCompilerServicesUnsafeVersion>
<SystemRuntimeInteropServicesRuntimeInformationVersion></SystemRuntimeInteropServicesRuntimeInformationVersion>
<SystemSecurityCryptographyAlgorithmsVersion>4.3.1</SystemSecurityCryptographyAlgorithmsVersion>
<SystemSecurityCryptographyProtectedDataVersion>4.7.0</SystemSecurityCryptographyProtectedDataVersion>
<SystemTextEncodingsWebVersion>8.0.0</SystemTextEncodingsWebVersion>
<SystemTextJsonVersion>8.0.1</SystemTextJsonVersion>
<SystemThreadingTasksExtensionsVersion>4.5.4</SystemThreadingTasksExtensionsVersion>
Expand Down
3 changes: 3 additions & 0 deletions src/PerfView/Authentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ public static void Configure(this SymbolReaderHttpHandler handler, Authenticatio

handler.ClearHandlers();

// Always add Symweb authentication.
handler.AddSymwebAuthentication(log);

// The order isn't critical, but we chose to put GCM last
// because the user might want to use GCM for GitHub and
// Developer identity for Azure DevOps. If GCM were first,
Expand Down
17 changes: 16 additions & 1 deletion src/PerfView/PerfView.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
<NeutralLanguage>en</NeutralLanguage>

<GeneratePackageOnBuild>true</GeneratePackageOnBuild>

<NuspecFile>PerfView.nuspec</NuspecFile>
<GenerateNuspecDependsOn>$(GenerateNuspecDependsOn);SetNuspecProperties</GenerateNuspecDependsOn>
</PropertyGroup>
Expand Down Expand Up @@ -87,11 +86,13 @@
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent.SupportFiles" Version="$(MicrosoftDiagnosticsTracingTraceEventSupportFilesVersion)" PrivateAssets="all" />
<PackageReference Include="Microsoft.Identity.Client" Version="$(MicrosoftIdentityClientVersion)" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="$(MicrosoftIdentityClientExtensionsMsalVersion)" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.IdentityModel.Abstractions" Version="$(MicrosoftIdentityModelAbstractionsVersion)" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="$(MicrosoftIdentityModelTokensVersion)" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="$(MicrosoftIdentityModelJsonWebTokensVersion)" GeneratePathProperty="true" />
<PackageReference Include="PerfView.SupportFiles" Version="$(PerfViewSupportFilesVersion)" PrivateAssets="all" />
<PackageReference Include="System.Buffers" Version="$(SystemBuffersVersion)" GeneratePathProperty="true" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="$(SystemDiagnosticsDiagnosticSourceVersion)" GeneratePathProperty="true" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="$(SystemSecurityCryptographyProtectedDataVersion)" GeneratePathProperty="true" />
<PackageReference Include="System.Memory" Version="$(SystemMemoryVersion)" GeneratePathProperty="true" />
<PackageReference Include="System.Numerics.Vectors" Version="$(SystemNumericsVectorsVersion)" GeneratePathProperty="true" />
<PackageReference Include="System.Text.Encodings.Web" Version="$(SystemTextEncodingsWebVersion)" GeneratePathProperty="true" />
Expand Down Expand Up @@ -510,6 +511,13 @@
<Link>Microsoft.Identity.Client.Extensions.Msal.dll</Link>
<Visible>False</Visible>
</EmbeddedResource>
<EmbeddedResource Include="$(PkgMicrosoft_IdentityModel_Abstractions)\lib\net461\Microsoft.IdentityModel.Abstractions.dll">
<Type>Non-Resx</Type>
<WithCulture>false</WithCulture>
<LogicalName>.\Microsoft.IdentityModel.Abstractions.dll</LogicalName>
<Link>Microsoft.IdentityModel.Abstractions.dll</Link>
<Visible>False</Visible>
</EmbeddedResource>
<EmbeddedResource Include="$(PkgMicrosoft_IdentityModel_JsonWebTokens)\lib\net461\Microsoft.IdentityModel.JsonWebTokens.dll">
<Type>Non-Resx</Type>
<WithCulture>false</WithCulture>
Expand Down Expand Up @@ -552,6 +560,13 @@
<Link>System.Numerics.Vectors.dll</Link>
<Visible>False</Visible>
</EmbeddedResource>
<EmbeddedResource Include="$(PkgSystem_Security_Cryptography_ProtectedData)\lib\net461\System.Security.Cryptography.ProtectedData.dll">
<Type>Non-Resx</Type>
<WithCulture>false</WithCulture>
<LogicalName>.\System.Security.Cryptography.ProtectedData.dll</LogicalName>
<Link>System.Security.Cryptography.ProtectedData.dll</Link>
<Visible>False</Visible>
</EmbeddedResource>
<EmbeddedResource Include="$(PkgSystem_Text_Encodings_Web)\lib\net462\System.Text.Encodings.Web.dll">
<Type>Non-Resx</Type>
<WithCulture>false</WithCulture>
Expand Down
173 changes: 173 additions & 0 deletions src/PerfView/SymbolReaderHttpHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,26 @@ public SymbolReaderHttpHandler AddAzureDevOpsAuthentication(TextWriter log, bool
return AddHandler(new AzureDevOpsHandler(log, new DefaultAzureCredential(options)));
}

/// <summary>
/// Add a handler for Symweb authentication using local credentials.
/// It will try to use cached credentials from Visual Studio, VS Code,
/// Azure Powershell and Azure CLI.
/// </summary>
/// <param name="log">A logger.</param>
/// <param name="silent">If no local credentials can be found, then a browser window will
/// be opened to prompt the user. Set this to true to if you don't want that.</param>
/// <returns>This instance for fluent chaining.</returns>
public SymbolReaderHttpHandler AddSymwebAuthentication(TextWriter log, bool silent = false)
{
DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions
{
ExcludeInteractiveBrowserCredential = silent,
ExcludeManagedIdentityCredential = true // This is not designed to be used in a service.
};

return AddHandler(new SymwebHandler(log, new DefaultAzureCredential(options)));
}

/// <summary>
/// Add a handler for GitHub device flow authentication.
/// </summary>
Expand Down Expand Up @@ -1572,6 +1592,101 @@ private static bool TryGetNextChallengeParameter(ref ReadOnlySpan<char> paramete
}
}

/// <summary>
/// A handler that adds authorization for Symweb.
/// </summary>
internal sealed class SymwebHandler : SymbolReaderAuthHandlerBase
{
/// <summary>
/// The value of <see cref="Symweb.Scope"/> stored in a single element
/// array suitable for passing to
/// <see cref="TokenCredential.GetTokenAsync(TokenRequestContext, CancellationToken)"/>.
/// </summary>
private static readonly string[] s_scopes = new[] { Symweb.Scope };

/// <summary>
/// Prefix to put in front of logging messages.
/// </summary>
private const string LogPrefix = "SymwebAuth: ";

/// <summary>
/// A provider of access tokens.
/// </summary>
private readonly TokenCredential _tokenCredential;

/// <summary>
/// Protect <see cref="_tokenCredential"/> against concurrent access.
/// </summary>
private readonly SemaphoreSlim _tokenCredentialGate = new SemaphoreSlim(initialCount: 1);

/// <summary>
/// An HTTP client used to discover the authority (login endpoint and tenant) for Symweb.
/// </summary>
private readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler() { CheckCertificateRevocationList = true });

/// <summary>
/// Construct a new <see cref="SymwebHandler"/> instance.
/// </summary>
/// <param name="tokenCredential">A provider of access tokens.</param>
public SymwebHandler(TextWriter log, TokenCredential tokenCredential) : base(log, LogPrefix)
{
_tokenCredential = tokenCredential ?? throw new ArgumentNullException(nameof(tokenCredential));
}

/// <summary>
/// Try to find the authority endpoint for Symweb
/// given a full URI.
/// </summary>
/// <param name="requestUri">The request URI.</param>
/// <param name="authority">The authority, if found.</param>
/// <returns>True if <paramref name="requestUri"/> represents a path to a
/// resource in Symweb.</returns>
protected override bool TryGetAuthority(Uri requestUri, out Uri authority) => Symweb.TryGetAuthority(requestUri, out authority);

/// <summary>
/// Get a token to access Symweb.
/// </summary>
/// <param name="context">The request context.</param>
/// <param name="next">The next handler.</param>
/// <param name="authority">The Symweb instance.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>An access token, or null if one could not be obtained.</returns>
protected override async Task<AuthToken?> GetAuthTokenAsync(RequestContext context, SymbolReaderHandlerDelegate next, Uri authority, CancellationToken cancellationToken)
{
// Get a new access token from the credential provider.
WriteLog("Asking for authorization to access {0}", authority);
AuthToken token = await GetTokenAsync(cancellationToken).ConfigureAwait(false);

return token;
}

/// <summary>
/// Get a new access token for Symweb from the <see cref="TokenCredential"/>.
/// </summary>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The access token.</returns>
private async Task<AuthToken> GetTokenAsync(CancellationToken cancellationToken)
{
await _tokenCredentialGate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// Use the token credential provider to acquire a new token.
TokenRequestContext requestContext = new TokenRequestContext(s_scopes);
AccessToken accessToken = await _tokenCredential.GetTokenAsync(requestContext, cancellationToken).ConfigureAwait(false);
return AuthToken.FromAzureCoreAccessToken(accessToken);
}
catch (Exception ex)
{
WriteStatusLog("Exception getting token. {0}", ex);
throw;
}
finally
{
_tokenCredentialGate.Release();
}
}
}

/// <summary>
/// A handler that handles GitHub device flow authorization.
/// </summary>
Expand Down Expand Up @@ -1861,6 +1976,64 @@ private sealed class AccessTokenResponse
}
}

/// <summary>
/// Contains constants, static properties and helper methods pertinent to Symweb.
/// </summary>
internal static class Symweb
{
/// <summary>
/// The OAuth scope to use when requesting tokens for Symweb.
/// </summary>
public const string Scope = "af9e1c69-e5e9-4331-8cc5-cdf93d57bafa/.default";

/// <summary>
/// The url host for Symweb.
/// </summary>
public const string SymwebHost = "symweb.azurefd.net";

/// <summary>
/// Try to find the authority endpoint for Symweb given a full URI.
/// </summary>
/// <param name="requestUri">The request URI.</param>
/// <param name="authority">The authority, if found.</param>
/// <returns>True if <paramref name="requestUri"/> represents a path to a
/// resource in Symweb.</returns>
public static bool TryGetAuthority(Uri requestUri, out Uri authority)
{
if (!requestUri.IsAbsoluteUri)
{
authority = null;
return false;
}

UriBuilder builder = null;
string host = requestUri.DnsSafeHost;
if (host.Equals(SymwebHost, StringComparison.OrdinalIgnoreCase))
{
builder = new UriBuilder
{
Host = SymwebHost
};
}

if (builder is null)
{
// Not a Symweb URI.
authority = null;
return false;
}

builder.Scheme = requestUri.Scheme;
if (!requestUri.IsDefaultPort)
{
builder.Port = requestUri.Port;
}

authority = builder.Uri;
return true;
}
}

/// <summary>
/// Contains constants, static properties and helper methods pertinent to Azure DevOps.
/// </summary>
Expand Down
4 changes: 1 addition & 3 deletions src/TraceEvent/Symbols/SymbolPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ public static string MicrosoftSymbolServerPath
if (s_MicrosoftSymbolServerPath == null)
{
s_MicrosoftSymbolServerPath = s_MicrosoftSymbolServerPath +
";" + @"SRV*https://msdl.microsoft.com/download/symbols" + // Operatig system Symbols
";" + @"SRV*https://nuget.smbsrc.net" + // Nuget symbols
";" + @"SRV*https://referencesource.microsoft.com/symbols"; // .NET Runtime desktop symbols
";" + @"SRV*https://msdl.microsoft.com/download/symbols";
}
return s_MicrosoftSymbolServerPath;
}
Expand Down
6 changes: 3 additions & 3 deletions src/TraceEvent/Symbols/SymbolReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ internal bool GetPhysicalFileFromServer(string serverPath, string pdbIndexPath,
{
m_log.WriteLine("FindSymbolFilePath: In task, sending HTTP request {0}", fullUri);

var responseTask = HttpClient.GetAsync(fullUri);
var responseTask = HttpClient.GetAsync(fullUri, HttpCompletionOption.ResponseHeadersRead);
responseTask.Wait();
var response = responseTask.Result.EnsureSuccessStatusCode();

Expand Down Expand Up @@ -1140,8 +1140,8 @@ internal bool GetPhysicalFileFromServer(string serverPath, string pdbIndexPath,
}
});

// Wait 25 seconds allowing for interruptions.
var limit = 250;
// Wait 60 seconds allowing for interruptions.
var limit = 600;

for (int i = 0; i < limit; i++)
{
Expand Down

0 comments on commit c805b11

Please sign in to comment.