using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Threading; using System.Threading.Tasks; namespace OpenTap.Authentication { /// This class stores information about the logged in client. [Browsable(false)] public class AuthenticationSettings : ComponentSettings { private string baseAddress = null; class AuthenticationClientHandler : HttpClientHandler { private readonly string domain; readonly bool withRetryPolicy; static readonly TimeSpan[] waits = { TimeSpan.Zero, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), }; async Task SendWithRetry(HttpRequestMessage request, CancellationToken cancellationToken) { foreach (var wait in waits) { await Task.Delay(wait, cancellationToken); var result = await base.SendAsync(request, cancellationToken); if (result.IsSuccessStatusCode == false && HttpUtils.TransientStatusCode(result.StatusCode) && wait != waits.Last()) continue; return result; } // There is no chance of getting down here. result from SendAsync is never null. throw new InvalidOperationException(); } public AuthenticationClientHandler(string domain = null, bool withRetryPolicy = false) { this.domain = domain; this.withRetryPolicy = withRetryPolicy; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { Current.PrepareRequest(request, domain, cancellationToken); if (withRetryPolicy) return SendWithRetry(request, cancellationToken); return base.SendAsync(request, cancellationToken); } } /// /// Token store containing access and refresh tokens. /// These tokens are used in the HttpClients returned by to authenticate requests. /// public IList Tokens { get; set; } = new List(); /// /// A well formed absolute URL used as as BaseAddress in HttpClients returned by . /// This setting determines what relative URLs (e.g. a package repository URL) are relative to. /// Can be null. /// [DefaultValue(null)] public string BaseAddress { get => baseAddress; set { if (Uri.IsWellFormedUriString(value, UriKind.Absolute)) baseAddress = value; else if (String.IsNullOrEmpty(value)) baseAddress = null; else throw new FormatException("BaseAddress must be a well formed absolute URI."); } } void PrepareRequest(HttpRequestMessage request, string domain, CancellationToken cancellationToken) { TokenInfo token = null; if (domain != null) token = Tokens.FirstOrDefault(t => t.Domain == domain); if (token == null) token = Tokens.FirstOrDefault(t => t.Domain == request.RequestUri.Host); if (token != null) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); } } private static string userAgent = null; /// /// Get a HttpClient with a preconfigued BaseAddress and preconfigured authentication using a Bearer token from . /// It is up to the caller of this method to control the lifetime of the HttpClient /// /// This value is compared to to find the token from to use as Bearer token for requests. If unspecified, the host part of the request URI is used. /// If the request should be retried in case of transient errors. /// The base address used in the returned client. A relative URL given here will be relative to . Default is . /// A preconfigued HttpClient object public HttpClient GetClient(string domain = null, bool withRetryPolicy = false, string baseAddress = null) { if (Uri.IsWellFormedUriString(domain, UriKind.Absolute)) throw new ArgumentException("Domain should only be the host part of a URI and not a full absolute URI.", nameof(domain)); var client = new HttpClient(new AuthenticationClientHandler(domain, withRetryPolicy)); if (baseAddress != null) { if (Uri.IsWellFormedUriString(baseAddress, UriKind.Absolute)) client.BaseAddress = new Uri(baseAddress); else if (Uri.IsWellFormedUriString(baseAddress, UriKind.Relative)) if (BaseAddress != null) client.BaseAddress = new Uri(new Uri(BaseAddress), baseAddress); else throw new ArgumentException("Address cannot be relative when AuthenticationSettings.BaseAddress is null.", nameof(baseAddress)); else throw new ArgumentException("Address must be a well formed URL or null.", nameof(baseAddress)); } else if(BaseAddress != null) client.BaseAddress = new Uri(BaseAddress); if (userAgent == null) { userAgent = $"OpenTAP/{PluginManager.GetOpenTapAssembly().SemanticVersion}"; if (Cli.CliActionExecutor.SelectedAction is ITypeData td) { // We are running a CLI Action. Add it's name and version to the User-Agent header var source = TypeData.GetTypeDataSource(td); userAgent += $" {td.Name}/{source.Version}"; } else { var asm = Assembly.GetEntryAssembly(); if (asm != null) { var assemblyData = PluginManager.GetSearcher().Assemblies.FirstOrDefault(ad => ad.Location == asm.Location); if (assemblyData?.SemanticVersion is SemanticVersion ver) { // The process was started from an assembly that we know ablout and that has a semantic version number. Add it's name and version to the User-Agent header userAgent += $" {assemblyData.Name}/{ver}"; } } } } var callingUseAgent = userAgent; if (Assembly.GetCallingAssembly() is Assembly asm2) { var assemblyData = PluginManager.GetSearcher().Assemblies.FirstOrDefault(ad => ad.Location == asm2.Location); if (assemblyData?.SemanticVersion is SemanticVersion ver) { // The process was started from an assembly that we know about and that has a semantic version number. Add it's name and version to the User-Agent header callingUseAgent += $" {assemblyData.Name}/{ver}"; } } client.DefaultRequestHeaders.Add("User-Agent", callingUseAgent); return client; } } }