mirror of
https://github.com/actions/runner.git
synced 2025-12-10 12:36:23 +00:00
638 lines
27 KiB
C#
638 lines
27 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using GitHub.Services.Common.Diagnostics;
|
|
using GitHub.Services.Common.Internal;
|
|
|
|
namespace GitHub.Services.Common
|
|
{
|
|
/// <summary>
|
|
/// Provides authentication for Visual Studio Services.
|
|
/// </summary>
|
|
public class VssHttpMessageHandler : HttpMessageHandler
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new <c>VssHttpMessageHandler</c> instance with default credentials and request
|
|
/// settings.
|
|
/// </summary>
|
|
public VssHttpMessageHandler()
|
|
: this(new VssCredentials(), new VssHttpRequestSettings())
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new <c>VssHttpMessageHandler</c> instance with the specified credentials and request
|
|
/// settings.
|
|
/// </summary>
|
|
/// <param name="credentials">The credentials which should be used</param>
|
|
/// <param name="settings">The request settings which should be used</param>
|
|
public VssHttpMessageHandler(
|
|
VssCredentials credentials,
|
|
VssHttpRequestSettings settings)
|
|
: this(credentials, settings, new HttpClientHandler())
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new <c>VssHttpMessageHandler</c> instance with the specified credentials and request
|
|
/// settings.
|
|
/// </summary>
|
|
/// <param name="credentials">The credentials which should be used</param>
|
|
/// <param name="settings">The request settings which should be used</param>
|
|
/// <param name="innerHandler"></param>
|
|
public VssHttpMessageHandler(
|
|
VssCredentials credentials,
|
|
VssHttpRequestSettings settings,
|
|
HttpMessageHandler innerHandler)
|
|
{
|
|
this.Credentials = credentials;
|
|
this.Settings = settings;
|
|
this.ExpectContinue = settings.ExpectContinue;
|
|
|
|
m_credentialWrapper = new CredentialWrapper();
|
|
m_messageInvoker = new HttpMessageInvoker(innerHandler);
|
|
|
|
// If we were given a pipeline make sure we find the inner-most handler to apply our settings as this
|
|
// will be the actual outgoing transport.
|
|
{
|
|
HttpMessageHandler transportHandler = innerHandler;
|
|
DelegatingHandler delegatingHandler = transportHandler as DelegatingHandler;
|
|
while (delegatingHandler != null)
|
|
{
|
|
transportHandler = delegatingHandler.InnerHandler;
|
|
delegatingHandler = transportHandler as DelegatingHandler;
|
|
}
|
|
|
|
m_transportHandler = transportHandler;
|
|
}
|
|
|
|
ApplySettings(m_transportHandler, m_credentialWrapper, this.Settings);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the credentials associated with this handler.
|
|
/// </summary>
|
|
public VssCredentials Credentials
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the settings associated with this handler.
|
|
/// </summary>
|
|
public VssHttpRequestSettings Settings
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
private Boolean ExpectContinue
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
protected override void Dispose(Boolean disposing)
|
|
{
|
|
base.Dispose(disposing);
|
|
|
|
if (disposing)
|
|
{
|
|
if (m_messageInvoker != null)
|
|
{
|
|
m_messageInvoker.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static readonly String PropertyName = "MS.VS.MessageHandler";
|
|
|
|
/// <summary>
|
|
/// Handles the authentication hand-shake for a Visual Studio service.
|
|
/// </summary>
|
|
/// <param name="request">The HTTP request message</param>
|
|
/// <param name="cancellationToken">The cancellation token used for cooperative cancellation</param>
|
|
/// <returns>A new <c>Task<HttpResponseMessage></c> which wraps the response from the remote service</returns>
|
|
protected override async Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
VssTraceActivity traceActivity = VssTraceActivity.Current;
|
|
|
|
var traceInfo = VssHttpMessageHandlerTraceInfo.GetTraceInfo(request);
|
|
traceInfo?.TraceHandlerStartTime();
|
|
|
|
if (!m_appliedClientCertificatesToTransportHandler &&
|
|
request.RequestUri.Scheme == "https")
|
|
{
|
|
HttpClientHandler httpClientHandler = m_transportHandler as HttpClientHandler;
|
|
if (httpClientHandler != null &&
|
|
this.Settings.ClientCertificateManager != null &&
|
|
this.Settings.ClientCertificateManager.ClientCertificates != null &&
|
|
this.Settings.ClientCertificateManager.ClientCertificates.Count > 0)
|
|
{
|
|
httpClientHandler.ClientCertificates.AddRange(this.Settings.ClientCertificateManager.ClientCertificates);
|
|
}
|
|
m_appliedClientCertificatesToTransportHandler = true;
|
|
}
|
|
|
|
if (!m_appliedServerCertificateValidationCallbackToTransportHandler &&
|
|
request.RequestUri.Scheme == "https")
|
|
{
|
|
HttpClientHandler httpClientHandler = m_transportHandler as HttpClientHandler;
|
|
if (httpClientHandler != null &&
|
|
this.Settings.ServerCertificateValidationCallback != null)
|
|
{
|
|
httpClientHandler.ServerCertificateCustomValidationCallback = this.Settings.ServerCertificateValidationCallback;
|
|
}
|
|
m_appliedServerCertificateValidationCallbackToTransportHandler = true;
|
|
}
|
|
|
|
// The .NET Core 2.1 runtime switched its HTTP default from HTTP 1.1 to HTTP 2.
|
|
// This causes problems with some versions of the Curl handler on Linux.
|
|
// See GitHub issue https://github.com/dotnet/corefx/issues/32376
|
|
if (Settings.UseHttp11)
|
|
{
|
|
request.Version = HttpVersion.Version11;
|
|
}
|
|
|
|
IssuedToken token = null;
|
|
IssuedTokenProvider provider;
|
|
if (this.Credentials.TryGetTokenProvider(request.RequestUri, out provider))
|
|
{
|
|
token = provider.CurrentToken;
|
|
}
|
|
|
|
// Add ourselves to the message so the underlying token issuers may use it if necessary
|
|
request.Options.Set(new HttpRequestOptionsKey<VssHttpMessageHandler>(VssHttpMessageHandler.PropertyName), this);
|
|
|
|
Boolean succeeded = false;
|
|
Boolean lastResponseDemandedProxyAuth = false;
|
|
Int32 retries = m_maxAuthRetries;
|
|
HttpResponseMessage response = null;
|
|
HttpResponseMessageWrapper responseWrapper;
|
|
CancellationTokenSource tokenSource = null;
|
|
|
|
try
|
|
{
|
|
tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
|
|
if (this.Settings.SendTimeout > TimeSpan.Zero)
|
|
{
|
|
tokenSource.CancelAfter(this.Settings.SendTimeout);
|
|
}
|
|
|
|
do
|
|
{
|
|
if (response != null)
|
|
{
|
|
response.Dispose();
|
|
}
|
|
|
|
ApplyHeaders(request);
|
|
|
|
// In the case of a Windows token, only apply it to the web proxy if it
|
|
// returned a 407 Proxy Authentication Required. If we didn't get this
|
|
// status code back, then the proxy (if there is one) is clearly working fine,
|
|
// so we shouldn't mess with its credentials.
|
|
ApplyToken(request, token, applyICredentialsToWebProxy: lastResponseDemandedProxyAuth);
|
|
lastResponseDemandedProxyAuth = false;
|
|
|
|
// The WinHttpHandler will chunk any content that does not have a computed length which is
|
|
// not what we want. By loading into a buffer up-front we bypass this behavior and there is
|
|
// no difference in the normal HttpClientHandler behavior here since this is what they were
|
|
// already doing.
|
|
await BufferRequestContentAsync(request, tokenSource.Token).ConfigureAwait(false);
|
|
|
|
traceInfo?.TraceBufferedRequestTime();
|
|
|
|
// ConfigureAwait(false) enables the continuation to be run outside any captured
|
|
// SyncronizationContext (such as ASP.NET's) which keeps things from deadlocking...
|
|
|
|
response = await m_messageInvoker.SendAsync(request, tokenSource.Token).ConfigureAwait(false);
|
|
|
|
traceInfo?.TraceRequestSendTime();
|
|
|
|
// Now buffer the response content if configured to do so. In general we will be buffering
|
|
// the response content in this location, except in the few cases where the caller has
|
|
// specified HttpCompletionOption.ResponseHeadersRead.
|
|
// Trace content type in case of error
|
|
await BufferResponseContentAsync(request, response, () => $"[ContentType: {response.Content.GetType().Name}]", tokenSource.Token).ConfigureAwait(false);
|
|
|
|
traceInfo?.TraceResponseContentTime();
|
|
|
|
responseWrapper = new HttpResponseMessageWrapper(response);
|
|
|
|
if (!this.Credentials.IsAuthenticationChallenge(responseWrapper))
|
|
{
|
|
// Validate the token after it has been successfully authenticated with the server.
|
|
if (provider != null)
|
|
{
|
|
provider.ValidateToken(token, responseWrapper);
|
|
}
|
|
|
|
// Make sure that once we can authenticate with the service that we turn off the
|
|
// Expect100Continue behavior to increase performance.
|
|
this.ExpectContinue = false;
|
|
succeeded = true;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
// In the case of a Windows token, only apply it to the web proxy if it
|
|
// returned a 407 Proxy Authentication Required. If we didn't get this
|
|
// status code back, then the proxy (if there is one) is clearly working fine,
|
|
// so we shouldn't mess with its credentials.
|
|
lastResponseDemandedProxyAuth = responseWrapper.StatusCode == HttpStatusCode.ProxyAuthenticationRequired;
|
|
|
|
// Invalidate the token and ensure that we have the correct token provider for the challenge
|
|
// which we just received
|
|
if (retries < m_maxAuthRetries)
|
|
{
|
|
VssHttpEventSource.Log.AuthenticationFailed(traceActivity, response);
|
|
}
|
|
else
|
|
{
|
|
VssHttpEventSource.Log.AuthenticationFailedOnFirstRequest(traceActivity, response);
|
|
}
|
|
|
|
if (provider != null)
|
|
{
|
|
provider.InvalidateToken(token);
|
|
}
|
|
|
|
// Ensure we have an appropriate token provider for the current challenge
|
|
provider = this.Credentials.CreateTokenProvider(request.RequestUri, responseWrapper, token);
|
|
|
|
// Make sure we don't invoke the provider in an invalid state
|
|
if (provider == null)
|
|
{
|
|
VssHttpEventSource.Log.IssuedTokenProviderNotFound(traceActivity);
|
|
break;
|
|
}
|
|
else if (provider.GetTokenIsInteractive && this.Credentials.PromptType == CredentialPromptType.DoNotPrompt)
|
|
{
|
|
VssHttpEventSource.Log.IssuedTokenProviderPromptRequired(traceActivity, provider);
|
|
break;
|
|
}
|
|
|
|
// If the user has already tried once but still unauthorized, stop retrying. The main scenario for this condition
|
|
// is a user typed in a valid credentials for a hosted account but the associated identity does not have
|
|
// access. We do not want to continually prompt 3 times without telling them the failure reason. In the
|
|
// next release we should rethink about presenting user the failure and options between retries.
|
|
IEnumerable<String> headerValues;
|
|
Boolean hasAuthenticateError =
|
|
response.Headers.TryGetValues(HttpHeaders.VssAuthenticateError, out headerValues) &&
|
|
!String.IsNullOrEmpty(headerValues.FirstOrDefault());
|
|
|
|
if (retries == 0 || (retries < m_maxAuthRetries && hasAuthenticateError))
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Now invoke the provider and await the result
|
|
token = await provider.GetTokenAsync(token, tokenSource.Token).ConfigureAwait(false);
|
|
|
|
// I always see 0 here, but the method above could take more time so keep for now
|
|
traceInfo?.TraceGetTokenTime();
|
|
|
|
// If we just received a token, lets ask the server for the VSID
|
|
request.Headers.Add(HttpHeaders.VssUserData, String.Empty);
|
|
|
|
retries--;
|
|
}
|
|
}
|
|
while (retries >= 0);
|
|
|
|
if (traceInfo != null)
|
|
{
|
|
traceInfo.TokenRetries = m_maxAuthRetries - retries;
|
|
}
|
|
|
|
// We're out of retries and the response was an auth challenge -- then the request was unauthorized
|
|
// and we will throw a strongly-typed exception with a friendly error message.
|
|
if (!succeeded && response != null && this.Credentials.IsAuthenticationChallenge(responseWrapper))
|
|
{
|
|
String message = null;
|
|
IEnumerable<String> serviceError;
|
|
|
|
if (response.Headers.TryGetValues(HttpHeaders.TfsServiceError, out serviceError))
|
|
{
|
|
message = UriUtility.UrlDecode(serviceError.FirstOrDefault());
|
|
}
|
|
else
|
|
{
|
|
message = CommonResources.VssUnauthorized(request.RequestUri.GetLeftPart(UriPartial.Authority));
|
|
}
|
|
|
|
// Make sure we do not leak the response object when raising an exception
|
|
if (response != null)
|
|
{
|
|
response.Dispose();
|
|
}
|
|
|
|
VssHttpEventSource.Log.HttpRequestUnauthorized(traceActivity, request, message);
|
|
VssUnauthorizedException unauthorizedException = new VssUnauthorizedException(message);
|
|
|
|
if (provider != null)
|
|
{
|
|
unauthorizedException.Data.Add(CredentialsType, provider.CredentialType);
|
|
}
|
|
|
|
throw unauthorizedException;
|
|
}
|
|
|
|
return response;
|
|
}
|
|
catch (OperationCanceledException ex)
|
|
{
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
VssHttpEventSource.Log.HttpRequestCancelled(traceActivity, request);
|
|
throw;
|
|
}
|
|
else
|
|
{
|
|
VssHttpEventSource.Log.HttpRequestTimedOut(traceActivity, request, this.Settings.SendTimeout);
|
|
throw new TimeoutException(CommonResources.HttpRequestTimeout(this.Settings.SendTimeout), ex);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// We always dispose of the token source since otherwise we leak resources if there is a timer pending
|
|
if (tokenSource != null)
|
|
{
|
|
tokenSource.Dispose();
|
|
}
|
|
|
|
traceInfo?.TraceTrailingTime();
|
|
}
|
|
}
|
|
|
|
private static async Task BufferRequestContentAsync(
|
|
HttpRequestMessage request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (request.Content != null &&
|
|
request.Headers.TransferEncodingChunked != true)
|
|
{
|
|
Int64? contentLength = request.Content.Headers.ContentLength;
|
|
if (contentLength == null)
|
|
{
|
|
await request.Content.LoadIntoBufferAsync().EnforceCancellation(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
// Explicitly turn off chunked encoding since we have computed the request content size
|
|
request.Headers.TransferEncodingChunked = false;
|
|
}
|
|
}
|
|
|
|
protected virtual async Task BufferResponseContentAsync(
|
|
HttpRequestMessage request,
|
|
HttpResponseMessage response,
|
|
Func<string> makeErrorMessage,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
// Determine whether or not we should go ahead and buffer the output under our timeout scope. If
|
|
// we do not perform this action here there is a potential network stack hang since we override
|
|
// the HttpClient.SendTimeout value and the cancellation token for monitoring request timeout does
|
|
// not survive beyond this scope.
|
|
if (response == null || response.StatusCode == HttpStatusCode.NoContent || response.Content == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Do not try to buffer with a size of 0. This forces all calls to effectively use the behavior of
|
|
// HttpCompletionOption.ResponseHeadersRead if that is desired.
|
|
if (this.Settings.MaxContentBufferSize == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Read the completion option provided by the caller. If we don't find the property then we
|
|
// assume it is OK to buffer by default.
|
|
HttpCompletionOption completionOption;
|
|
if (!request.Options.TryGetValue(VssHttpRequestSettings.HttpCompletionOptionPropertyName, out completionOption))
|
|
{
|
|
completionOption = HttpCompletionOption.ResponseContentRead;
|
|
}
|
|
|
|
// If the caller specified that response content should be read then we need to go ahead and
|
|
// buffer it all up to the maximum buffer size specified by the settings. Anything larger than
|
|
// the maximum will trigger an error in the underlying stack.
|
|
if (completionOption == HttpCompletionOption.ResponseContentRead)
|
|
{
|
|
await response.Content.LoadIntoBufferAsync(this.Settings.MaxContentBufferSize).EnforceCancellation(cancellationToken, makeErrorMessage).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private void ApplyHeaders(HttpRequestMessage request)
|
|
{
|
|
if (this.Settings.ApplyTo(request))
|
|
{
|
|
VssTraceActivity activity = request.GetActivity();
|
|
if (activity != null &&
|
|
activity != VssTraceActivity.Empty &&
|
|
!request.Headers.Contains(HttpHeaders.TfsSessionHeader))
|
|
{
|
|
request.Headers.Add(HttpHeaders.TfsSessionHeader, activity.Id.ToString("D"));
|
|
}
|
|
|
|
request.Headers.ExpectContinue = this.ExpectContinue;
|
|
}
|
|
}
|
|
|
|
private void ApplyToken(
|
|
HttpRequestMessage request,
|
|
IssuedToken token,
|
|
bool applyICredentialsToWebProxy = false)
|
|
{
|
|
if (token == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ICredentials credentialsToken = token as ICredentials;
|
|
if (credentialsToken != null)
|
|
{
|
|
if (applyICredentialsToWebProxy)
|
|
{
|
|
HttpClientHandler httpClientHandler = m_transportHandler as HttpClientHandler;
|
|
|
|
if (httpClientHandler != null &&
|
|
httpClientHandler.Proxy != null)
|
|
{
|
|
httpClientHandler.Proxy.Credentials = credentialsToken;
|
|
}
|
|
}
|
|
|
|
m_credentialWrapper.InnerCredentials = credentialsToken;
|
|
}
|
|
else
|
|
{
|
|
token.ApplyTo(new HttpRequestMessageWrapper(request));
|
|
}
|
|
}
|
|
|
|
private static void ApplySettings(
|
|
HttpMessageHandler handler,
|
|
ICredentials defaultCredentials,
|
|
VssHttpRequestSettings settings)
|
|
{
|
|
HttpClientHandler httpClientHandler = handler as HttpClientHandler;
|
|
if (httpClientHandler != null)
|
|
{
|
|
httpClientHandler.AllowAutoRedirect = settings.AllowAutoRedirect;
|
|
httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
|
|
//Setting httpClientHandler.UseDefaultCredentials to false in .Net Core, clears httpClientHandler.Credentials if
|
|
//credentials is already set to defaultcredentials. Therefore httpClientHandler.Credentials must be
|
|
//set after httpClientHandler.UseDefaultCredentials.
|
|
httpClientHandler.UseDefaultCredentials = false;
|
|
httpClientHandler.Credentials = defaultCredentials;
|
|
httpClientHandler.PreAuthenticate = false;
|
|
httpClientHandler.Proxy = DefaultWebProxy;
|
|
httpClientHandler.UseCookies = false;
|
|
httpClientHandler.UseProxy = true;
|
|
|
|
if (settings.CompressionEnabled)
|
|
{
|
|
httpClientHandler.AutomaticDecompression = DecompressionMethods.GZip;
|
|
}
|
|
}
|
|
}
|
|
|
|
// setting this to WebRequest.DefaultWebProxy in NETSTANDARD is causing a System.PlatformNotSupportedException
|
|
//.in System.Net.SystemWebProxy.IsBypassed. Comment in IsBypassed method indicates ".NET Core and .NET Native
|
|
// code will handle this exception and call into WinInet/WinHttp as appropriate to use the system proxy."
|
|
// This needs to be investigated further.
|
|
private static IWebProxy s_defaultWebProxy = null;
|
|
|
|
/// <summary>
|
|
/// Allows you to set a proxy to be used by all VssHttpMessageHandler requests without affecting the global WebRequest.DefaultWebProxy. If not set it returns the WebRequest.DefaultWebProxy.
|
|
/// </summary>
|
|
public static IWebProxy DefaultWebProxy
|
|
{
|
|
get
|
|
{
|
|
var toReturn = WebProxyWrapper.Wrap(s_defaultWebProxy);
|
|
|
|
if (null != toReturn &&
|
|
toReturn.Credentials == null)
|
|
{
|
|
toReturn.Credentials = CredentialCache.DefaultCredentials;
|
|
}
|
|
|
|
return toReturn;
|
|
}
|
|
set
|
|
{
|
|
s_defaultWebProxy = value;
|
|
}
|
|
}
|
|
|
|
internal const String CredentialsType = nameof(CredentialsType);
|
|
|
|
private const Int32 m_maxAuthRetries = 3;
|
|
private HttpMessageInvoker m_messageInvoker;
|
|
private CredentialWrapper m_credentialWrapper;
|
|
private bool m_appliedClientCertificatesToTransportHandler;
|
|
private bool m_appliedServerCertificateValidationCallbackToTransportHandler;
|
|
private readonly HttpMessageHandler m_transportHandler;
|
|
|
|
//.Net Core does not attempt NTLM schema on Linux, unless ICredentials is a CredentialCache instance
|
|
//This workaround may not be needed after this corefx fix is consumed: https://github.com/dotnet/corefx/pull/7923
|
|
private sealed class CredentialWrapper : CredentialCache, ICredentials
|
|
{
|
|
public ICredentials InnerCredentials
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
NetworkCredential ICredentials.GetCredential(
|
|
Uri uri,
|
|
String authType)
|
|
{
|
|
return InnerCredentials != null ? InnerCredentials.GetCredential(uri, authType) : null;
|
|
}
|
|
}
|
|
|
|
private sealed class WebProxyWrapper : IWebProxy
|
|
{
|
|
private WebProxyWrapper(IWebProxy toWrap)
|
|
{
|
|
m_wrapped = toWrap;
|
|
m_credentials = null;
|
|
}
|
|
|
|
public static WebProxyWrapper Wrap(IWebProxy toWrap)
|
|
{
|
|
if (null == toWrap)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new WebProxyWrapper(toWrap);
|
|
}
|
|
|
|
public ICredentials Credentials
|
|
{
|
|
get
|
|
{
|
|
ICredentials credentials = m_credentials;
|
|
|
|
if (null == credentials)
|
|
{
|
|
// This means to fall back to the Credentials from the wrapped
|
|
// IWebProxy.
|
|
credentials = m_wrapped.Credentials;
|
|
}
|
|
else if (Object.ReferenceEquals(credentials, m_nullCredentials))
|
|
{
|
|
// This sentinel value means we have explicitly had our credentials
|
|
// set to null.
|
|
credentials = null;
|
|
}
|
|
|
|
return credentials;
|
|
}
|
|
|
|
set
|
|
{
|
|
if (null == value)
|
|
{
|
|
// Use this as a sentinel value to distinguish the case when someone has
|
|
// explicitly set our credentials to null. We don't want to fall back to
|
|
// m_wrapped.Credentials when we have credentials that are explicitly null.
|
|
m_credentials = m_nullCredentials;
|
|
}
|
|
else
|
|
{
|
|
m_credentials = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
public Uri GetProxy(Uri destination)
|
|
{
|
|
return m_wrapped.GetProxy(destination);
|
|
}
|
|
|
|
public bool IsBypassed(Uri host)
|
|
{
|
|
return m_wrapped.IsBypassed(host);
|
|
}
|
|
|
|
private readonly IWebProxy m_wrapped;
|
|
private ICredentials m_credentials;
|
|
|
|
private static readonly ICredentials m_nullCredentials = new CredentialWrapper();
|
|
}
|
|
}
|
|
}
|