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;
}
}
}