diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 52abd247a..3c63e9634 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -45,6 +45,7 @@ 0.3.1 4.60.1 4.60.1 + 7.1.2 7.1.2 7.1.2 8.0.0 @@ -58,6 +59,8 @@ 4.7.0 6.0.0 + 4.3.1 + 4.7.0 8.0.0 8.0.1 4.5.4 diff --git a/src/PerfView/Authentication.cs b/src/PerfView/Authentication.cs index 2ab6da7aa..a6f764423 100644 --- a/src/PerfView/Authentication.cs +++ b/src/PerfView/Authentication.cs @@ -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, diff --git a/src/PerfView/PerfView.csproj b/src/PerfView/PerfView.csproj index f6b77f73a..eaa9732e4 100644 --- a/src/PerfView/PerfView.csproj +++ b/src/PerfView/PerfView.csproj @@ -37,7 +37,6 @@ en true - PerfView.nuspec $(GenerateNuspecDependsOn);SetNuspecProperties @@ -87,11 +86,13 @@ + + @@ -510,6 +511,13 @@ Microsoft.Identity.Client.Extensions.Msal.dll False + + Non-Resx + false + .\Microsoft.IdentityModel.Abstractions.dll + Microsoft.IdentityModel.Abstractions.dll + False + Non-Resx false @@ -552,6 +560,13 @@ System.Numerics.Vectors.dll False + + Non-Resx + false + .\System.Security.Cryptography.ProtectedData.dll + System.Security.Cryptography.ProtectedData.dll + False + Non-Resx false diff --git a/src/PerfView/SymbolReaderHttpHandler.cs b/src/PerfView/SymbolReaderHttpHandler.cs index 05789cb08..fe9b499a6 100644 --- a/src/PerfView/SymbolReaderHttpHandler.cs +++ b/src/PerfView/SymbolReaderHttpHandler.cs @@ -162,6 +162,26 @@ public SymbolReaderHttpHandler AddAzureDevOpsAuthentication(TextWriter log, bool return AddHandler(new AzureDevOpsHandler(log, new DefaultAzureCredential(options))); } + /// + /// 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. + /// + /// A logger. + /// 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. + /// This instance for fluent chaining. + 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))); + } + /// /// Add a handler for GitHub device flow authentication. /// @@ -1572,6 +1592,101 @@ private static bool TryGetNextChallengeParameter(ref ReadOnlySpan paramete } } + /// + /// A handler that adds authorization for Symweb. + /// + internal sealed class SymwebHandler : SymbolReaderAuthHandlerBase + { + /// + /// The value of stored in a single element + /// array suitable for passing to + /// . + /// + private static readonly string[] s_scopes = new[] { Symweb.Scope }; + + /// + /// Prefix to put in front of logging messages. + /// + private const string LogPrefix = "SymwebAuth: "; + + /// + /// A provider of access tokens. + /// + private readonly TokenCredential _tokenCredential; + + /// + /// Protect against concurrent access. + /// + private readonly SemaphoreSlim _tokenCredentialGate = new SemaphoreSlim(initialCount: 1); + + /// + /// An HTTP client used to discover the authority (login endpoint and tenant) for Symweb. + /// + private readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler() { CheckCertificateRevocationList = true }); + + /// + /// Construct a new instance. + /// + /// A provider of access tokens. + public SymwebHandler(TextWriter log, TokenCredential tokenCredential) : base(log, LogPrefix) + { + _tokenCredential = tokenCredential ?? throw new ArgumentNullException(nameof(tokenCredential)); + } + + /// + /// Try to find the authority endpoint for Symweb + /// given a full URI. + /// + /// The request URI. + /// The authority, if found. + /// True if represents a path to a + /// resource in Symweb. + protected override bool TryGetAuthority(Uri requestUri, out Uri authority) => Symweb.TryGetAuthority(requestUri, out authority); + + /// + /// Get a token to access Symweb. + /// + /// The request context. + /// The next handler. + /// The Symweb instance. + /// A cancellation token. + /// An access token, or null if one could not be obtained. + protected override async Task 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; + } + + /// + /// Get a new access token for Symweb from the . + /// + /// A cancellation token. + /// The access token. + private async Task 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(); + } + } + } + /// /// A handler that handles GitHub device flow authorization. /// @@ -1861,6 +1976,64 @@ private sealed class AccessTokenResponse } } + /// + /// Contains constants, static properties and helper methods pertinent to Symweb. + /// + internal static class Symweb + { + /// + /// The OAuth scope to use when requesting tokens for Symweb. + /// + public const string Scope = "af9e1c69-e5e9-4331-8cc5-cdf93d57bafa/.default"; + + /// + /// The url host for Symweb. + /// + public const string SymwebHost = "symweb.azurefd.net"; + + /// + /// Try to find the authority endpoint for Symweb given a full URI. + /// + /// The request URI. + /// The authority, if found. + /// True if represents a path to a + /// resource in Symweb. + 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; + } + } + /// /// Contains constants, static properties and helper methods pertinent to Azure DevOps. /// diff --git a/src/TraceEvent/Symbols/SymbolPath.cs b/src/TraceEvent/Symbols/SymbolPath.cs index 54ed139cf..81685aa56 100644 --- a/src/TraceEvent/Symbols/SymbolPath.cs +++ b/src/TraceEvent/Symbols/SymbolPath.cs @@ -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; } diff --git a/src/TraceEvent/Symbols/SymbolReader.cs b/src/TraceEvent/Symbols/SymbolReader.cs index 5d3c1a102..67fcfc59c 100644 --- a/src/TraceEvent/Symbols/SymbolReader.cs +++ b/src/TraceEvent/Symbols/SymbolReader.cs @@ -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(); @@ -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++) {