-
Notifications
You must be signed in to change notification settings - Fork 702
/
Copy pathHttpSourceAuthenticationHandler.cs
318 lines (267 loc) · 11.1 KB
/
HttpSourceAuthenticationHandler.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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Configuration;
namespace NuGet.Protocol
{
public class HttpSourceAuthenticationHandler : DelegatingHandler
{
public static readonly int MaxAuthRetries = AmbientAuthenticationState.MaxAuthRetries;
// Only one source may prompt at a time
private readonly static SemaphoreSlim _credentialPromptLock = new SemaphoreSlim(1, 1);
private readonly PackageSource _packageSource;
private readonly HttpClientHandler _clientHandler;
private readonly ICredentialService _credentialService;
private readonly SemaphoreSlim _httpClientLock = new SemaphoreSlim(1, 1);
private Dictionary<string, AmbientAuthenticationState> _authStates = new Dictionary<string, AmbientAuthenticationState>();
private HttpSourceCredentials _credentials;
private bool _isDisposed = false;
public HttpSourceAuthenticationHandler(
PackageSource packageSource,
HttpClientHandler clientHandler,
ICredentialService credentialService)
: base(clientHandler)
{
_packageSource = packageSource ?? throw new ArgumentNullException(nameof(packageSource));
_clientHandler = clientHandler ?? throw new ArgumentNullException(nameof(clientHandler));
// credential service is optional as credentials may be attached to a package source
_credentialService = credentialService;
// Create a new wrapper for ICredentials that can be modified
if (_credentialService == null || !_credentialService.HandlesDefaultCredentials)
{
// This is used to match the value of HttpClientHandler.UseDefaultCredentials = true
_credentials = new HttpSourceCredentials(CredentialCache.DefaultNetworkCredentials);
}
else
{
_credentials = new HttpSourceCredentials();
}
if (packageSource.Credentials != null &&
packageSource.Credentials.IsValid())
{
_credentials.Credentials = packageSource.Credentials.ToICredentials();
}
_clientHandler.Credentials = _credentials;
// Always take the credentials from the helper.
_clientHandler.UseDefaultCredentials = false;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (_isDisposed)
{
throw new ObjectDisposedException(objectName: null); // we don't know the caller name
}
HttpResponseMessage response = null;
ICredentials promptCredentials = null;
var configuration = request.GetOrCreateConfiguration();
// Authorizing may take multiple attempts
while (true)
{
// Clean up any previous responses
if (response != null)
{
response.Dispose();
}
// store the auth state before sending the request
var beforeLockVersion = _credentials.Version;
using (var req = request.Clone())
{
response = await base.SendAsync(req, cancellationToken);
}
if (_credentialService == null)
{
return response;
}
if (response.StatusCode == HttpStatusCode.Unauthorized ||
(configuration.PromptOn403 && response.StatusCode == HttpStatusCode.Forbidden))
{
List<Stopwatch> stopwatches = null;
#if NET5_0_OR_GREATER
if (request.Options.TryGetValue(
new HttpRequestOptionsKey<List<Stopwatch>>(HttpRetryHandler.StopwatchPropertyName),
out stopwatches))
{
#else
if (request.Properties.TryGetValue(HttpRetryHandler.StopwatchPropertyName, out var value))
{
stopwatches = value as List<Stopwatch>;
#endif
if (stopwatches != null)
{
foreach (var stopwatch in stopwatches)
{
stopwatch.Stop();
}
}
}
promptCredentials = await AcquireCredentialsAsync(
response.StatusCode,
beforeLockVersion,
configuration.Logger,
cancellationToken);
if (stopwatches != null)
{
foreach (var stopwatch in stopwatches)
{
stopwatch.Start();
}
}
if (promptCredentials == null)
{
return response;
}
continue;
}
if (promptCredentials != null)
{
CredentialsSuccessfullyUsed(_packageSource.SourceUri, promptCredentials);
}
return response;
}
}
private async Task<ICredentials> AcquireCredentialsAsync(HttpStatusCode statusCode, Guid credentialsVersion, ILogger log, CancellationToken cancellationToken)
{
// Only one request may prompt and attempt to auth at a time
await _httpClientLock.WaitAsync(cancellationToken);
try
{
cancellationToken.ThrowIfCancellationRequested();
// Auth may have happened on another thread, if so just continue
if (credentialsVersion != _credentials.Version)
{
return _credentials.Credentials;
}
var authState = GetAuthenticationState();
if (authState.IsBlocked)
{
cancellationToken.ThrowIfCancellationRequested();
return null;
}
// Construct a reasonable message for the prompt to use.
CredentialRequestType type;
string message;
if (statusCode == HttpStatusCode.Unauthorized)
{
type = CredentialRequestType.Unauthorized;
message = string.Format(
CultureInfo.CurrentCulture,
Strings.Http_CredentialsForUnauthorized,
_packageSource.Source);
}
else
{
type = CredentialRequestType.Forbidden;
message = string.Format(
CultureInfo.CurrentCulture,
Strings.Http_CredentialsForForbidden,
_packageSource.Source);
}
var promptCredentials = await PromptForCredentialsAsync(
type,
message,
authState,
log,
cancellationToken);
if (promptCredentials == null)
{
return null;
}
_credentials.Credentials = promptCredentials;
return promptCredentials;
}
finally
{
_httpClientLock.Release();
}
}
private AmbientAuthenticationState GetAuthenticationState()
{
var correlationId = ActivityCorrelationId.Current;
AmbientAuthenticationState authState;
if (!_authStates.TryGetValue(correlationId, out authState))
{
authState = new AmbientAuthenticationState();
_authStates[correlationId] = authState;
}
return authState;
}
private async Task<ICredentials> PromptForCredentialsAsync(
CredentialRequestType type,
string message,
AmbientAuthenticationState authState,
ILogger log,
CancellationToken token)
{
ICredentials promptCredentials;
// Only one prompt may display at a time.
await _credentialPromptLock.WaitAsync(token);
try
{
// Get the proxy for this URI so we can pass it to the credentialService methods
// this lets them use the proxy if they have to hit the network.
var proxyCache = ProxyCache.Instance;
var proxy = proxyCache?.GetProxy(_packageSource.SourceUri);
promptCredentials = await _credentialService
.GetCredentialsAsync(_packageSource.SourceUri, proxy, type, message, token);
if (promptCredentials == null)
{
// If this is the case, this means none of the credential providers were able to
// handle the credential request or no credentials were available for the
// endpoint.
authState.Block();
}
else
{
authState.Increment();
}
}
catch (OperationCanceledException)
{
// This indicates a non-human cancellation.
throw;
}
catch (Exception e)
{
// If this is the case, this means there was a fatal exception when interacting
// with the credential service (or its underlying credential providers). Either way,
// block asking for credentials for the live of this operation.
log.LogError(ExceptionUtilities.DisplayMessage(e));
promptCredentials = null;
authState.Block();
}
finally
{
_credentialPromptLock.Release();
}
return promptCredentials;
}
private void CredentialsSuccessfullyUsed(Uri uri, ICredentials credentials)
{
HttpHandlerResourceV3.CredentialsSuccessfullyUsed?.Invoke(uri, credentials);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (_isDisposed)
{
return;
}
if (disposing)
{
// free managed resources
_httpClientLock.Dispose();
_authStates = null;
_credentials = null;
}
_isDisposed = true;
}
}
}