chr
2026-04-05 fe750b791d5b517cc4e9bc8e99a9a75139a0cfba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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
{
    /// <summary>  This class stores information about the logged in client. </summary>
    [Browsable(false)]
    public class AuthenticationSettings : ComponentSettings<AuthenticationSettings>
    {
        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<HttpResponseMessage> 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<HttpResponseMessage> SendAsync(HttpRequestMessage request,
                CancellationToken cancellationToken)
            {
                Current.PrepareRequest(request, domain, cancellationToken);
                if (withRetryPolicy)
                    return SendWithRetry(request, cancellationToken);
                return base.SendAsync(request, cancellationToken);
            }
        }
 
        /// <summary>
        /// Token store containing access and refresh tokens.
        /// These tokens are used in the HttpClients returned by <see cref="GetClient"/> to authenticate requests.
        /// </summary>
        public IList<TokenInfo> Tokens { get; set; } = new List<TokenInfo>();
 
        /// <summary> 
        /// A well formed absolute URL used as as BaseAddress in HttpClients returned by <see cref="GetClient"/>. 
        /// This setting determines what relative URLs (e.g. a package repository URL) are relative to.
        /// Can be null.
        /// </summary>
        [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;
 
        /// <summary>
        /// Get a HttpClient with a preconfigued BaseAddress and preconfigured authentication using a Bearer token from <see cref="Tokens"/>.
        /// It is up to the caller of this method to control the lifetime of the HttpClient
        /// </summary>
        /// <param name="domain">This value is compared to <see cref="TokenInfo.Domain"/> to find the token from <see cref="Tokens"/> to use as Bearer token for requests. If unspecified, the host part of the request URI is used.</param>
        /// <param name="withRetryPolicy">If the request should be retried in case of transient errors.</param>
        /// <param name="baseAddress">The base address used in the returned client. A relative URL given here will be relative to <see cref="BaseAddress"/>. Default is <see cref="BaseAddress"/>.</param>
        /// <returns>A preconfigued HttpClient object</returns>
        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;
        }
    }
}