mirror of
https://github.com/actions/runner.git
synced 2025-12-25 10:57:32 +08:00
changes to support specific run service URL (#2158)
* changes to support run service url * feedback
This commit is contained in:
committed by
GitHub
parent
b6a46f2114
commit
252f4de577
@@ -0,0 +1,20 @@
|
||||
using GitHub.Services.OAuth;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public static class VssCredentialsExtension
|
||||
{
|
||||
public static VssOAuthCredential ToOAuthCredentials(
|
||||
this VssCredentials credentials)
|
||||
{
|
||||
if (credentials.Federated.CredentialType == VssCredentialsType.OAuth)
|
||||
{
|
||||
return credentials.Federated as VssOAuthCredential;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
194
src/Sdk/Common/Common/RawClientHttpRequestSettings.cs
Normal file
194
src/Sdk/Common/Common/RawClientHttpRequestSettings.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using GitHub.Services.WebApi.Utilities.Internal;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public class RawClientHttpRequestSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Timespan to wait before timing out a request. Defaults to 100 seconds
|
||||
/// </summary>
|
||||
public TimeSpan SendTimeout
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// User-Agent header passed along in the request,
|
||||
/// For multiple values, the order in the list is the order
|
||||
/// in which they will appear in the header
|
||||
/// </summary>
|
||||
public List<ProductInfoHeaderValue> UserAgent
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the culture is passed in the Accept-Language header
|
||||
/// </summary>
|
||||
public ICollection<CultureInfo> AcceptLanguages
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_acceptLanguages;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A unique identifier for the user session
|
||||
/// </summary>
|
||||
public Guid SessionId
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optional implementation used to validate server certificate validation
|
||||
/// </summary>
|
||||
public Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> ServerCertificateValidationCallback
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Number of times to retry a request that has an ambient failure
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property is only used by RawConnection, so only relevant on the client
|
||||
/// </remarks>
|
||||
[DefaultValue(c_defaultMaxRetry)]
|
||||
public Int32 MaxRetryRequest
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property name used to reference this object.
|
||||
/// </summary>
|
||||
public const String PropertyName = "Actions.RequestSettings";
|
||||
|
||||
public static RawClientHttpRequestSettings Default => s_defaultSettings.Value;
|
||||
|
||||
protected RawClientHttpRequestSettings(RawClientHttpRequestSettings copy)
|
||||
{
|
||||
this.SendTimeout = copy.SendTimeout;
|
||||
this.m_acceptLanguages = new List<CultureInfo>(copy.AcceptLanguages);
|
||||
this.SessionId = copy.SessionId;
|
||||
this.UserAgent = new List<ProductInfoHeaderValue>(copy.UserAgent);
|
||||
this.ServerCertificateValidationCallback = copy.ServerCertificateValidationCallback;
|
||||
this.MaxRetryRequest = copy.MaxRetryRequest;
|
||||
}
|
||||
|
||||
public RawClientHttpRequestSettings Clone()
|
||||
{
|
||||
return new RawClientHttpRequestSettings(this);
|
||||
}
|
||||
|
||||
public RawClientHttpRequestSettings()
|
||||
: this(Guid.NewGuid())
|
||||
{
|
||||
}
|
||||
|
||||
public RawClientHttpRequestSettings(Guid sessionId)
|
||||
{
|
||||
this.SendTimeout = s_defaultTimeout;
|
||||
if (!String.IsNullOrEmpty(CultureInfo.CurrentUICulture.Name)) // InvariantCulture for example has an empty name.
|
||||
{
|
||||
this.AcceptLanguages.Add(CultureInfo.CurrentUICulture);
|
||||
}
|
||||
this.SessionId = sessionId;
|
||||
this.ServerCertificateValidationCallback = null;
|
||||
|
||||
// If different, we'll also add CurrentCulture to the request headers,
|
||||
// but UICulture was added first, so it gets first preference
|
||||
if (!CultureInfo.CurrentCulture.Equals(CultureInfo.CurrentUICulture) && !String.IsNullOrEmpty(CultureInfo.CurrentCulture.Name))
|
||||
{
|
||||
this.AcceptLanguages.Add(CultureInfo.CurrentCulture);
|
||||
}
|
||||
|
||||
this.MaxRetryRequest = c_defaultMaxRetry;
|
||||
|
||||
#if DEBUG
|
||||
string customClientRequestTimeout = Environment.GetEnvironmentVariable("VSS_Client_Request_Timeout");
|
||||
if (!string.IsNullOrEmpty(customClientRequestTimeout) && int.TryParse(customClientRequestTimeout, out int customTimeout))
|
||||
{
|
||||
// avoid disrupting a debug session due to the request timing out by setting a custom timeout.
|
||||
this.SendTimeout = TimeSpan.FromSeconds(customTimeout);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
protected internal virtual Boolean ApplyTo(HttpRequestMessage request)
|
||||
{
|
||||
// Make sure we only apply the settings to the request once
|
||||
if (request.Options.TryGetValue<object>(PropertyName, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
request.Options.Set(new HttpRequestOptionsKey<RawClientHttpRequestSettings>(PropertyName), this);
|
||||
|
||||
if (this.AcceptLanguages != null && this.AcceptLanguages.Count > 0)
|
||||
{
|
||||
// An empty or null CultureInfo name will cause an ArgumentNullException in the
|
||||
// StringWithQualityHeaderValue constructor. CultureInfo.InvariantCulture is an example of
|
||||
// a CultureInfo that has an empty name.
|
||||
foreach (CultureInfo culture in this.AcceptLanguages.Where(a => !String.IsNullOrEmpty(a.Name)))
|
||||
{
|
||||
request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(culture.Name));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.UserAgent != null)
|
||||
{
|
||||
foreach (var headerVal in this.UserAgent)
|
||||
{
|
||||
if (!request.Headers.UserAgent.Contains(headerVal))
|
||||
{
|
||||
request.Headers.UserAgent.Add(headerVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!request.Headers.Contains(Internal.RawHttpHeaders.SessionHeader))
|
||||
{
|
||||
request.Headers.Add(Internal.RawHttpHeaders.SessionHeader, this.SessionId.ToString("D"));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the default request settings.
|
||||
/// </summary>
|
||||
/// <returns>The default request settings</returns>
|
||||
private static RawClientHttpRequestSettings ConstructDefaultSettings()
|
||||
{
|
||||
// Set up reasonable defaults in case the registry keys are not present
|
||||
var settings = new RawClientHttpRequestSettings();
|
||||
settings.UserAgent = UserAgentUtility.GetDefaultRestUserAgent();
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
private static Lazy<RawClientHttpRequestSettings> s_defaultSettings
|
||||
= new Lazy<RawClientHttpRequestSettings>(ConstructDefaultSettings);
|
||||
|
||||
private const Int32 c_defaultMaxRetry = 3;
|
||||
private static readonly TimeSpan s_defaultTimeout = TimeSpan.FromSeconds(100); //default WebAPI timeout
|
||||
private ICollection<CultureInfo> m_acceptLanguages = new List<CultureInfo>();
|
||||
}
|
||||
}
|
||||
12
src/Sdk/Common/Common/RawHttpHeaders.cs
Normal file
12
src/Sdk/Common/Common/RawHttpHeaders.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace GitHub.Services.Common.Internal
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class RawHttpHeaders
|
||||
{
|
||||
public const String SessionHeader = "X-Runner-Session";
|
||||
}
|
||||
}
|
||||
349
src/Sdk/Common/Common/RawHttpMessageHandler.cs
Normal file
349
src/Sdk/Common/Common/RawHttpMessageHandler.cs
Normal file
@@ -0,0 +1,349 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Services.Common.Diagnostics;
|
||||
using GitHub.Services.Common.Internal;
|
||||
using GitHub.Services.OAuth;
|
||||
|
||||
namespace GitHub.Services.Common
|
||||
{
|
||||
public class RawHttpMessageHandler: HttpMessageHandler
|
||||
{
|
||||
public RawHttpMessageHandler(
|
||||
VssOAuthCredential credentials)
|
||||
: this(credentials, new RawClientHttpRequestSettings())
|
||||
{
|
||||
}
|
||||
|
||||
public RawHttpMessageHandler(
|
||||
VssOAuthCredential credentials,
|
||||
RawClientHttpRequestSettings settings)
|
||||
: this(credentials, settings, new HttpClientHandler())
|
||||
{
|
||||
}
|
||||
|
||||
public RawHttpMessageHandler(
|
||||
VssOAuthCredential credentials,
|
||||
RawClientHttpRequestSettings settings,
|
||||
HttpMessageHandler innerHandler)
|
||||
{
|
||||
this.Credentials = credentials;
|
||||
this.Settings = settings;
|
||||
m_messageInvoker = new HttpMessageInvoker(innerHandler);
|
||||
m_credentialWrapper = new CredentialWrapper();
|
||||
|
||||
// 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);
|
||||
|
||||
m_thisLock = new Object();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the credentials associated with this handler.
|
||||
/// </summary>
|
||||
public VssOAuthCredential Credentials
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the settings associated with this handler.
|
||||
/// </summary>
|
||||
public RawClientHttpRequestSettings Settings
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
// 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 RawHttpMessageHandler 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;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
VssTraceActivity traceActivity = VssTraceActivity.Current;
|
||||
|
||||
lock (m_thisLock)
|
||||
{
|
||||
// Ensure that we attempt to use the most appropriate authentication mechanism by default.
|
||||
if (m_tokenProvider == null)
|
||||
{
|
||||
m_tokenProvider = this.Credentials.GetTokenProvider(request.RequestUri);
|
||||
}
|
||||
}
|
||||
|
||||
CancellationTokenSource tokenSource = null;
|
||||
HttpResponseMessage response = null;
|
||||
Boolean succeeded = false;
|
||||
HttpResponseMessageWrapper responseWrapper;
|
||||
|
||||
Int32 retries = m_maxAuthRetries;
|
||||
try
|
||||
{
|
||||
tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
if (this.Settings.SendTimeout > TimeSpan.Zero)
|
||||
{
|
||||
tokenSource.CancelAfter(this.Settings.SendTimeout);
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
if (response != null)
|
||||
{
|
||||
response.Dispose();
|
||||
}
|
||||
|
||||
// Let's start with sending a token
|
||||
IssuedToken token = await m_tokenProvider.GetTokenAsync(null, tokenSource.Token).ConfigureAwait(false);
|
||||
ApplyToken(request, token);
|
||||
|
||||
// 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);
|
||||
|
||||
responseWrapper = new HttpResponseMessageWrapper(response);
|
||||
|
||||
var isUnAuthorized = responseWrapper.StatusCode == HttpStatusCode.Unauthorized;
|
||||
if (!isUnAuthorized)
|
||||
{
|
||||
// Validate the token after it has been successfully authenticated with the server.
|
||||
m_tokenProvider?.ValidateToken(token, responseWrapper);
|
||||
succeeded = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_tokenProvider?.InvalidateToken(token);
|
||||
|
||||
if (retries == 0 || retries < m_maxAuthRetries)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
token = await m_tokenProvider.GetTokenAsync(token, tokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
retries--;
|
||||
}
|
||||
}
|
||||
while (retries >= 0);
|
||||
|
||||
// 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 && responseWrapper.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
// Make sure we do not leak the response object when raising an exception
|
||||
if (response != null)
|
||||
{
|
||||
response.Dispose();
|
||||
}
|
||||
|
||||
var message = CommonResources.VssUnauthorized(request.RequestUri.GetLeftPart(UriPartial.Authority));
|
||||
VssHttpEventSource.Log.HttpRequestUnauthorized(traceActivity, request, message);
|
||||
VssUnauthorizedException unauthorizedException = new VssUnauthorizedException(message);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyToken(
|
||||
HttpRequestMessage request,
|
||||
IssuedToken token)
|
||||
{
|
||||
switch (token)
|
||||
{
|
||||
case null:
|
||||
return;
|
||||
case ICredentials credentialsToken:
|
||||
m_credentialWrapper.InnerCredentials = credentialsToken;
|
||||
break;
|
||||
default:
|
||||
token.ApplyTo(new HttpRequestMessageWrapper(request));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplySettings(
|
||||
HttpMessageHandler handler,
|
||||
ICredentials defaultCredentials,
|
||||
RawClientHttpRequestSettings settings)
|
||||
{
|
||||
HttpClientHandler httpClientHandler = handler as HttpClientHandler;
|
||||
if (httpClientHandler != null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly HttpMessageHandler m_transportHandler;
|
||||
private HttpMessageInvoker m_messageInvoker;
|
||||
private CredentialWrapper m_credentialWrapper;
|
||||
private object m_thisLock;
|
||||
private const Int32 m_maxAuthRetries = 3;
|
||||
private VssOAuthTokenProvider m_tokenProvider;
|
||||
|
||||
//.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user