-
Notifications
You must be signed in to change notification settings - Fork 1.9k
/
Copy pathHostProviderRegistry.cs
228 lines (198 loc) · 10.4 KB
/
HostProviderRegistry.cs
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace Microsoft.Git.CredentialManager
{
/// <summary>
/// Priority in which host providers are queried during auto-detection.
/// </summary>
public enum HostProviderPriority
{
Low = 0,
Normal = 1,
High = 2,
}
/// <summary>
/// Represents a collection of <see cref="IHostProvider"/>s which are selected based on Git credential query
/// <see cref="InputArguments"/>.
/// </summary>
/// <remarks>
/// All registered <see cref="IHostProvider"/>s will be disposed when this <see cref="IHostProviderRegistry"/> is disposed.
/// </remarks>
public interface IHostProviderRegistry : IDisposable
{
/// <summary>
/// Add the given <see cref="IHostProvider"/> to this registry.
/// </summary>
/// <param name="hostProvider">Host provider to register.</param>
/// <param name="priority">Priority at which the provider will be considered when auto-detecting.</param>
/// <remarks>Providers will be disposed of when this registry instance is disposed itself.</remarks>
void Register(IHostProvider hostProvider, HostProviderPriority priority);
/// <summary>
/// Select a <see cref="IHostProvider"/> that can service the Git credential query based on the
/// <see cref="InputArguments"/>.
/// </summary>
/// <param name="input">Input arguments of a Git credential query.</param>
/// <returns>A host provider that can service the given query.</returns>
Task<IHostProvider> GetProviderAsync(InputArguments input);
}
/// <summary>
/// Host provider registry where each provider is queried by priority order until the first
/// provider that supports the credential query or matches the endpoint query is found.
/// </summary>
public class HostProviderRegistry : IHostProviderRegistry
{
private readonly ICommandContext _context;
private readonly IDictionary<HostProviderPriority, ICollection<IHostProvider>> _hostProviders;
public HostProviderRegistry(ICommandContext context)
{
EnsureArgument.NotNull(context, nameof(context));
_context = context;
_hostProviders = new Dictionary<HostProviderPriority, ICollection<IHostProvider>>();
}
public void Register(IHostProvider hostProvider, HostProviderPriority priority)
{
EnsureArgument.NotNull(hostProvider, nameof(hostProvider));
if (StringComparer.OrdinalIgnoreCase.Equals(hostProvider.Id, Constants.ProviderIdAuto))
{
throw new ArgumentException(
$"A host provider cannot be registered with the ID '{Constants.ProviderIdAuto}'",
nameof(hostProvider));
}
if (hostProvider.SupportedAuthorityIds.Any(y => StringComparer.OrdinalIgnoreCase.Equals(y, Constants.AuthorityIdAuto)))
{
throw new ArgumentException(
$"A host provider cannot be registered with the legacy authority ID '{Constants.AuthorityIdAuto}'",
nameof(hostProvider));
}
if (!_hostProviders.TryGetValue(priority, out ICollection<IHostProvider> providers))
{
providers = new List<IHostProvider>();
_hostProviders[priority] = providers;
}
providers.Add(hostProvider);
}
public async Task<IHostProvider> GetProviderAsync(InputArguments input)
{
IHostProvider provider;
//
// Try and locate a specified provider
//
if (_context.Settings.ProviderOverride is string providerId)
{
_context.Trace.WriteLine($"Host provider override was set id='{providerId}'");
if (!StringComparer.OrdinalIgnoreCase.Equals(Constants.ProviderIdAuto, providerId))
{
provider = _hostProviders
.SelectMany(x => x.Value)
.FirstOrDefault(x => StringComparer.OrdinalIgnoreCase.Equals(x.Id, providerId));
if (provider is null)
{
_context.Trace.WriteLine($"No host provider was found with ID '{providerId}'.. falling back to auto-detection.");
_context.Streams.Error.WriteLine($"warning: a host provider override was set but no such provider '{providerId}' was found. Falling back to auto-detection.");
}
else
{
return provider;
}
}
}
//
// Try and locate a provider by supported authorities
//
else if (_context.Settings.LegacyAuthorityOverride is string authority)
{
_context.Trace.WriteLine($"Host provider authority override was set authority='{authority}'");
_context.Streams.Error.WriteLine("warning: the `credential.authority` and `GCM_AUTHORITY` settings are deprecated.");
_context.Streams.Error.WriteLine($"warning: see {Constants.HelpUrls.GcmAuthorityDeprecated} for more information.");
if (!StringComparer.OrdinalIgnoreCase.Equals(Constants.AuthorityIdAuto, authority))
{
provider = _hostProviders
.SelectMany(x => x.Value)
.FirstOrDefault(x => x.SupportedAuthorityIds.Contains(authority, StringComparer.OrdinalIgnoreCase));
if (provider is null)
{
_context.Trace.WriteLine($"No host provider was found with authority '{authority}'.. falling back to auto-detection.");
_context.Streams.Error.WriteLine($"warning: a supported authority override was set but no such provider supporting authority '{authority}' was found. Falling back to auto-detection.");
}
else
{
return provider;
}
}
}
//
// Auto-detection
//
_context.Trace.WriteLine("Performing auto-detection of host provider.");
var uri = input.GetRemoteUri();
var probeTimeout = TimeSpan.FromMilliseconds(_context.Settings.AutoDetectProviderTimeout);
_context.Trace.WriteLine($"Auto-detect probe timeout is {probeTimeout.TotalSeconds} ms.");
HttpResponseMessage probeResponse = null;
async Task<IHostProvider> MatchProviderAsync(HostProviderPriority priority)
{
if (_hostProviders.TryGetValue(priority, out ICollection<IHostProvider> providers))
{
_context.Trace.WriteLine($"Checking against {providers.Count} host providers registered with priority '{priority}'.");
// Try matching using the static Git input arguments first (cheap)
if (providers.TryGetFirst(x => x.IsSupported(input), out IHostProvider match))
{
return match;
}
// Try matching using the HTTP response from a query to the remote URL (expensive).
// The user may have disabled this feature with a zero or negative timeout for performance reasons.
// We only probe the remote once and reuse the same response for all providers.
if (probeTimeout.TotalMilliseconds > 0)
{
if (probeResponse is null)
{
_context.Trace.WriteLine("Querying remote URL for host provider auto-detection.");
_context.Streams.Error.WriteLine($"info: detecting host provider for '{uri}'...");
using (HttpClient client = _context.HttpClientFactory.CreateClient())
{
client.Timeout = probeTimeout;
try
{
probeResponse = await client.HeadAsync(uri);
}
catch (TaskCanceledException)
{
_context.Streams.Error.WriteLine($"warning: auto-detection of host provider took too long (>{probeTimeout.TotalMilliseconds}ms)");
_context.Streams.Error.WriteLine($"warning: see {Constants.HelpUrls.GcmAutoDetect} for more information.");
}
catch (Exception ex)
{
// The auto detect probing failed for some other reason.
// We don't particular care why, but we should not crash!
_context.Streams.Error.WriteLine($"warning: failed to probe '{uri}' to detect provider");
_context.Streams.Error.WriteLine($"warning: {ex.Message}");
_context.Streams.Error.WriteLine($"warning: see {Constants.HelpUrls.GcmAutoDetect} for more information.");
}
}
}
if (providers.TryGetFirst(x => x.IsSupported(probeResponse), out match))
{
return match;
}
}
}
return null;
}
// Match providers starting with the highest priority
return await MatchProviderAsync(HostProviderPriority.High) ??
await MatchProviderAsync(HostProviderPriority.Normal) ??
await MatchProviderAsync(HostProviderPriority.Low) ??
throw new Exception("No host provider available to service this request.");
}
public void Dispose()
{
// Dispose of all registered providers to give them a chance to clean up and release any resources
foreach (IHostProvider provider in _hostProviders.Values.SelectMany(x => x))
{
provider.Dispose();
}
}
}
}