mirror of
https://github.com/actions/runner.git
synced 2025-12-10 20:36:49 +00:00
delete un-used code. (#218)
This commit is contained in:
@@ -1,33 +0,0 @@
|
|||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Common
|
|
||||||
{
|
|
||||||
//Stephen Toub: http://blogs.msdn.com/b/pfxteam/archive/2012/02/11/10266920.aspx
|
|
||||||
|
|
||||||
public class AsyncManualResetEvent
|
|
||||||
{
|
|
||||||
private volatile TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>();
|
|
||||||
|
|
||||||
public Task WaitAsync() { return m_tcs.Task; }
|
|
||||||
|
|
||||||
public void Set()
|
|
||||||
{
|
|
||||||
var tcs = m_tcs;
|
|
||||||
Task.Factory.StartNew(s => ((TaskCompletionSource<bool>)s).TrySetResult(true),
|
|
||||||
tcs, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default);
|
|
||||||
tcs.Task.Wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var tcs = m_tcs;
|
|
||||||
if (!tcs.Task.IsCompleted ||
|
|
||||||
Interlocked.CompareExchange(ref m_tcs, new TaskCompletionSource<bool>(), tcs) == tcs)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -71,15 +71,6 @@ namespace GitHub.Runner.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataContract]
|
|
||||||
public sealed class RunnerRuntimeOptions
|
|
||||||
{
|
|
||||||
#if OS_WINDOWS
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public bool GitUseSecureChannel { get; set; }
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
[ServiceLocator(Default = typeof(ConfigurationStore))]
|
[ServiceLocator(Default = typeof(ConfigurationStore))]
|
||||||
public interface IConfigurationStore : IRunnerService
|
public interface IConfigurationStore : IRunnerService
|
||||||
{
|
{
|
||||||
@@ -92,9 +83,6 @@ namespace GitHub.Runner.Common
|
|||||||
void SaveSettings(RunnerSettings settings);
|
void SaveSettings(RunnerSettings settings);
|
||||||
void DeleteCredential();
|
void DeleteCredential();
|
||||||
void DeleteSettings();
|
void DeleteSettings();
|
||||||
RunnerRuntimeOptions GetRunnerRuntimeOptions();
|
|
||||||
void SaveRunnerRuntimeOptions(RunnerRuntimeOptions options);
|
|
||||||
void DeleteRunnerRuntimeOptions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ConfigurationStore : RunnerService, IConfigurationStore
|
public sealed class ConfigurationStore : RunnerService, IConfigurationStore
|
||||||
@@ -103,11 +91,9 @@ namespace GitHub.Runner.Common
|
|||||||
private string _configFilePath;
|
private string _configFilePath;
|
||||||
private string _credFilePath;
|
private string _credFilePath;
|
||||||
private string _serviceConfigFilePath;
|
private string _serviceConfigFilePath;
|
||||||
private string _runtimeOptionsFilePath;
|
|
||||||
|
|
||||||
private CredentialData _creds;
|
private CredentialData _creds;
|
||||||
private RunnerSettings _settings;
|
private RunnerSettings _settings;
|
||||||
private RunnerRuntimeOptions _runtimeOptions;
|
|
||||||
|
|
||||||
public override void Initialize(IHostContext hostContext)
|
public override void Initialize(IHostContext hostContext)
|
||||||
{
|
{
|
||||||
@@ -130,16 +116,12 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
_serviceConfigFilePath = hostContext.GetConfigFile(WellKnownConfigFile.Service);
|
_serviceConfigFilePath = hostContext.GetConfigFile(WellKnownConfigFile.Service);
|
||||||
Trace.Info("ServiceConfigFilePath: {0}", _serviceConfigFilePath);
|
Trace.Info("ServiceConfigFilePath: {0}", _serviceConfigFilePath);
|
||||||
|
|
||||||
_runtimeOptionsFilePath = hostContext.GetConfigFile(WellKnownConfigFile.Options);
|
|
||||||
Trace.Info("RuntimeOptionsFilePath: {0}", _runtimeOptionsFilePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string RootFolder { get; private set; }
|
public string RootFolder { get; private set; }
|
||||||
|
|
||||||
public bool HasCredentials()
|
public bool HasCredentials()
|
||||||
{
|
{
|
||||||
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
|
|
||||||
Trace.Info("HasCredentials()");
|
Trace.Info("HasCredentials()");
|
||||||
bool credsStored = (new FileInfo(_credFilePath)).Exists;
|
bool credsStored = (new FileInfo(_credFilePath)).Exists;
|
||||||
Trace.Info("stored {0}", credsStored);
|
Trace.Info("stored {0}", credsStored);
|
||||||
@@ -149,14 +131,13 @@ namespace GitHub.Runner.Common
|
|||||||
public bool IsConfigured()
|
public bool IsConfigured()
|
||||||
{
|
{
|
||||||
Trace.Info("IsConfigured()");
|
Trace.Info("IsConfigured()");
|
||||||
bool configured = HostContext.RunMode == RunMode.Local || (new FileInfo(_configFilePath)).Exists;
|
bool configured = new FileInfo(_configFilePath).Exists;
|
||||||
Trace.Info("IsConfigured: {0}", configured);
|
Trace.Info("IsConfigured: {0}", configured);
|
||||||
return configured;
|
return configured;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsServiceConfigured()
|
public bool IsServiceConfigured()
|
||||||
{
|
{
|
||||||
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
|
|
||||||
Trace.Info("IsServiceConfigured()");
|
Trace.Info("IsServiceConfigured()");
|
||||||
bool serviceConfigured = (new FileInfo(_serviceConfigFilePath)).Exists;
|
bool serviceConfigured = (new FileInfo(_serviceConfigFilePath)).Exists;
|
||||||
Trace.Info($"IsServiceConfigured: {serviceConfigured}");
|
Trace.Info($"IsServiceConfigured: {serviceConfigured}");
|
||||||
@@ -165,7 +146,6 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
public CredentialData GetCredentials()
|
public CredentialData GetCredentials()
|
||||||
{
|
{
|
||||||
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
|
|
||||||
if (_creds == null)
|
if (_creds == null)
|
||||||
{
|
{
|
||||||
_creds = IOUtil.LoadObject<CredentialData>(_credFilePath);
|
_creds = IOUtil.LoadObject<CredentialData>(_credFilePath);
|
||||||
@@ -195,7 +175,6 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
public void SaveCredential(CredentialData credential)
|
public void SaveCredential(CredentialData credential)
|
||||||
{
|
{
|
||||||
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
|
|
||||||
Trace.Info("Saving {0} credential @ {1}", credential.Scheme, _credFilePath);
|
Trace.Info("Saving {0} credential @ {1}", credential.Scheme, _credFilePath);
|
||||||
if (File.Exists(_credFilePath))
|
if (File.Exists(_credFilePath))
|
||||||
{
|
{
|
||||||
@@ -211,7 +190,6 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
public void SaveSettings(RunnerSettings settings)
|
public void SaveSettings(RunnerSettings settings)
|
||||||
{
|
{
|
||||||
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
|
|
||||||
Trace.Info("Saving runner settings.");
|
Trace.Info("Saving runner settings.");
|
||||||
if (File.Exists(_configFilePath))
|
if (File.Exists(_configFilePath))
|
||||||
{
|
{
|
||||||
@@ -227,44 +205,12 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
public void DeleteCredential()
|
public void DeleteCredential()
|
||||||
{
|
{
|
||||||
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
|
|
||||||
IOUtil.Delete(_credFilePath, default(CancellationToken));
|
IOUtil.Delete(_credFilePath, default(CancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteSettings()
|
public void DeleteSettings()
|
||||||
{
|
{
|
||||||
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
|
|
||||||
IOUtil.Delete(_configFilePath, default(CancellationToken));
|
IOUtil.Delete(_configFilePath, default(CancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
public RunnerRuntimeOptions GetRunnerRuntimeOptions()
|
|
||||||
{
|
|
||||||
if (_runtimeOptions == null && File.Exists(_runtimeOptionsFilePath))
|
|
||||||
{
|
|
||||||
_runtimeOptions = IOUtil.LoadObject<RunnerRuntimeOptions>(_runtimeOptionsFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _runtimeOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SaveRunnerRuntimeOptions(RunnerRuntimeOptions options)
|
|
||||||
{
|
|
||||||
Trace.Info("Saving runtime options.");
|
|
||||||
if (File.Exists(_runtimeOptionsFilePath))
|
|
||||||
{
|
|
||||||
// Delete existing runtime options file first, since the file is hidden and not able to overwrite.
|
|
||||||
Trace.Info("Delete exist runtime options file.");
|
|
||||||
IOUtil.DeleteFile(_runtimeOptionsFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
IOUtil.SaveObject(options, _runtimeOptionsFilePath);
|
|
||||||
Trace.Info("Options Saved.");
|
|
||||||
File.SetAttributes(_runtimeOptionsFilePath, File.GetAttributes(_runtimeOptionsFilePath) | FileAttributes.Hidden);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DeleteRunnerRuntimeOptions()
|
|
||||||
{
|
|
||||||
IOUtil.Delete(_runtimeOptionsFilePath, default(CancellationToken));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,6 @@
|
|||||||
|
|
||||||
namespace GitHub.Runner.Common
|
namespace GitHub.Runner.Common
|
||||||
{
|
{
|
||||||
public enum RunMode
|
|
||||||
{
|
|
||||||
Normal, // Keep "Normal" first (default value).
|
|
||||||
Local,
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum WellKnownDirectory
|
public enum WellKnownDirectory
|
||||||
{
|
{
|
||||||
Bin,
|
Bin,
|
||||||
@@ -94,10 +88,6 @@ namespace GitHub.Runner.Common
|
|||||||
public static readonly string MonitorSocketAddress = "monitorsocketaddress";
|
public static readonly string MonitorSocketAddress = "monitorsocketaddress";
|
||||||
public static readonly string Name = "name";
|
public static readonly string Name = "name";
|
||||||
public static readonly string Pool = "pool";
|
public static readonly string Pool = "pool";
|
||||||
public static readonly string SslCACert = "sslcacert";
|
|
||||||
public static readonly string SslClientCert = "sslclientcert";
|
|
||||||
public static readonly string SslClientCertKey = "sslclientcertkey";
|
|
||||||
public static readonly string SslClientCertArchive = "sslclientcertarchive";
|
|
||||||
public static readonly string StartupType = "startuptype";
|
public static readonly string StartupType = "startuptype";
|
||||||
public static readonly string Url = "url";
|
public static readonly string Url = "url";
|
||||||
public static readonly string UserName = "username";
|
public static readonly string UserName = "username";
|
||||||
@@ -105,14 +95,10 @@ namespace GitHub.Runner.Common
|
|||||||
public static readonly string Work = "work";
|
public static readonly string Work = "work";
|
||||||
|
|
||||||
// Secret args. Must be added to the "Secrets" getter as well.
|
// Secret args. Must be added to the "Secrets" getter as well.
|
||||||
public static readonly string Password = "password";
|
|
||||||
public static readonly string SslClientCertPassword = "sslclientcertpassword";
|
|
||||||
public static readonly string Token = "token";
|
public static readonly string Token = "token";
|
||||||
public static readonly string WindowsLogonPassword = "windowslogonpassword";
|
public static readonly string WindowsLogonPassword = "windowslogonpassword";
|
||||||
public static string[] Secrets => new[]
|
public static string[] Secrets => new[]
|
||||||
{
|
{
|
||||||
Password,
|
|
||||||
SslClientCertPassword,
|
|
||||||
Token,
|
Token,
|
||||||
WindowsLogonPassword,
|
WindowsLogonPassword,
|
||||||
};
|
};
|
||||||
@@ -131,13 +117,10 @@ namespace GitHub.Runner.Common
|
|||||||
public static class Flags
|
public static class Flags
|
||||||
{
|
{
|
||||||
public static readonly string Commit = "commit";
|
public static readonly string Commit = "commit";
|
||||||
public static readonly string GitUseSChannel = "gituseschannel";
|
|
||||||
public static readonly string Help = "help";
|
public static readonly string Help = "help";
|
||||||
public static readonly string Replace = "replace";
|
public static readonly string Replace = "replace";
|
||||||
public static readonly string LaunchBrowser = "launchbrowser";
|
|
||||||
public static readonly string Once = "once";
|
public static readonly string Once = "once";
|
||||||
public static readonly string RunAsService = "runasservice";
|
public static readonly string RunAsService = "runasservice";
|
||||||
public static readonly string SslSkipCertValidation = "sslskipcertvalidation";
|
|
||||||
public static readonly string Unattended = "unattended";
|
public static readonly string Unattended = "unattended";
|
||||||
public static readonly string Version = "version";
|
public static readonly string Version = "version";
|
||||||
}
|
}
|
||||||
@@ -164,9 +147,7 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
public static class Configuration
|
public static class Configuration
|
||||||
{
|
{
|
||||||
public static readonly string AAD = "AAD";
|
|
||||||
public static readonly string OAuthAccessToken = "OAuthAccessToken";
|
public static readonly string OAuthAccessToken = "OAuthAccessToken";
|
||||||
public static readonly string PAT = "PAT";
|
|
||||||
public static readonly string OAuth = "OAuth";
|
public static readonly string OAuth = "OAuth";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ namespace GitHub.Runner.Common
|
|||||||
{
|
{
|
||||||
public interface IHostContext : IDisposable
|
public interface IHostContext : IDisposable
|
||||||
{
|
{
|
||||||
RunMode RunMode { get; set; }
|
|
||||||
StartupType StartupType { get; set; }
|
StartupType StartupType { get; set; }
|
||||||
CancellationToken RunnerShutdownToken { get; }
|
CancellationToken RunnerShutdownToken { get; }
|
||||||
ShutdownReason RunnerShutdownReason { get; }
|
ShutdownReason RunnerShutdownReason { get; }
|
||||||
@@ -58,7 +57,6 @@ namespace GitHub.Runner.Common
|
|||||||
private readonly ProductInfoHeaderValue _userAgent = new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version);
|
private readonly ProductInfoHeaderValue _userAgent = new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version);
|
||||||
private CancellationTokenSource _runnerShutdownTokenSource = new CancellationTokenSource();
|
private CancellationTokenSource _runnerShutdownTokenSource = new CancellationTokenSource();
|
||||||
private object _perfLock = new object();
|
private object _perfLock = new object();
|
||||||
private RunMode _runMode = RunMode.Normal;
|
|
||||||
private Tracing _trace;
|
private Tracing _trace;
|
||||||
private Tracing _vssTrace;
|
private Tracing _vssTrace;
|
||||||
private Tracing _httpTrace;
|
private Tracing _httpTrace;
|
||||||
@@ -194,20 +192,6 @@ namespace GitHub.Runner.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public RunMode RunMode
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return _runMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_trace.Info($"Set run mode: {value}");
|
|
||||||
_runMode = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetDirectory(WellKnownDirectory directory)
|
public string GetDirectory(WellKnownDirectory directory)
|
||||||
{
|
{
|
||||||
string path;
|
string path;
|
||||||
|
|||||||
@@ -32,11 +32,6 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
public async Task ConnectAsync(VssConnection jobConnection)
|
public async Task ConnectAsync(VssConnection jobConnection)
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_connection = jobConnection;
|
_connection = jobConnection;
|
||||||
int attemptCount = 5;
|
int attemptCount = 5;
|
||||||
while (!_connection.HasAuthenticated && attemptCount-- > 0)
|
while (!_connection.HasAuthenticated && attemptCount-- > 0)
|
||||||
@@ -73,88 +68,48 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
public Task<TaskLog> AppendLogContentAsync(Guid scopeIdentifier, string hubName, Guid planId, int logId, Stream uploadStream, CancellationToken cancellationToken)
|
public Task<TaskLog> AppendLogContentAsync(Guid scopeIdentifier, string hubName, Guid planId, int logId, Stream uploadStream, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return Task.FromResult<TaskLog>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return _taskClient.AppendLogContentAsync(scopeIdentifier, hubName, planId, logId, uploadStream, cancellationToken: cancellationToken);
|
return _taskClient.AppendLogContentAsync(scopeIdentifier, hubName, planId, logId, uploadStream, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task AppendTimelineRecordFeedAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, Guid stepId, IList<string> lines, CancellationToken cancellationToken)
|
public Task AppendTimelineRecordFeedAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, Guid stepId, IList<string> lines, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return _taskClient.AppendTimelineRecordFeedAsync(scopeIdentifier, hubName, planId, timelineId, timelineRecordId, stepId, lines, cancellationToken: cancellationToken);
|
return _taskClient.AppendTimelineRecordFeedAsync(scopeIdentifier, hubName, planId, timelineId, timelineRecordId, stepId, lines, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<TaskAttachment> CreateAttachmentAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, string type, string name, Stream uploadStream, CancellationToken cancellationToken)
|
public Task<TaskAttachment> CreateAttachmentAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, string type, string name, Stream uploadStream, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return Task.FromResult<TaskAttachment>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return _taskClient.CreateAttachmentAsync(scopeIdentifier, hubName, planId, timelineId, timelineRecordId, type, name, uploadStream, cancellationToken: cancellationToken);
|
return _taskClient.CreateAttachmentAsync(scopeIdentifier, hubName, planId, timelineId, timelineRecordId, type, name, uploadStream, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<TaskLog> CreateLogAsync(Guid scopeIdentifier, string hubName, Guid planId, TaskLog log, CancellationToken cancellationToken)
|
public Task<TaskLog> CreateLogAsync(Guid scopeIdentifier, string hubName, Guid planId, TaskLog log, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return Task.FromResult<TaskLog>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return _taskClient.CreateLogAsync(scopeIdentifier, hubName, planId, log, cancellationToken: cancellationToken);
|
return _taskClient.CreateLogAsync(scopeIdentifier, hubName, planId, log, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Timeline> CreateTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken)
|
public Task<Timeline> CreateTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return Task.FromResult<Timeline>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return _taskClient.CreateTimelineAsync(scopeIdentifier, hubName, planId, new Timeline(timelineId), cancellationToken: cancellationToken);
|
return _taskClient.CreateTimelineAsync(scopeIdentifier, hubName, planId, new Timeline(timelineId), cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<TimelineRecord>> UpdateTimelineRecordsAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, IEnumerable<TimelineRecord> records, CancellationToken cancellationToken)
|
public Task<List<TimelineRecord>> UpdateTimelineRecordsAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, IEnumerable<TimelineRecord> records, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return Task.FromResult<List<TimelineRecord>>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return _taskClient.UpdateTimelineRecordsAsync(scopeIdentifier, hubName, planId, timelineId, records, cancellationToken: cancellationToken);
|
return _taskClient.UpdateTimelineRecordsAsync(scopeIdentifier, hubName, planId, timelineId, records, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task RaisePlanEventAsync<T>(Guid scopeIdentifier, string hubName, Guid planId, T eventData, CancellationToken cancellationToken) where T : JobEvent
|
public Task RaisePlanEventAsync<T>(Guid scopeIdentifier, string hubName, Guid planId, T eventData, CancellationToken cancellationToken) where T : JobEvent
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return _taskClient.RaisePlanEventAsync(scopeIdentifier, hubName, planId, eventData, cancellationToken: cancellationToken);
|
return _taskClient.RaisePlanEventAsync(scopeIdentifier, hubName, planId, eventData, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Timeline> GetTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken)
|
public Task<Timeline> GetTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return Task.FromResult<Timeline>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return _taskClient.GetTimelineAsync(scopeIdentifier, hubName, planId, timelineId, includeRecords: true, cancellationToken: cancellationToken);
|
return _taskClient.GetTimelineAsync(scopeIdentifier, hubName, planId, timelineId, includeRecords: true, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ namespace GitHub.Runner.Common
|
|||||||
private Task[] _allDequeueTasks;
|
private Task[] _allDequeueTasks;
|
||||||
private readonly TaskCompletionSource<int> _jobCompletionSource = new TaskCompletionSource<int>();
|
private readonly TaskCompletionSource<int> _jobCompletionSource = new TaskCompletionSource<int>();
|
||||||
private bool _queueInProcess = false;
|
private bool _queueInProcess = false;
|
||||||
private ITerminal _term;
|
|
||||||
|
|
||||||
public event EventHandler<ThrottlingEventArgs> JobServerQueueThrottling;
|
public event EventHandler<ThrottlingEventArgs> JobServerQueueThrottling;
|
||||||
|
|
||||||
@@ -85,11 +84,6 @@ namespace GitHub.Runner.Common
|
|||||||
public void Start(Pipelines.AgentJobRequestMessage jobRequest)
|
public void Start(Pipelines.AgentJobRequestMessage jobRequest)
|
||||||
{
|
{
|
||||||
Trace.Entering();
|
Trace.Entering();
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
_term = HostContext.GetService<ITerminal>();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_queueInProcess)
|
if (_queueInProcess)
|
||||||
{
|
{
|
||||||
@@ -129,11 +123,6 @@ namespace GitHub.Runner.Common
|
|||||||
// TimelineUpdate queue error will become critical when timeline records contain output variabls.
|
// TimelineUpdate queue error will become critical when timeline records contain output variabls.
|
||||||
public async Task ShutdownAsync()
|
public async Task ShutdownAsync()
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_queueInProcess)
|
if (!_queueInProcess)
|
||||||
{
|
{
|
||||||
Trace.Info("No-op, all queue process tasks have been stopped.");
|
Trace.Info("No-op, all queue process tasks have been stopped.");
|
||||||
@@ -169,32 +158,11 @@ namespace GitHub.Runner.Common
|
|||||||
public void QueueWebConsoleLine(Guid stepRecordId, string line)
|
public void QueueWebConsoleLine(Guid stepRecordId, string line)
|
||||||
{
|
{
|
||||||
Trace.Verbose("Enqueue web console line queue: {0}", line);
|
Trace.Verbose("Enqueue web console line queue: {0}", line);
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
if ((line ?? string.Empty).StartsWith("##[section]"))
|
|
||||||
{
|
|
||||||
Console.WriteLine("******************************************************************************");
|
|
||||||
Console.WriteLine(line.Substring("##[section]".Length));
|
|
||||||
Console.WriteLine("******************************************************************************");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_webConsoleLineQueue.Enqueue(new ConsoleLineInfo(stepRecordId, line));
|
_webConsoleLineQueue.Enqueue(new ConsoleLineInfo(stepRecordId, line));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void QueueFileUpload(Guid timelineId, Guid timelineRecordId, string type, string name, string path, bool deleteSource)
|
public void QueueFileUpload(Guid timelineId, Guid timelineRecordId, string type, string name, string path, bool deleteSource)
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArgUtil.NotEmpty(timelineId, nameof(timelineId));
|
ArgUtil.NotEmpty(timelineId, nameof(timelineId));
|
||||||
ArgUtil.NotEmpty(timelineRecordId, nameof(timelineRecordId));
|
ArgUtil.NotEmpty(timelineRecordId, nameof(timelineRecordId));
|
||||||
|
|
||||||
@@ -215,11 +183,6 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
public void QueueTimelineRecordUpdate(Guid timelineId, TimelineRecord timelineRecord)
|
public void QueueTimelineRecordUpdate(Guid timelineId, TimelineRecord timelineRecord)
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArgUtil.NotEmpty(timelineId, nameof(timelineId));
|
ArgUtil.NotEmpty(timelineId, nameof(timelineId));
|
||||||
ArgUtil.NotNull(timelineRecord, nameof(timelineRecord));
|
ArgUtil.NotNull(timelineRecord, nameof(timelineRecord));
|
||||||
ArgUtil.NotEmpty(timelineRecord.Id, nameof(timelineRecord.Id));
|
ArgUtil.NotEmpty(timelineRecord.Id, nameof(timelineRecord.Id));
|
||||||
|
|||||||
@@ -1,231 +0,0 @@
|
|||||||
using System;
|
|
||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Security;
|
|
||||||
using System.Net.Http;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
using GitHub.Runner.Sdk;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Common
|
|
||||||
{
|
|
||||||
[ServiceLocator(Default = typeof(RunnerCertificateManager))]
|
|
||||||
public interface IRunnerCertificateManager : IRunnerService
|
|
||||||
{
|
|
||||||
bool SkipServerCertificateValidation { get; }
|
|
||||||
string CACertificateFile { get; }
|
|
||||||
string ClientCertificateFile { get; }
|
|
||||||
string ClientCertificatePrivateKeyFile { get; }
|
|
||||||
string ClientCertificateArchiveFile { get; }
|
|
||||||
string ClientCertificatePassword { get; }
|
|
||||||
IVssClientCertificateManager VssClientCertificateManager { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RunnerCertificateManager : RunnerService, IRunnerCertificateManager
|
|
||||||
{
|
|
||||||
private RunnerClientCertificateManager _runnerClientCertificateManager = new RunnerClientCertificateManager();
|
|
||||||
|
|
||||||
public bool SkipServerCertificateValidation { private set; get; }
|
|
||||||
public string CACertificateFile { private set; get; }
|
|
||||||
public string ClientCertificateFile { private set; get; }
|
|
||||||
public string ClientCertificatePrivateKeyFile { private set; get; }
|
|
||||||
public string ClientCertificateArchiveFile { private set; get; }
|
|
||||||
public string ClientCertificatePassword { private set; get; }
|
|
||||||
public IVssClientCertificateManager VssClientCertificateManager => _runnerClientCertificateManager;
|
|
||||||
|
|
||||||
public override void Initialize(IHostContext hostContext)
|
|
||||||
{
|
|
||||||
base.Initialize(hostContext);
|
|
||||||
LoadCertificateSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should only be called from config
|
|
||||||
public void SetupCertificate(bool skipCertValidation, string caCert, string clientCert, string clientCertPrivateKey, string clientCertArchive, string clientCertPassword)
|
|
||||||
{
|
|
||||||
Trace.Info("Setup runner certificate setting base on configuration inputs.");
|
|
||||||
|
|
||||||
if (skipCertValidation)
|
|
||||||
{
|
|
||||||
Trace.Info("Ignore SSL server certificate validation error");
|
|
||||||
SkipServerCertificateValidation = true;
|
|
||||||
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(caCert))
|
|
||||||
{
|
|
||||||
ArgUtil.File(caCert, nameof(caCert));
|
|
||||||
Trace.Info($"Self-Signed CA '{caCert}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(clientCert))
|
|
||||||
{
|
|
||||||
ArgUtil.File(clientCert, nameof(clientCert));
|
|
||||||
ArgUtil.File(clientCertPrivateKey, nameof(clientCertPrivateKey));
|
|
||||||
ArgUtil.File(clientCertArchive, nameof(clientCertArchive));
|
|
||||||
|
|
||||||
Trace.Info($"Client cert '{clientCert}'");
|
|
||||||
Trace.Info($"Client cert private key '{clientCertPrivateKey}'");
|
|
||||||
Trace.Info($"Client cert archive '{clientCertArchive}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
CACertificateFile = caCert;
|
|
||||||
ClientCertificateFile = clientCert;
|
|
||||||
ClientCertificatePrivateKeyFile = clientCertPrivateKey;
|
|
||||||
ClientCertificateArchiveFile = clientCertArchive;
|
|
||||||
ClientCertificatePassword = clientCertPassword;
|
|
||||||
|
|
||||||
_runnerClientCertificateManager.AddClientCertificate(ClientCertificateArchiveFile, ClientCertificatePassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should only be called from config
|
|
||||||
public void SaveCertificateSetting()
|
|
||||||
{
|
|
||||||
string certSettingFile = HostContext.GetConfigFile(WellKnownConfigFile.Certificates);
|
|
||||||
IOUtil.DeleteFile(certSettingFile);
|
|
||||||
|
|
||||||
var setting = new RunnerCertificateSetting();
|
|
||||||
if (SkipServerCertificateValidation)
|
|
||||||
{
|
|
||||||
Trace.Info($"Store Skip ServerCertificateValidation setting to '{certSettingFile}'");
|
|
||||||
setting.SkipServerCertValidation = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(CACertificateFile))
|
|
||||||
{
|
|
||||||
Trace.Info($"Store CA cert setting to '{certSettingFile}'");
|
|
||||||
setting.CACert = CACertificateFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ClientCertificateFile) &&
|
|
||||||
!string.IsNullOrEmpty(ClientCertificatePrivateKeyFile) &&
|
|
||||||
!string.IsNullOrEmpty(ClientCertificateArchiveFile))
|
|
||||||
{
|
|
||||||
Trace.Info($"Store client cert settings to '{certSettingFile}'");
|
|
||||||
|
|
||||||
setting.ClientCert = ClientCertificateFile;
|
|
||||||
setting.ClientCertPrivatekey = ClientCertificatePrivateKeyFile;
|
|
||||||
setting.ClientCertArchive = ClientCertificateArchiveFile;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ClientCertificatePassword))
|
|
||||||
{
|
|
||||||
string lookupKey = Guid.NewGuid().ToString("D").ToUpperInvariant();
|
|
||||||
Trace.Info($"Store client cert private key password with lookup key {lookupKey}");
|
|
||||||
|
|
||||||
var credStore = HostContext.GetService<IRunnerCredentialStore>();
|
|
||||||
credStore.Write($"GITHUB_ACTIONS_RUNNER_CLIENT_CERT_PASSWORD_{lookupKey}", "GitHub", ClientCertificatePassword);
|
|
||||||
|
|
||||||
setting.ClientCertPasswordLookupKey = lookupKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SkipServerCertificateValidation ||
|
|
||||||
!string.IsNullOrEmpty(CACertificateFile) ||
|
|
||||||
!string.IsNullOrEmpty(ClientCertificateFile))
|
|
||||||
{
|
|
||||||
IOUtil.SaveObject(setting, certSettingFile);
|
|
||||||
File.SetAttributes(certSettingFile, File.GetAttributes(certSettingFile) | FileAttributes.Hidden);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should only be called from unconfig
|
|
||||||
public void DeleteCertificateSetting()
|
|
||||||
{
|
|
||||||
string certSettingFile = HostContext.GetConfigFile(WellKnownConfigFile.Certificates);
|
|
||||||
if (File.Exists(certSettingFile))
|
|
||||||
{
|
|
||||||
Trace.Info($"Load runner certificate setting from '{certSettingFile}'");
|
|
||||||
var certSetting = IOUtil.LoadObject<RunnerCertificateSetting>(certSettingFile);
|
|
||||||
|
|
||||||
if (certSetting != null && !string.IsNullOrEmpty(certSetting.ClientCertPasswordLookupKey))
|
|
||||||
{
|
|
||||||
Trace.Info("Delete client cert private key password from credential store.");
|
|
||||||
var credStore = HostContext.GetService<IRunnerCredentialStore>();
|
|
||||||
credStore.Delete($"GITHUB_ACTIONS_RUNNER_CLIENT_CERT_PASSWORD_{certSetting.ClientCertPasswordLookupKey}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Trace.Info($"Delete cert setting file: {certSettingFile}");
|
|
||||||
IOUtil.DeleteFile(certSettingFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadCertificateSettings()
|
|
||||||
{
|
|
||||||
string certSettingFile = HostContext.GetConfigFile(WellKnownConfigFile.Certificates);
|
|
||||||
if (File.Exists(certSettingFile))
|
|
||||||
{
|
|
||||||
Trace.Info($"Load runner certificate setting from '{certSettingFile}'");
|
|
||||||
var certSetting = IOUtil.LoadObject<RunnerCertificateSetting>(certSettingFile);
|
|
||||||
ArgUtil.NotNull(certSetting, nameof(RunnerCertificateSetting));
|
|
||||||
|
|
||||||
if (certSetting.SkipServerCertValidation)
|
|
||||||
{
|
|
||||||
Trace.Info("Ignore SSL server certificate validation error");
|
|
||||||
SkipServerCertificateValidation = true;
|
|
||||||
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(certSetting.CACert))
|
|
||||||
{
|
|
||||||
// make sure all settings file exist
|
|
||||||
ArgUtil.File(certSetting.CACert, nameof(certSetting.CACert));
|
|
||||||
Trace.Info($"CA '{certSetting.CACert}'");
|
|
||||||
CACertificateFile = certSetting.CACert;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(certSetting.ClientCert))
|
|
||||||
{
|
|
||||||
// make sure all settings file exist
|
|
||||||
ArgUtil.File(certSetting.ClientCert, nameof(certSetting.ClientCert));
|
|
||||||
ArgUtil.File(certSetting.ClientCertPrivatekey, nameof(certSetting.ClientCertPrivatekey));
|
|
||||||
ArgUtil.File(certSetting.ClientCertArchive, nameof(certSetting.ClientCertArchive));
|
|
||||||
|
|
||||||
Trace.Info($"Client cert '{certSetting.ClientCert}'");
|
|
||||||
Trace.Info($"Client cert private key '{certSetting.ClientCertPrivatekey}'");
|
|
||||||
Trace.Info($"Client cert archive '{certSetting.ClientCertArchive}'");
|
|
||||||
|
|
||||||
ClientCertificateFile = certSetting.ClientCert;
|
|
||||||
ClientCertificatePrivateKeyFile = certSetting.ClientCertPrivatekey;
|
|
||||||
ClientCertificateArchiveFile = certSetting.ClientCertArchive;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(certSetting.ClientCertPasswordLookupKey))
|
|
||||||
{
|
|
||||||
var cerdStore = HostContext.GetService<IRunnerCredentialStore>();
|
|
||||||
ClientCertificatePassword = cerdStore.Read($"GITHUB_ACTIONS_RUNNER_CLIENT_CERT_PASSWORD_{certSetting.ClientCertPasswordLookupKey}").Password;
|
|
||||||
HostContext.SecretMasker.AddValue(ClientCertificatePassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
_runnerClientCertificateManager.AddClientCertificate(ClientCertificateArchiveFile, ClientCertificatePassword);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Trace.Info("No certificate setting found.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataContract]
|
|
||||||
internal class RunnerCertificateSetting
|
|
||||||
{
|
|
||||||
[DataMember]
|
|
||||||
public bool SkipServerCertValidation { get; set; }
|
|
||||||
|
|
||||||
[DataMember]
|
|
||||||
public string CACert { get; set; }
|
|
||||||
|
|
||||||
[DataMember]
|
|
||||||
public string ClientCert { get; set; }
|
|
||||||
|
|
||||||
[DataMember]
|
|
||||||
public string ClientCertPrivatekey { get; set; }
|
|
||||||
|
|
||||||
[DataMember]
|
|
||||||
public string ClientCertArchive { get; set; }
|
|
||||||
|
|
||||||
[DataMember]
|
|
||||||
public string ClientCertPasswordLookupKey { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,948 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
|
||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using GitHub.Runner.Sdk;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Common
|
|
||||||
{
|
|
||||||
// The purpose of this class is to store user's credential during runner configuration and retrive the credential back at runtime.
|
|
||||||
#if OS_WINDOWS
|
|
||||||
[ServiceLocator(Default = typeof(WindowsRunnerCredentialStore))]
|
|
||||||
#elif OS_OSX
|
|
||||||
[ServiceLocator(Default = typeof(MacOSRunnerCredentialStore))]
|
|
||||||
#else
|
|
||||||
[ServiceLocator(Default = typeof(LinuxRunnerCredentialStore))]
|
|
||||||
#endif
|
|
||||||
public interface IRunnerCredentialStore : IRunnerService
|
|
||||||
{
|
|
||||||
NetworkCredential Write(string target, string username, string password);
|
|
||||||
|
|
||||||
// throw exception when target not found from cred store
|
|
||||||
NetworkCredential Read(string target);
|
|
||||||
|
|
||||||
// throw exception when target not found from cred store
|
|
||||||
void Delete(string target);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if OS_WINDOWS
|
|
||||||
// Windows credential store is per user.
|
|
||||||
// This is a limitation for user configure the runner run as windows service, when user's current login account is different with the service run as account.
|
|
||||||
// Ex: I login the box as domain\admin, configure the runner as windows service and run as domian\buildserver
|
|
||||||
// domain\buildserver won't read the stored credential from domain\admin's windows credential store.
|
|
||||||
// To workaround this limitation.
|
|
||||||
// Anytime we try to save a credential:
|
|
||||||
// 1. store it into current user's windows credential store
|
|
||||||
// 2. use DP-API do a machine level encrypt and store the encrypted content on disk.
|
|
||||||
// At the first time we try to read the credential:
|
|
||||||
// 1. read from current user's windows credential store, delete the DP-API encrypted backup content on disk if the windows credential store read succeed.
|
|
||||||
// 2. if credential not found in current user's windows credential store, read from the DP-API encrypted backup content on disk,
|
|
||||||
// write the credential back the current user's windows credential store and delete the backup on disk.
|
|
||||||
public sealed class WindowsRunnerCredentialStore : RunnerService, IRunnerCredentialStore
|
|
||||||
{
|
|
||||||
private string _credStoreFile;
|
|
||||||
private Dictionary<string, string> _credStore;
|
|
||||||
|
|
||||||
public override void Initialize(IHostContext hostContext)
|
|
||||||
{
|
|
||||||
base.Initialize(hostContext);
|
|
||||||
|
|
||||||
_credStoreFile = hostContext.GetConfigFile(WellKnownConfigFile.CredentialStore);
|
|
||||||
if (File.Exists(_credStoreFile))
|
|
||||||
{
|
|
||||||
_credStore = IOUtil.LoadObject<Dictionary<string, string>>(_credStoreFile);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_credStore = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public NetworkCredential Write(string target, string username, string password)
|
|
||||||
{
|
|
||||||
Trace.Entering();
|
|
||||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
|
||||||
ArgUtil.NotNullOrEmpty(username, nameof(username));
|
|
||||||
ArgUtil.NotNullOrEmpty(password, nameof(password));
|
|
||||||
|
|
||||||
// save to .credential_store file first, then Windows credential store
|
|
||||||
string usernameBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(username));
|
|
||||||
string passwordBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(password));
|
|
||||||
|
|
||||||
// Base64Username:Base64Password -> DP-API machine level encrypt -> Base64Encoding
|
|
||||||
string encryptedUsernamePassword = Convert.ToBase64String(ProtectedData.Protect(Encoding.UTF8.GetBytes($"{usernameBase64}:{passwordBase64}"), null, DataProtectionScope.LocalMachine));
|
|
||||||
Trace.Info($"Credentials for '{target}' written to credential store file.");
|
|
||||||
_credStore[target] = encryptedUsernamePassword;
|
|
||||||
|
|
||||||
// save to .credential_store file
|
|
||||||
SyncCredentialStoreFile();
|
|
||||||
|
|
||||||
// save to Windows Credential Store
|
|
||||||
return WriteInternal(target, username, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public NetworkCredential Read(string target)
|
|
||||||
{
|
|
||||||
Trace.Entering();
|
|
||||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
|
||||||
IntPtr credPtr = IntPtr.Zero;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (CredRead(target, CredentialType.Generic, 0, out credPtr))
|
|
||||||
{
|
|
||||||
Credential credStruct = (Credential)Marshal.PtrToStructure(credPtr, typeof(Credential));
|
|
||||||
int passwordLength = (int)credStruct.CredentialBlobSize;
|
|
||||||
string password = passwordLength > 0 ? Marshal.PtrToStringUni(credStruct.CredentialBlob, passwordLength / sizeof(char)) : String.Empty;
|
|
||||||
string username = Marshal.PtrToStringUni(credStruct.UserName);
|
|
||||||
Trace.Info($"Credentials for '{target}' read from windows credential store.");
|
|
||||||
|
|
||||||
// delete from .credential_store file since we are able to read it from windows credential store
|
|
||||||
if (_credStore.Remove(target))
|
|
||||||
{
|
|
||||||
Trace.Info($"Delete credentials for '{target}' from credential store file.");
|
|
||||||
SyncCredentialStoreFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new NetworkCredential(username, password);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Can't read from Windows Credential Store, fail back to .credential_store file
|
|
||||||
if (_credStore.ContainsKey(target) && !string.IsNullOrEmpty(_credStore[target]))
|
|
||||||
{
|
|
||||||
Trace.Info($"Credentials for '{target}' read from credential store file.");
|
|
||||||
|
|
||||||
// Base64Decode -> DP-API machine level decrypt -> Base64Username:Base64Password -> Base64Decode
|
|
||||||
string decryptedUsernamePassword = Encoding.UTF8.GetString(ProtectedData.Unprotect(Convert.FromBase64String(_credStore[target]), null, DataProtectionScope.LocalMachine));
|
|
||||||
|
|
||||||
string[] credential = decryptedUsernamePassword.Split(':');
|
|
||||||
if (credential.Length == 2 && !string.IsNullOrEmpty(credential[0]) && !string.IsNullOrEmpty(credential[1]))
|
|
||||||
{
|
|
||||||
string username = Encoding.UTF8.GetString(Convert.FromBase64String(credential[0]));
|
|
||||||
string password = Encoding.UTF8.GetString(Convert.FromBase64String(credential[1]));
|
|
||||||
|
|
||||||
// store back to windows credential store for current user
|
|
||||||
NetworkCredential creds = WriteInternal(target, username, password);
|
|
||||||
|
|
||||||
// delete from .credential_store file since we are able to write the credential to windows credential store for current user.
|
|
||||||
if (_credStore.Remove(target))
|
|
||||||
{
|
|
||||||
Trace.Info($"Delete credentials for '{target}' from credential store file.");
|
|
||||||
SyncCredentialStoreFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
return creds;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(decryptedUsernamePassword));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Win32Exception(Marshal.GetLastWin32Error(), $"CredRead throw an error for '{target}'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (credPtr != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
CredFree(credPtr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Delete(string target)
|
|
||||||
{
|
|
||||||
Trace.Entering();
|
|
||||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
|
||||||
|
|
||||||
// remove from .credential_store file
|
|
||||||
if (_credStore.Remove(target))
|
|
||||||
{
|
|
||||||
Trace.Info($"Delete credentials for '{target}' from credential store file.");
|
|
||||||
SyncCredentialStoreFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove from windows credential store
|
|
||||||
if (!CredDelete(target, CredentialType.Generic, 0))
|
|
||||||
{
|
|
||||||
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Failed to delete credentials for {target}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Trace.Info($"Credentials for '{target}' deleted from windows credential store.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private NetworkCredential WriteInternal(string target, string username, string password)
|
|
||||||
{
|
|
||||||
// save to Windows Credential Store
|
|
||||||
Credential credential = new Credential()
|
|
||||||
{
|
|
||||||
Type = CredentialType.Generic,
|
|
||||||
Persist = (UInt32)CredentialPersist.LocalMachine,
|
|
||||||
TargetName = Marshal.StringToCoTaskMemUni(target),
|
|
||||||
UserName = Marshal.StringToCoTaskMemUni(username),
|
|
||||||
CredentialBlob = Marshal.StringToCoTaskMemUni(password),
|
|
||||||
CredentialBlobSize = (UInt32)Encoding.Unicode.GetByteCount(password),
|
|
||||||
AttributeCount = 0,
|
|
||||||
Comment = IntPtr.Zero,
|
|
||||||
Attributes = IntPtr.Zero,
|
|
||||||
TargetAlias = IntPtr.Zero
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (CredWrite(ref credential, 0))
|
|
||||||
{
|
|
||||||
Trace.Info($"Credentials for '{target}' written to windows credential store.");
|
|
||||||
return new NetworkCredential(username, password);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
int error = Marshal.GetLastWin32Error();
|
|
||||||
throw new Win32Exception(error, "Failed to write credentials");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (credential.CredentialBlob != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
Marshal.FreeCoTaskMem(credential.CredentialBlob);
|
|
||||||
}
|
|
||||||
if (credential.TargetName != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
Marshal.FreeCoTaskMem(credential.TargetName);
|
|
||||||
}
|
|
||||||
if (credential.UserName != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
Marshal.FreeCoTaskMem(credential.UserName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SyncCredentialStoreFile()
|
|
||||||
{
|
|
||||||
Trace.Info("Sync in-memory credential store with credential store file.");
|
|
||||||
|
|
||||||
// delete the cred store file first anyway, since it's a readonly file.
|
|
||||||
IOUtil.DeleteFile(_credStoreFile);
|
|
||||||
|
|
||||||
// delete cred store file when all creds gone
|
|
||||||
if (_credStore.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
IOUtil.SaveObject(_credStore, _credStoreFile);
|
|
||||||
File.SetAttributes(_credStoreFile, File.GetAttributes(_credStoreFile) | FileAttributes.Hidden);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
||||||
internal static extern bool CredDelete(string target, CredentialType type, int reservedFlag);
|
|
||||||
|
|
||||||
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
||||||
internal static extern bool CredRead(string target, CredentialType type, int reservedFlag, out IntPtr CredentialPtr);
|
|
||||||
|
|
||||||
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
||||||
internal static extern bool CredWrite([In] ref Credential userCredential, [In] UInt32 flags);
|
|
||||||
|
|
||||||
[DllImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)]
|
|
||||||
internal static extern bool CredFree([In] IntPtr cred);
|
|
||||||
|
|
||||||
internal enum CredentialPersist : UInt32
|
|
||||||
{
|
|
||||||
Session = 0x01,
|
|
||||||
LocalMachine = 0x02
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum CredentialType : uint
|
|
||||||
{
|
|
||||||
Generic = 0x01,
|
|
||||||
DomainPassword = 0x02,
|
|
||||||
DomainCertificate = 0x03
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
||||||
internal struct Credential
|
|
||||||
{
|
|
||||||
public UInt32 Flags;
|
|
||||||
public CredentialType Type;
|
|
||||||
public IntPtr TargetName;
|
|
||||||
public IntPtr Comment;
|
|
||||||
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
||||||
public UInt32 CredentialBlobSize;
|
|
||||||
public IntPtr CredentialBlob;
|
|
||||||
public UInt32 Persist;
|
|
||||||
public UInt32 AttributeCount;
|
|
||||||
public IntPtr Attributes;
|
|
||||||
public IntPtr TargetAlias;
|
|
||||||
public IntPtr UserName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#elif OS_OSX
|
|
||||||
public sealed class MacOSRunnerCredentialStore : RunnerService, IRunnerCredentialStore
|
|
||||||
{
|
|
||||||
private const string _osxRunnerCredStoreKeyChainName = "_GITHUB_ACTIONS_RUNNER_CREDSTORE_INTERNAL_";
|
|
||||||
|
|
||||||
// Keychain requires a password, but this is not intended to add security
|
|
||||||
private const string _osxRunnerCredStoreKeyChainPassword = "C46F23C36AF94B72B1EAEE32C68670A0";
|
|
||||||
|
|
||||||
private string _securityUtil;
|
|
||||||
|
|
||||||
private string _runnerCredStoreKeyChain;
|
|
||||||
|
|
||||||
public override void Initialize(IHostContext hostContext)
|
|
||||||
{
|
|
||||||
base.Initialize(hostContext);
|
|
||||||
|
|
||||||
_securityUtil = WhichUtil.Which("security", true, Trace);
|
|
||||||
|
|
||||||
_runnerCredStoreKeyChain = hostContext.GetConfigFile(WellKnownConfigFile.CredentialStore);
|
|
||||||
|
|
||||||
// Create osx key chain if it doesn't exists.
|
|
||||||
if (!File.Exists(_runnerCredStoreKeyChain))
|
|
||||||
{
|
|
||||||
List<string> securityOut = new List<string>();
|
|
||||||
List<string> securityError = new List<string>();
|
|
||||||
object outputLock = new object();
|
|
||||||
using (var p = HostContext.CreateService<IProcessInvoker>())
|
|
||||||
{
|
|
||||||
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(stdout.Data))
|
|
||||||
{
|
|
||||||
lock (outputLock)
|
|
||||||
{
|
|
||||||
securityOut.Add(stdout.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(stderr.Data))
|
|
||||||
{
|
|
||||||
lock (outputLock)
|
|
||||||
{
|
|
||||||
securityError.Add(stderr.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// make sure the 'security' has access to the key so we won't get prompt at runtime.
|
|
||||||
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
|
||||||
fileName: _securityUtil,
|
|
||||||
arguments: $"create-keychain -p {_osxRunnerCredStoreKeyChainPassword} \"{_runnerCredStoreKeyChain}\"",
|
|
||||||
environment: null,
|
|
||||||
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
|
|
||||||
if (exitCode == 0)
|
|
||||||
{
|
|
||||||
Trace.Info($"Successfully create-keychain for {_runnerCredStoreKeyChain}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (securityOut.Count > 0)
|
|
||||||
{
|
|
||||||
Trace.Error(string.Join(Environment.NewLine, securityOut));
|
|
||||||
}
|
|
||||||
if (securityError.Count > 0)
|
|
||||||
{
|
|
||||||
Trace.Error(string.Join(Environment.NewLine, securityError));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidOperationException($"'security create-keychain' failed with exit code {exitCode}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Try unlock and lock the keychain, make sure it's still in good stage
|
|
||||||
UnlockKeyChain();
|
|
||||||
LockKeyChain();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public NetworkCredential Write(string target, string username, string password)
|
|
||||||
{
|
|
||||||
Trace.Entering();
|
|
||||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
|
||||||
ArgUtil.NotNullOrEmpty(username, nameof(username));
|
|
||||||
ArgUtil.NotNullOrEmpty(password, nameof(password));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
UnlockKeyChain();
|
|
||||||
|
|
||||||
// base64encode username + ':' + base64encode password
|
|
||||||
// OSX keychain requires you provide -s target and -a username to retrieve password
|
|
||||||
// So, we will trade both username and password as 'secret' store into keychain
|
|
||||||
string usernameBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(username));
|
|
||||||
string passwordBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(password));
|
|
||||||
string secretForKeyChain = $"{usernameBase64}:{passwordBase64}";
|
|
||||||
|
|
||||||
List<string> securityOut = new List<string>();
|
|
||||||
List<string> securityError = new List<string>();
|
|
||||||
object outputLock = new object();
|
|
||||||
using (var p = HostContext.CreateService<IProcessInvoker>())
|
|
||||||
{
|
|
||||||
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(stdout.Data))
|
|
||||||
{
|
|
||||||
lock (outputLock)
|
|
||||||
{
|
|
||||||
securityOut.Add(stdout.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(stderr.Data))
|
|
||||||
{
|
|
||||||
lock (outputLock)
|
|
||||||
{
|
|
||||||
securityError.Add(stderr.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// make sure the 'security' has access to the key so we won't get prompt at runtime.
|
|
||||||
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
|
||||||
fileName: _securityUtil,
|
|
||||||
arguments: $"add-generic-password -s {target} -a GITHUBACTIONSRUNNER -w {secretForKeyChain} -T \"{_securityUtil}\" \"{_runnerCredStoreKeyChain}\"",
|
|
||||||
environment: null,
|
|
||||||
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
|
|
||||||
if (exitCode == 0)
|
|
||||||
{
|
|
||||||
Trace.Info($"Successfully add-generic-password for {target} (GITHUBACTIONSRUNNER)");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (securityOut.Count > 0)
|
|
||||||
{
|
|
||||||
Trace.Error(string.Join(Environment.NewLine, securityOut));
|
|
||||||
}
|
|
||||||
if (securityError.Count > 0)
|
|
||||||
{
|
|
||||||
Trace.Error(string.Join(Environment.NewLine, securityError));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidOperationException($"'security add-generic-password' failed with exit code {exitCode}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new NetworkCredential(username, password);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
LockKeyChain();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public NetworkCredential Read(string target)
|
|
||||||
{
|
|
||||||
Trace.Entering();
|
|
||||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
UnlockKeyChain();
|
|
||||||
|
|
||||||
string username;
|
|
||||||
string password;
|
|
||||||
|
|
||||||
List<string> securityOut = new List<string>();
|
|
||||||
List<string> securityError = new List<string>();
|
|
||||||
object outputLock = new object();
|
|
||||||
using (var p = HostContext.CreateService<IProcessInvoker>())
|
|
||||||
{
|
|
||||||
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(stdout.Data))
|
|
||||||
{
|
|
||||||
lock (outputLock)
|
|
||||||
{
|
|
||||||
securityOut.Add(stdout.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(stderr.Data))
|
|
||||||
{
|
|
||||||
lock (outputLock)
|
|
||||||
{
|
|
||||||
securityError.Add(stderr.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
|
||||||
fileName: _securityUtil,
|
|
||||||
arguments: $"find-generic-password -s {target} -a GITHUBACTIONSRUNNER -w -g \"{_runnerCredStoreKeyChain}\"",
|
|
||||||
environment: null,
|
|
||||||
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
|
|
||||||
if (exitCode == 0)
|
|
||||||
{
|
|
||||||
string keyChainSecret = securityOut.First();
|
|
||||||
string[] secrets = keyChainSecret.Split(':');
|
|
||||||
if (secrets.Length == 2 && !string.IsNullOrEmpty(secrets[0]) && !string.IsNullOrEmpty(secrets[1]))
|
|
||||||
{
|
|
||||||
Trace.Info($"Successfully find-generic-password for {target} (GITHUBACTIONSRUNNER)");
|
|
||||||
username = Encoding.UTF8.GetString(Convert.FromBase64String(secrets[0]));
|
|
||||||
password = Encoding.UTF8.GetString(Convert.FromBase64String(secrets[1]));
|
|
||||||
return new NetworkCredential(username, password);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(keyChainSecret));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (securityOut.Count > 0)
|
|
||||||
{
|
|
||||||
Trace.Error(string.Join(Environment.NewLine, securityOut));
|
|
||||||
}
|
|
||||||
if (securityError.Count > 0)
|
|
||||||
{
|
|
||||||
Trace.Error(string.Join(Environment.NewLine, securityError));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidOperationException($"'security find-generic-password' failed with exit code {exitCode}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
LockKeyChain();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Delete(string target)
|
|
||||||
{
|
|
||||||
Trace.Entering();
|
|
||||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
UnlockKeyChain();
|
|
||||||
|
|
||||||
List<string> securityOut = new List<string>();
|
|
||||||
List<string> securityError = new List<string>();
|
|
||||||
object outputLock = new object();
|
|
||||||
|
|
||||||
using (var p = HostContext.CreateService<IProcessInvoker>())
|
|
||||||
{
|
|
||||||
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(stdout.Data))
|
|
||||||
{
|
|
||||||
lock (outputLock)
|
|
||||||
{
|
|
||||||
securityOut.Add(stdout.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(stderr.Data))
|
|
||||||
{
|
|
||||||
lock (outputLock)
|
|
||||||
{
|
|
||||||
securityError.Add(stderr.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
|
||||||
fileName: _securityUtil,
|
|
||||||
arguments: $"delete-generic-password -s {target} -a GITHUBACTIONSRUNNER \"{_runnerCredStoreKeyChain}\"",
|
|
||||||
environment: null,
|
|
||||||
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
|
|
||||||
if (exitCode == 0)
|
|
||||||
{
|
|
||||||
Trace.Info($"Successfully delete-generic-password for {target} (GITHUBACTIONSRUNNER)");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (securityOut.Count > 0)
|
|
||||||
{
|
|
||||||
Trace.Error(string.Join(Environment.NewLine, securityOut));
|
|
||||||
}
|
|
||||||
if (securityError.Count > 0)
|
|
||||||
{
|
|
||||||
Trace.Error(string.Join(Environment.NewLine, securityError));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidOperationException($"'security delete-generic-password' failed with exit code {exitCode}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
LockKeyChain();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UnlockKeyChain()
|
|
||||||
{
|
|
||||||
Trace.Entering();
|
|
||||||
ArgUtil.NotNullOrEmpty(_securityUtil, nameof(_securityUtil));
|
|
||||||
ArgUtil.NotNullOrEmpty(_runnerCredStoreKeyChain, nameof(_runnerCredStoreKeyChain));
|
|
||||||
|
|
||||||
List<string> securityOut = new List<string>();
|
|
||||||
List<string> securityError = new List<string>();
|
|
||||||
object outputLock = new object();
|
|
||||||
using (var p = HostContext.CreateService<IProcessInvoker>())
|
|
||||||
{
|
|
||||||
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(stdout.Data))
|
|
||||||
{
|
|
||||||
lock (outputLock)
|
|
||||||
{
|
|
||||||
securityOut.Add(stdout.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(stderr.Data))
|
|
||||||
{
|
|
||||||
lock (outputLock)
|
|
||||||
{
|
|
||||||
securityError.Add(stderr.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// make sure the 'security' has access to the key so we won't get prompt at runtime.
|
|
||||||
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
|
||||||
fileName: _securityUtil,
|
|
||||||
arguments: $"unlock-keychain -p {_osxRunnerCredStoreKeyChainPassword} \"{_runnerCredStoreKeyChain}\"",
|
|
||||||
environment: null,
|
|
||||||
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
|
|
||||||
if (exitCode == 0)
|
|
||||||
{
|
|
||||||
Trace.Info($"Successfully unlock-keychain for {_runnerCredStoreKeyChain}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (securityOut.Count > 0)
|
|
||||||
{
|
|
||||||
Trace.Error(string.Join(Environment.NewLine, securityOut));
|
|
||||||
}
|
|
||||||
if (securityError.Count > 0)
|
|
||||||
{
|
|
||||||
Trace.Error(string.Join(Environment.NewLine, securityError));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidOperationException($"'security unlock-keychain' failed with exit code {exitCode}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LockKeyChain()
|
|
||||||
{
|
|
||||||
Trace.Entering();
|
|
||||||
ArgUtil.NotNullOrEmpty(_securityUtil, nameof(_securityUtil));
|
|
||||||
ArgUtil.NotNullOrEmpty(_runnerCredStoreKeyChain, nameof(_runnerCredStoreKeyChain));
|
|
||||||
|
|
||||||
List<string> securityOut = new List<string>();
|
|
||||||
List<string> securityError = new List<string>();
|
|
||||||
object outputLock = new object();
|
|
||||||
using (var p = HostContext.CreateService<IProcessInvoker>())
|
|
||||||
{
|
|
||||||
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(stdout.Data))
|
|
||||||
{
|
|
||||||
lock (outputLock)
|
|
||||||
{
|
|
||||||
securityOut.Add(stdout.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(stderr.Data))
|
|
||||||
{
|
|
||||||
lock (outputLock)
|
|
||||||
{
|
|
||||||
securityError.Add(stderr.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// make sure the 'security' has access to the key so we won't get prompt at runtime.
|
|
||||||
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
|
||||||
fileName: _securityUtil,
|
|
||||||
arguments: $"lock-keychain \"{_runnerCredStoreKeyChain}\"",
|
|
||||||
environment: null,
|
|
||||||
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
|
|
||||||
if (exitCode == 0)
|
|
||||||
{
|
|
||||||
Trace.Info($"Successfully lock-keychain for {_runnerCredStoreKeyChain}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (securityOut.Count > 0)
|
|
||||||
{
|
|
||||||
Trace.Error(string.Join(Environment.NewLine, securityOut));
|
|
||||||
}
|
|
||||||
if (securityError.Count > 0)
|
|
||||||
{
|
|
||||||
Trace.Error(string.Join(Environment.NewLine, securityError));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidOperationException($"'security lock-keychain' failed with exit code {exitCode}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
public sealed class LinuxRunnerCredentialStore : RunnerService, IRunnerCredentialStore
|
|
||||||
{
|
|
||||||
// 'ghrunner' 128 bits iv
|
|
||||||
private readonly byte[] iv = new byte[] { 0x67, 0x68, 0x72, 0x75, 0x6e, 0x6e, 0x65, 0x72, 0x67, 0x68, 0x72, 0x75, 0x6e, 0x6e, 0x65, 0x72 };
|
|
||||||
|
|
||||||
// 256 bits key
|
|
||||||
private byte[] _symmetricKey;
|
|
||||||
private string _credStoreFile;
|
|
||||||
private Dictionary<string, Credential> _credStore;
|
|
||||||
|
|
||||||
public override void Initialize(IHostContext hostContext)
|
|
||||||
{
|
|
||||||
base.Initialize(hostContext);
|
|
||||||
|
|
||||||
_credStoreFile = hostContext.GetConfigFile(WellKnownConfigFile.CredentialStore);
|
|
||||||
if (File.Exists(_credStoreFile))
|
|
||||||
{
|
|
||||||
_credStore = IOUtil.LoadObject<Dictionary<string, Credential>>(_credStoreFile);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_credStore = new Dictionary<string, Credential>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
string machineId;
|
|
||||||
if (File.Exists("/etc/machine-id"))
|
|
||||||
{
|
|
||||||
// try use machine-id as encryption key
|
|
||||||
// this helps avoid accidental information disclosure, but isn't intended for true security
|
|
||||||
machineId = File.ReadAllLines("/etc/machine-id").FirstOrDefault();
|
|
||||||
Trace.Info($"machine-id length {machineId?.Length ?? 0}.");
|
|
||||||
|
|
||||||
// machine-id doesn't exist or machine-id is not 256 bits
|
|
||||||
if (string.IsNullOrEmpty(machineId) || machineId.Length != 32)
|
|
||||||
{
|
|
||||||
Trace.Warning("Can not get valid machine id from '/etc/machine-id'.");
|
|
||||||
machineId = "43e7fe5da07740cf914b90f1dac51c2a";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// /etc/machine-id not exist
|
|
||||||
Trace.Warning("/etc/machine-id doesn't exist.");
|
|
||||||
machineId = "43e7fe5da07740cf914b90f1dac51c2a";
|
|
||||||
}
|
|
||||||
|
|
||||||
List<byte> keyBuilder = new List<byte>();
|
|
||||||
foreach (var c in machineId)
|
|
||||||
{
|
|
||||||
keyBuilder.Add(Convert.ToByte(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
_symmetricKey = keyBuilder.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public NetworkCredential Write(string target, string username, string password)
|
|
||||||
{
|
|
||||||
Trace.Entering();
|
|
||||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
|
||||||
ArgUtil.NotNullOrEmpty(username, nameof(username));
|
|
||||||
ArgUtil.NotNullOrEmpty(password, nameof(password));
|
|
||||||
|
|
||||||
Trace.Info($"Store credential for '{target}' to cred store.");
|
|
||||||
Credential cred = new Credential(username, Encrypt(password));
|
|
||||||
_credStore[target] = cred;
|
|
||||||
SyncCredentialStoreFile();
|
|
||||||
return new NetworkCredential(username, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public NetworkCredential Read(string target)
|
|
||||||
{
|
|
||||||
Trace.Entering();
|
|
||||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
|
||||||
Trace.Info($"Read credential for '{target}' from cred store.");
|
|
||||||
if (_credStore.ContainsKey(target))
|
|
||||||
{
|
|
||||||
Credential cred = _credStore[target];
|
|
||||||
if (!string.IsNullOrEmpty(cred.UserName) && !string.IsNullOrEmpty(cred.Password))
|
|
||||||
{
|
|
||||||
Trace.Info($"Return credential for '{target}' from cred store.");
|
|
||||||
return new NetworkCredential(cred.UserName, Decrypt(cred.Password));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new KeyNotFoundException(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Delete(string target)
|
|
||||||
{
|
|
||||||
Trace.Entering();
|
|
||||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
|
||||||
|
|
||||||
if (_credStore.ContainsKey(target))
|
|
||||||
{
|
|
||||||
Trace.Info($"Delete credential for '{target}' from cred store.");
|
|
||||||
_credStore.Remove(target);
|
|
||||||
SyncCredentialStoreFile();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new KeyNotFoundException(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SyncCredentialStoreFile()
|
|
||||||
{
|
|
||||||
Trace.Entering();
|
|
||||||
Trace.Info("Sync in-memory credential store with credential store file.");
|
|
||||||
|
|
||||||
// delete cred store file when all creds gone
|
|
||||||
if (_credStore.Count == 0)
|
|
||||||
{
|
|
||||||
IOUtil.DeleteFile(_credStoreFile);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!File.Exists(_credStoreFile))
|
|
||||||
{
|
|
||||||
CreateCredentialStoreFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
IOUtil.SaveObject(_credStore, _credStoreFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string Encrypt(string secret)
|
|
||||||
{
|
|
||||||
using (Aes aes = Aes.Create())
|
|
||||||
{
|
|
||||||
aes.Key = _symmetricKey;
|
|
||||||
aes.IV = iv;
|
|
||||||
|
|
||||||
// Create a decrytor to perform the stream transform.
|
|
||||||
ICryptoTransform encryptor = aes.CreateEncryptor();
|
|
||||||
|
|
||||||
// Create the streams used for encryption.
|
|
||||||
using (MemoryStream msEncrypt = new MemoryStream())
|
|
||||||
{
|
|
||||||
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
|
|
||||||
{
|
|
||||||
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
|
|
||||||
{
|
|
||||||
swEncrypt.Write(secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Convert.ToBase64String(msEncrypt.ToArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string Decrypt(string encryptedText)
|
|
||||||
{
|
|
||||||
using (Aes aes = Aes.Create())
|
|
||||||
{
|
|
||||||
aes.Key = _symmetricKey;
|
|
||||||
aes.IV = iv;
|
|
||||||
|
|
||||||
// Create a decrytor to perform the stream transform.
|
|
||||||
ICryptoTransform decryptor = aes.CreateDecryptor();
|
|
||||||
|
|
||||||
// Create the streams used for decryption.
|
|
||||||
using (MemoryStream msDecrypt = new MemoryStream(Convert.FromBase64String(encryptedText)))
|
|
||||||
{
|
|
||||||
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
|
|
||||||
{
|
|
||||||
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
|
|
||||||
{
|
|
||||||
// Read the decrypted bytes from the decrypting stream and place them in a string.
|
|
||||||
return srDecrypt.ReadToEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateCredentialStoreFile()
|
|
||||||
{
|
|
||||||
File.WriteAllText(_credStoreFile, "");
|
|
||||||
File.SetAttributes(_credStoreFile, File.GetAttributes(_credStoreFile) | FileAttributes.Hidden);
|
|
||||||
|
|
||||||
// Try to lock down the .credentials_store file to the owner/group
|
|
||||||
var chmodPath = WhichUtil.Which("chmod", trace: Trace);
|
|
||||||
if (!String.IsNullOrEmpty(chmodPath))
|
|
||||||
{
|
|
||||||
var arguments = $"600 {new FileInfo(_credStoreFile).FullName}";
|
|
||||||
using (var invoker = HostContext.CreateService<IProcessInvoker>())
|
|
||||||
{
|
|
||||||
var exitCode = invoker.ExecuteAsync(HostContext.GetDirectory(WellKnownDirectory.Root), chmodPath, arguments, null, default(CancellationToken)).GetAwaiter().GetResult();
|
|
||||||
if (exitCode == 0)
|
|
||||||
{
|
|
||||||
Trace.Info("Successfully set permissions for credentials store file {0}", _credStoreFile);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Trace.Warning("Unable to successfully set permissions for credentials store file {0}. Received exit code {1} from {2}", _credStoreFile, exitCode, chmodPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Trace.Warning("Unable to locate chmod to set permissions for credentials store file {0}.", _credStoreFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataContract]
|
|
||||||
internal class Credential
|
|
||||||
{
|
|
||||||
public Credential()
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public Credential(string userName, string password)
|
|
||||||
{
|
|
||||||
UserName = userName;
|
|
||||||
Password = password;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(IsRequired = true)]
|
|
||||||
public string UserName { get; set; }
|
|
||||||
|
|
||||||
[DataMember(IsRequired = true)]
|
|
||||||
public string Password { get; set; }
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
@@ -66,11 +66,6 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
public async Task ConnectAsync(Uri serverUrl, VssCredentials credentials)
|
public async Task ConnectAsync(Uri serverUrl, VssCredentials credentials)
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var createGenericConnection = EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(100));
|
var createGenericConnection = EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(100));
|
||||||
var createMessageConnection = EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(60));
|
var createMessageConnection = EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(60));
|
||||||
var createRequestConnection = EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(60));
|
var createRequestConnection = EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(60));
|
||||||
@@ -303,29 +298,18 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
public Task<TaskAgentJobRequest> RenewAgentRequestAsync(int poolId, long requestId, Guid lockToken, CancellationToken cancellationToken = default(CancellationToken))
|
public Task<TaskAgentJobRequest> RenewAgentRequestAsync(int poolId, long requestId, Guid lockToken, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return Task.FromResult(JsonUtility.FromString<TaskAgentJobRequest>("{ lockedUntil: \"" + DateTime.Now.Add(TimeSpan.FromMinutes(5)).ToString("u") + "\" }"));
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckConnection(RunnerConnectionType.JobRequest);
|
CheckConnection(RunnerConnectionType.JobRequest);
|
||||||
return _requestTaskAgentClient.RenewAgentRequestAsync(poolId, requestId, lockToken, cancellationToken: cancellationToken);
|
return _requestTaskAgentClient.RenewAgentRequestAsync(poolId, requestId, lockToken, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<TaskAgentJobRequest> FinishAgentRequestAsync(int poolId, long requestId, Guid lockToken, DateTime finishTime, TaskResult result, CancellationToken cancellationToken = default(CancellationToken))
|
public Task<TaskAgentJobRequest> FinishAgentRequestAsync(int poolId, long requestId, Guid lockToken, DateTime finishTime, TaskResult result, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
{
|
{
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
return Task.FromResult<TaskAgentJobRequest>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckConnection(RunnerConnectionType.JobRequest);
|
CheckConnection(RunnerConnectionType.JobRequest);
|
||||||
return _requestTaskAgentClient.FinishAgentRequestAsync(poolId, requestId, lockToken, finishTime, result, cancellationToken: cancellationToken);
|
return _requestTaskAgentClient.FinishAgentRequestAsync(poolId, requestId, lockToken, finishTime, result, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<TaskAgentJobRequest> GetAgentRequestAsync(int poolId, long requestId, CancellationToken cancellationToken = default(CancellationToken))
|
public Task<TaskAgentJobRequest> GetAgentRequestAsync(int poolId, long requestId, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
{
|
{
|
||||||
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
|
|
||||||
CheckConnection(RunnerConnectionType.JobRequest);
|
CheckConnection(RunnerConnectionType.JobRequest);
|
||||||
return _requestTaskAgentClient.GetAgentRequestAsync(poolId, requestId, cancellationToken: cancellationToken);
|
return _requestTaskAgentClient.GetAgentRequestAsync(poolId, requestId, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -335,7 +319,6 @@ namespace GitHub.Runner.Common
|
|||||||
//-----------------------------------------------------------------
|
//-----------------------------------------------------------------
|
||||||
public Task<List<PackageMetadata>> GetPackagesAsync(string packageType, string platform, int top, CancellationToken cancellationToken)
|
public Task<List<PackageMetadata>> GetPackagesAsync(string packageType, string platform, int top, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
|
|
||||||
CheckConnection(RunnerConnectionType.Generic);
|
CheckConnection(RunnerConnectionType.Generic);
|
||||||
return _genericTaskAgentClient.GetPackagesAsync(packageType, platform, top, cancellationToken: cancellationToken);
|
return _genericTaskAgentClient.GetPackagesAsync(packageType, platform, top, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,14 +28,10 @@ namespace GitHub.Runner.Listener
|
|||||||
private readonly string[] validFlags =
|
private readonly string[] validFlags =
|
||||||
{
|
{
|
||||||
Constants.Runner.CommandLine.Flags.Commit,
|
Constants.Runner.CommandLine.Flags.Commit,
|
||||||
#if OS_WINDOWS
|
|
||||||
Constants.Runner.CommandLine.Flags.GitUseSChannel,
|
|
||||||
#endif
|
|
||||||
Constants.Runner.CommandLine.Flags.Help,
|
Constants.Runner.CommandLine.Flags.Help,
|
||||||
Constants.Runner.CommandLine.Flags.Replace,
|
Constants.Runner.CommandLine.Flags.Replace,
|
||||||
Constants.Runner.CommandLine.Flags.RunAsService,
|
Constants.Runner.CommandLine.Flags.RunAsService,
|
||||||
Constants.Runner.CommandLine.Flags.Once,
|
Constants.Runner.CommandLine.Flags.Once,
|
||||||
Constants.Runner.CommandLine.Flags.SslSkipCertValidation,
|
|
||||||
Constants.Runner.CommandLine.Flags.Unattended,
|
Constants.Runner.CommandLine.Flags.Unattended,
|
||||||
Constants.Runner.CommandLine.Flags.Version
|
Constants.Runner.CommandLine.Flags.Version
|
||||||
};
|
};
|
||||||
@@ -45,13 +41,7 @@ namespace GitHub.Runner.Listener
|
|||||||
Constants.Runner.CommandLine.Args.Auth,
|
Constants.Runner.CommandLine.Args.Auth,
|
||||||
Constants.Runner.CommandLine.Args.MonitorSocketAddress,
|
Constants.Runner.CommandLine.Args.MonitorSocketAddress,
|
||||||
Constants.Runner.CommandLine.Args.Name,
|
Constants.Runner.CommandLine.Args.Name,
|
||||||
Constants.Runner.CommandLine.Args.Password,
|
|
||||||
Constants.Runner.CommandLine.Args.Pool,
|
Constants.Runner.CommandLine.Args.Pool,
|
||||||
Constants.Runner.CommandLine.Args.SslCACert,
|
|
||||||
Constants.Runner.CommandLine.Args.SslClientCert,
|
|
||||||
Constants.Runner.CommandLine.Args.SslClientCertKey,
|
|
||||||
Constants.Runner.CommandLine.Args.SslClientCertArchive,
|
|
||||||
Constants.Runner.CommandLine.Args.SslClientCertPassword,
|
|
||||||
Constants.Runner.CommandLine.Args.StartupType,
|
Constants.Runner.CommandLine.Args.StartupType,
|
||||||
Constants.Runner.CommandLine.Args.Token,
|
Constants.Runner.CommandLine.Args.Token,
|
||||||
Constants.Runner.CommandLine.Args.Url,
|
Constants.Runner.CommandLine.Args.Url,
|
||||||
@@ -73,9 +63,6 @@ namespace GitHub.Runner.Listener
|
|||||||
public bool Unattended => TestFlag(Constants.Runner.CommandLine.Flags.Unattended);
|
public bool Unattended => TestFlag(Constants.Runner.CommandLine.Flags.Unattended);
|
||||||
public bool Version => TestFlag(Constants.Runner.CommandLine.Flags.Version);
|
public bool Version => TestFlag(Constants.Runner.CommandLine.Flags.Version);
|
||||||
|
|
||||||
#if OS_WINDOWS
|
|
||||||
public bool GitUseSChannel => TestFlag(Constants.Runner.CommandLine.Flags.GitUseSChannel);
|
|
||||||
#endif
|
|
||||||
public bool RunOnce => TestFlag(Constants.Runner.CommandLine.Flags.Once);
|
public bool RunOnce => TestFlag(Constants.Runner.CommandLine.Flags.Once);
|
||||||
|
|
||||||
// Constructor.
|
// Constructor.
|
||||||
@@ -160,13 +147,6 @@ namespace GitHub.Runner.Listener
|
|||||||
defaultValue: false);
|
defaultValue: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool GetAutoLaunchBrowser()
|
|
||||||
{
|
|
||||||
return TestFlagOrPrompt(
|
|
||||||
name: Constants.Runner.CommandLine.Flags.LaunchBrowser,
|
|
||||||
description: "Would you like to launch your browser for AAD Device Code Flow? (Y/N)",
|
|
||||||
defaultValue: true);
|
|
||||||
}
|
|
||||||
//
|
//
|
||||||
// Args.
|
// Args.
|
||||||
//
|
//
|
||||||
@@ -179,24 +159,6 @@ namespace GitHub.Runner.Listener
|
|||||||
validator: Validators.AuthSchemeValidator);
|
validator: Validators.AuthSchemeValidator);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetPassword()
|
|
||||||
{
|
|
||||||
return GetArgOrPrompt(
|
|
||||||
name: Constants.Runner.CommandLine.Args.Password,
|
|
||||||
description: "What is your GitHub password?",
|
|
||||||
defaultValue: string.Empty,
|
|
||||||
validator: Validators.NonEmptyValidator);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetPool()
|
|
||||||
{
|
|
||||||
return GetArgOrPrompt(
|
|
||||||
name: Constants.Runner.CommandLine.Args.Pool,
|
|
||||||
description: "Enter the name of your runner pool:",
|
|
||||||
defaultValue: "default",
|
|
||||||
validator: Validators.NonEmptyValidator);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetRunnerName()
|
public string GetRunnerName()
|
||||||
{
|
{
|
||||||
return GetArgOrPrompt(
|
return GetArgOrPrompt(
|
||||||
@@ -210,7 +172,7 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
return GetArgOrPrompt(
|
return GetArgOrPrompt(
|
||||||
name: Constants.Runner.CommandLine.Args.Token,
|
name: Constants.Runner.CommandLine.Args.Token,
|
||||||
description: "Enter your personal access token:",
|
description: "What is your pool admin oauth access token?",
|
||||||
defaultValue: string.Empty,
|
defaultValue: string.Empty,
|
||||||
validator: Validators.NonEmptyValidator);
|
validator: Validators.NonEmptyValidator);
|
||||||
}
|
}
|
||||||
@@ -219,7 +181,7 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
return GetArgOrPrompt(
|
return GetArgOrPrompt(
|
||||||
name: Constants.Runner.CommandLine.Args.Token,
|
name: Constants.Runner.CommandLine.Args.Token,
|
||||||
description: "Enter runner register token:",
|
description: "What is your runner register token?",
|
||||||
defaultValue: string.Empty,
|
defaultValue: string.Empty,
|
||||||
validator: Validators.NonEmptyValidator);
|
validator: Validators.NonEmptyValidator);
|
||||||
}
|
}
|
||||||
@@ -249,15 +211,6 @@ namespace GitHub.Runner.Listener
|
|||||||
validator: Validators.ServerUrlValidator);
|
validator: Validators.ServerUrlValidator);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetUserName()
|
|
||||||
{
|
|
||||||
return GetArgOrPrompt(
|
|
||||||
name: Constants.Runner.CommandLine.Args.UserName,
|
|
||||||
description: "What is your GitHub username?",
|
|
||||||
defaultValue: string.Empty,
|
|
||||||
validator: Validators.NonEmptyValidator);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetWindowsLogonAccount(string defaultValue, string descriptionMsg)
|
public string GetWindowsLogonAccount(string defaultValue, string descriptionMsg)
|
||||||
{
|
{
|
||||||
return GetArgOrPrompt(
|
return GetArgOrPrompt(
|
||||||
@@ -296,36 +249,6 @@ namespace GitHub.Runner.Listener
|
|||||||
return GetArg(Constants.Runner.CommandLine.Args.StartupType);
|
return GetArg(Constants.Runner.CommandLine.Args.StartupType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool GetSkipCertificateValidation()
|
|
||||||
{
|
|
||||||
return TestFlag(Constants.Runner.CommandLine.Flags.SslSkipCertValidation);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetCACertificate()
|
|
||||||
{
|
|
||||||
return GetArg(Constants.Runner.CommandLine.Args.SslCACert);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetClientCertificate()
|
|
||||||
{
|
|
||||||
return GetArg(Constants.Runner.CommandLine.Args.SslClientCert);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetClientCertificatePrivateKey()
|
|
||||||
{
|
|
||||||
return GetArg(Constants.Runner.CommandLine.Args.SslClientCertKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetClientCertificateArchrive()
|
|
||||||
{
|
|
||||||
return GetArg(Constants.Runner.CommandLine.Args.SslClientCertArchive);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetClientCertificatePassword()
|
|
||||||
{
|
|
||||||
return GetArg(Constants.Runner.CommandLine.Args.SslClientCertPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Private helpers.
|
// Private helpers.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -79,61 +79,12 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
_term.WriteLine("| |", ConsoleColor.White);
|
_term.WriteLine("| |", ConsoleColor.White);
|
||||||
_term.WriteLine("--------------------------------------------------------------------------------", ConsoleColor.White);
|
_term.WriteLine("--------------------------------------------------------------------------------", ConsoleColor.White);
|
||||||
|
|
||||||
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
|
|
||||||
Trace.Info(nameof(ConfigureAsync));
|
Trace.Info(nameof(ConfigureAsync));
|
||||||
if (IsConfigured())
|
if (IsConfigured())
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Cannot configure the runner because it is already configured. To reconfigure the runner, run 'config.cmd remove' or './config.sh remove' first.");
|
throw new InvalidOperationException("Cannot configure the runner because it is already configured. To reconfigure the runner, run 'config.cmd remove' or './config.sh remove' first.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate cert setting from commandline args
|
|
||||||
var runnerCertManager = HostContext.GetService<IRunnerCertificateManager>();
|
|
||||||
bool saveCertSetting = false;
|
|
||||||
bool skipCertValidation = command.GetSkipCertificateValidation();
|
|
||||||
string caCert = command.GetCACertificate();
|
|
||||||
string clientCert = command.GetClientCertificate();
|
|
||||||
string clientCertKey = command.GetClientCertificatePrivateKey();
|
|
||||||
string clientCertArchive = command.GetClientCertificateArchrive();
|
|
||||||
string clientCertPassword = command.GetClientCertificatePassword();
|
|
||||||
|
|
||||||
// We require all Certificate files are under agent root.
|
|
||||||
// So we can set ACL correctly when configure as service
|
|
||||||
if (!string.IsNullOrEmpty(caCert))
|
|
||||||
{
|
|
||||||
caCert = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), caCert);
|
|
||||||
ArgUtil.File(caCert, nameof(caCert));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(clientCert) &&
|
|
||||||
!string.IsNullOrEmpty(clientCertKey) &&
|
|
||||||
!string.IsNullOrEmpty(clientCertArchive))
|
|
||||||
{
|
|
||||||
// Ensure all client cert pieces are there.
|
|
||||||
clientCert = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), clientCert);
|
|
||||||
clientCertKey = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), clientCertKey);
|
|
||||||
clientCertArchive = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), clientCertArchive);
|
|
||||||
|
|
||||||
ArgUtil.File(clientCert, nameof(clientCert));
|
|
||||||
ArgUtil.File(clientCertKey, nameof(clientCertKey));
|
|
||||||
ArgUtil.File(clientCertArchive, nameof(clientCertArchive));
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(clientCert) ||
|
|
||||||
!string.IsNullOrEmpty(clientCertKey) ||
|
|
||||||
!string.IsNullOrEmpty(clientCertArchive))
|
|
||||||
{
|
|
||||||
// Print out which args are missing.
|
|
||||||
ArgUtil.NotNullOrEmpty(Constants.Runner.CommandLine.Args.SslClientCert, Constants.Runner.CommandLine.Args.SslClientCert);
|
|
||||||
ArgUtil.NotNullOrEmpty(Constants.Runner.CommandLine.Args.SslClientCertKey, Constants.Runner.CommandLine.Args.SslClientCertKey);
|
|
||||||
ArgUtil.NotNullOrEmpty(Constants.Runner.CommandLine.Args.SslClientCertArchive, Constants.Runner.CommandLine.Args.SslClientCertArchive);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skipCertValidation || !string.IsNullOrEmpty(caCert) || !string.IsNullOrEmpty(clientCert))
|
|
||||||
{
|
|
||||||
Trace.Info("Reset runner cert setting base on commandline args.");
|
|
||||||
(runnerCertManager as RunnerCertificateManager).SetupCertificate(skipCertValidation, caCert, clientCert, clientCertKey, clientCertArchive, clientCertPassword);
|
|
||||||
saveCertSetting = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
RunnerSettings runnerSettings = new RunnerSettings();
|
RunnerSettings runnerSettings = new RunnerSettings();
|
||||||
|
|
||||||
bool isHostedServer = false;
|
bool isHostedServer = false;
|
||||||
@@ -353,31 +304,10 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
|
|
||||||
_store.SaveSettings(runnerSettings);
|
_store.SaveSettings(runnerSettings);
|
||||||
|
|
||||||
if (saveCertSetting)
|
|
||||||
{
|
|
||||||
Trace.Info("Save agent cert setting to disk.");
|
|
||||||
(runnerCertManager as RunnerCertificateManager).SaveCertificateSetting();
|
|
||||||
}
|
|
||||||
|
|
||||||
_term.WriteLine();
|
_term.WriteLine();
|
||||||
_term.WriteSuccessMessage("Settings Saved.");
|
_term.WriteSuccessMessage("Settings Saved.");
|
||||||
_term.WriteLine();
|
_term.WriteLine();
|
||||||
|
|
||||||
bool saveRuntimeOptions = false;
|
|
||||||
var runtimeOptions = new RunnerRuntimeOptions();
|
|
||||||
#if OS_WINDOWS
|
|
||||||
if (command.GitUseSChannel)
|
|
||||||
{
|
|
||||||
saveRuntimeOptions = true;
|
|
||||||
runtimeOptions.GitUseSecureChannel = true;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if (saveRuntimeOptions)
|
|
||||||
{
|
|
||||||
Trace.Info("Save agent runtime options to disk.");
|
|
||||||
_store.SaveRunnerRuntimeOptions(runtimeOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if OS_WINDOWS
|
#if OS_WINDOWS
|
||||||
// config windows service
|
// config windows service
|
||||||
bool runAsService = command.GetRunAsService();
|
bool runAsService = command.GetRunAsService();
|
||||||
@@ -397,7 +327,6 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
|
|
||||||
public async Task UnconfigureAsync(CommandSettings command)
|
public async Task UnconfigureAsync(CommandSettings command)
|
||||||
{
|
{
|
||||||
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
|
|
||||||
string currentAction = string.Empty;
|
string currentAction = string.Empty;
|
||||||
|
|
||||||
_term.WriteSection("Runner removal");
|
_term.WriteSection("Runner removal");
|
||||||
@@ -491,13 +420,6 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
currentAction = "Removing .runner";
|
currentAction = "Removing .runner";
|
||||||
if (isConfigured)
|
if (isConfigured)
|
||||||
{
|
{
|
||||||
|
|
||||||
// delete agent cert setting
|
|
||||||
(HostContext.GetService<IRunnerCertificateManager>() as RunnerCertificateManager).DeleteCertificateSetting();
|
|
||||||
|
|
||||||
// delete agent runtime option
|
|
||||||
_store.DeleteRunnerRuntimeOptions();
|
|
||||||
|
|
||||||
_store.DeleteSettings();
|
_store.DeleteSettings();
|
||||||
_term.WriteSuccessMessage("Removed .runner");
|
_term.WriteSuccessMessage("Removed .runner");
|
||||||
}
|
}
|
||||||
@@ -520,7 +442,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
Trace.Info(nameof(GetCredentialProvider));
|
Trace.Info(nameof(GetCredentialProvider));
|
||||||
|
|
||||||
var credentialManager = HostContext.GetService<ICredentialManager>();
|
var credentialManager = HostContext.GetService<ICredentialManager>();
|
||||||
string authType = command.GetAuth(defaultValue: Constants.Configuration.AAD);
|
string authType = command.GetAuth(defaultValue: Constants.Configuration.OAuthAccessToken);
|
||||||
|
|
||||||
// Create the credential.
|
// Create the credential.
|
||||||
Trace.Info("Creating credential for auth: {0}", authType);
|
Trace.Info("Creating credential for auth: {0}", authType);
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
{
|
{
|
||||||
public static readonly Dictionary<string, Type> CredentialTypes = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
|
public static readonly Dictionary<string, Type> CredentialTypes = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ Constants.Configuration.AAD, typeof(AadDeviceCodeAccessToken)},
|
|
||||||
{ Constants.Configuration.PAT, typeof(PersonalAccessToken)},
|
|
||||||
{ Constants.Configuration.OAuth, typeof(OAuthCredential)},
|
{ Constants.Configuration.OAuth, typeof(OAuthCredential)},
|
||||||
{ Constants.Configuration.OAuthAccessToken, typeof(OAuthAccessTokenCredential)},
|
{ Constants.Configuration.OAuthAccessToken, typeof(OAuthAccessTokenCredential)},
|
||||||
};
|
};
|
||||||
@@ -80,7 +78,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
|
|
||||||
if (string.Equals(TokenSchema, "OAuthAccessToken", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(TokenSchema, "OAuthAccessToken", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return new VssCredentials(null, new VssOAuthAccessTokenCredential(Token), CredentialPromptType.DoNotPrompt);
|
return new VssCredentials(new VssOAuthAccessTokenCredential(Token), CredentialPromptType.DoNotPrompt);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using Microsoft.IdentityModel.Clients.ActiveDirectory;
|
|
||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using GitHub.Services.Client;
|
|
||||||
using GitHub.Services.Common;
|
using GitHub.Services.Common;
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Services.OAuth;
|
using GitHub.Services.OAuth;
|
||||||
@@ -37,125 +29,6 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
public abstract void EnsureCredential(IHostContext context, CommandSettings command, string serverUrl);
|
public abstract void EnsureCredential(IHostContext context, CommandSettings command, string serverUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class AadDeviceCodeAccessToken : CredentialProvider
|
|
||||||
{
|
|
||||||
private string _azureDevOpsClientId = "97877f11-0fc6-4aee-b1ff-febb0519dd00";
|
|
||||||
|
|
||||||
public override Boolean RequireInteractive => true;
|
|
||||||
|
|
||||||
public AadDeviceCodeAccessToken() : base(Constants.Configuration.AAD) { }
|
|
||||||
|
|
||||||
public override VssCredentials GetVssCredentials(IHostContext context)
|
|
||||||
{
|
|
||||||
ArgUtil.NotNull(context, nameof(context));
|
|
||||||
Tracing trace = context.GetTrace(nameof(AadDeviceCodeAccessToken));
|
|
||||||
trace.Info(nameof(GetVssCredentials));
|
|
||||||
ArgUtil.NotNull(CredentialData, nameof(CredentialData));
|
|
||||||
|
|
||||||
CredentialData.Data.TryGetValue(Constants.Runner.CommandLine.Args.Url, out string serverUrl);
|
|
||||||
ArgUtil.NotNullOrEmpty(serverUrl, nameof(serverUrl));
|
|
||||||
|
|
||||||
var tenantAuthorityUrl = GetTenantAuthorityUrl(context, serverUrl);
|
|
||||||
if (tenantAuthorityUrl == null)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException($"'{serverUrl}' is not backed by Azure Active Directory.");
|
|
||||||
}
|
|
||||||
|
|
||||||
LoggerCallbackHandler.LogCallback = ((LogLevel level, string message, bool containsPii) =>
|
|
||||||
{
|
|
||||||
switch (level)
|
|
||||||
{
|
|
||||||
case LogLevel.Information:
|
|
||||||
trace.Info(message);
|
|
||||||
break;
|
|
||||||
case LogLevel.Error:
|
|
||||||
trace.Error(message);
|
|
||||||
break;
|
|
||||||
case LogLevel.Warning:
|
|
||||||
trace.Warning(message);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
trace.Verbose(message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
LoggerCallbackHandler.UseDefaultLogging = false;
|
|
||||||
AuthenticationContext ctx = new AuthenticationContext(tenantAuthorityUrl.AbsoluteUri);
|
|
||||||
var queryParameters = $"redirect_uri={Uri.EscapeDataString(new Uri(serverUrl).GetLeftPart(UriPartial.Authority))}";
|
|
||||||
DeviceCodeResult codeResult = ctx.AcquireDeviceCodeAsync("https://management.core.windows.net/", _azureDevOpsClientId, queryParameters).GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
var term = context.GetService<ITerminal>();
|
|
||||||
term.WriteLine($"Please finish AAD device code flow in browser ({codeResult.VerificationUrl}), user code: {codeResult.UserCode}");
|
|
||||||
if (string.Equals(CredentialData.Data[Constants.Runner.CommandLine.Flags.LaunchBrowser], bool.TrueString, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
#if OS_WINDOWS
|
|
||||||
Process.Start(new ProcessStartInfo() { FileName = codeResult.VerificationUrl, UseShellExecute = true });
|
|
||||||
#elif OS_LINUX
|
|
||||||
Process.Start(new ProcessStartInfo() { FileName = "xdg-open", Arguments = codeResult.VerificationUrl });
|
|
||||||
#else
|
|
||||||
Process.Start(new ProcessStartInfo() { FileName = "open", Arguments = codeResult.VerificationUrl });
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// not able to open browser, ex: xdg-open/open is not installed.
|
|
||||||
trace.Error(ex);
|
|
||||||
term.WriteLine($"Fail to open browser. {codeResult.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthenticationResult authResult = ctx.AcquireTokenByDeviceCodeAsync(codeResult).GetAwaiter().GetResult();
|
|
||||||
ArgUtil.NotNull(authResult, nameof(authResult));
|
|
||||||
trace.Info($"receive AAD auth result with {authResult.AccessTokenType} token");
|
|
||||||
|
|
||||||
var aadCred = new VssAadCredential(new VssAadToken(authResult));
|
|
||||||
VssCredentials creds = new VssCredentials(null, aadCred, CredentialPromptType.DoNotPrompt);
|
|
||||||
trace.Info("cred created");
|
|
||||||
|
|
||||||
return creds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void EnsureCredential(IHostContext context, CommandSettings command, string serverUrl)
|
|
||||||
{
|
|
||||||
ArgUtil.NotNull(context, nameof(context));
|
|
||||||
Tracing trace = context.GetTrace(nameof(AadDeviceCodeAccessToken));
|
|
||||||
trace.Info(nameof(EnsureCredential));
|
|
||||||
ArgUtil.NotNull(command, nameof(command));
|
|
||||||
CredentialData.Data[Constants.Runner.CommandLine.Args.Url] = serverUrl;
|
|
||||||
CredentialData.Data[Constants.Runner.CommandLine.Flags.LaunchBrowser] = command.GetAutoLaunchBrowser().ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Uri GetTenantAuthorityUrl(IHostContext context, string serverUrl)
|
|
||||||
{
|
|
||||||
using (var client = new HttpClient(context.CreateHttpClientHandler()))
|
|
||||||
{
|
|
||||||
client.DefaultRequestHeaders.Accept.Clear();
|
|
||||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
|
||||||
client.DefaultRequestHeaders.Add("X-TFS-FedAuthRedirect", "Suppress");
|
|
||||||
client.DefaultRequestHeaders.UserAgent.Clear();
|
|
||||||
client.DefaultRequestHeaders.UserAgent.AddRange(VssClientHttpRequestSettings.Default.UserAgent);
|
|
||||||
var requestMessage = new HttpRequestMessage(HttpMethod.Head, $"{serverUrl.Trim('/')}/_apis/connectiondata");
|
|
||||||
var response = client.SendAsync(requestMessage).GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
// Get the tenant from the Login URL, MSA backed accounts will not return `Bearer` www-authenticate header.
|
|
||||||
var bearerResult = response.Headers.WwwAuthenticate.Where(p => p.Scheme.Equals("Bearer", StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
|
|
||||||
if (bearerResult != null && bearerResult.Parameter.StartsWith("authorization_uri=", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var authorizationUri = bearerResult.Parameter.Substring("authorization_uri=".Length);
|
|
||||||
if (Uri.TryCreate(authorizationUri, UriKind.Absolute, out Uri aadTenantUrl))
|
|
||||||
{
|
|
||||||
return aadTenantUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class OAuthAccessTokenCredential : CredentialProvider
|
public sealed class OAuthAccessTokenCredential : CredentialProvider
|
||||||
{
|
{
|
||||||
public OAuthAccessTokenCredential() : base(Constants.Configuration.OAuthAccessToken) { }
|
public OAuthAccessTokenCredential() : base(Constants.Configuration.OAuthAccessToken) { }
|
||||||
@@ -175,7 +48,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
ArgUtil.NotNullOrEmpty(token, nameof(token));
|
ArgUtil.NotNullOrEmpty(token, nameof(token));
|
||||||
|
|
||||||
trace.Info("token retrieved: {0} chars", token.Length);
|
trace.Info("token retrieved: {0} chars", token.Length);
|
||||||
VssCredentials creds = new VssCredentials(null, new VssOAuthAccessTokenCredential(token), CredentialPromptType.DoNotPrompt);
|
VssCredentials creds = new VssCredentials(new VssOAuthAccessTokenCredential(token), CredentialPromptType.DoNotPrompt);
|
||||||
trace.Info("cred created");
|
trace.Info("cred created");
|
||||||
|
|
||||||
return creds;
|
return creds;
|
||||||
@@ -190,42 +63,4 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
CredentialData.Data[Constants.Runner.CommandLine.Args.Token] = command.GetToken();
|
CredentialData.Data[Constants.Runner.CommandLine.Args.Token] = command.GetToken();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PersonalAccessToken : CredentialProvider
|
|
||||||
{
|
|
||||||
public PersonalAccessToken() : base(Constants.Configuration.PAT) { }
|
|
||||||
|
|
||||||
public override VssCredentials GetVssCredentials(IHostContext context)
|
|
||||||
{
|
|
||||||
ArgUtil.NotNull(context, nameof(context));
|
|
||||||
Tracing trace = context.GetTrace(nameof(PersonalAccessToken));
|
|
||||||
trace.Info(nameof(GetVssCredentials));
|
|
||||||
ArgUtil.NotNull(CredentialData, nameof(CredentialData));
|
|
||||||
string token;
|
|
||||||
if (!CredentialData.Data.TryGetValue(Constants.Runner.CommandLine.Args.Token, out token))
|
|
||||||
{
|
|
||||||
token = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArgUtil.NotNullOrEmpty(token, nameof(token));
|
|
||||||
|
|
||||||
trace.Info("token retrieved: {0} chars", token.Length);
|
|
||||||
|
|
||||||
// PAT uses a basic credential
|
|
||||||
VssBasicCredential basicCred = new VssBasicCredential("ActionsRunner", token);
|
|
||||||
VssCredentials creds = new VssCredentials(null, basicCred, CredentialPromptType.DoNotPrompt);
|
|
||||||
trace.Info("cred created");
|
|
||||||
|
|
||||||
return creds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void EnsureCredential(IHostContext context, CommandSettings command, string serverUrl)
|
|
||||||
{
|
|
||||||
ArgUtil.NotNull(context, nameof(context));
|
|
||||||
Tracing trace = context.GetTrace(nameof(PersonalAccessToken));
|
|
||||||
trace.Info(nameof(EnsureCredential));
|
|
||||||
ArgUtil.NotNull(command, nameof(command));
|
|
||||||
CredentialData.Data[Constants.Runner.CommandLine.Args.Token] = command.GetToken();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
|
|
||||||
// Construct a credentials cache with a single OAuth credential for communication. The windows credential
|
// Construct a credentials cache with a single OAuth credential for communication. The windows credential
|
||||||
// is explicitly set to null to ensure we never do that negotiation.
|
// is explicitly set to null to ensure we never do that negotiation.
|
||||||
return new VssCredentials(null, agentCredential, CredentialPromptType.DoNotPrompt);
|
return new VssCredentials(agentCredential, CredentialPromptType.DoNotPrompt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,25 +38,6 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
return CredentialManager.CredentialTypes.ContainsKey(value);
|
return CredentialManager.CredentialTypes.ContainsKey(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool FilePathValidator(string value)
|
|
||||||
{
|
|
||||||
var directoryInfo = new DirectoryInfo(value);
|
|
||||||
|
|
||||||
if (!directoryInfo.Exists)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(value);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool BoolValidator(string value)
|
public static bool BoolValidator(string value)
|
||||||
{
|
{
|
||||||
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
|
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ namespace GitHub.Runner.Listener
|
|||||||
void Run(Pipelines.AgentJobRequestMessage message, bool runOnce = false);
|
void Run(Pipelines.AgentJobRequestMessage message, bool runOnce = false);
|
||||||
bool Cancel(JobCancelMessage message);
|
bool Cancel(JobCancelMessage message);
|
||||||
Task WaitAsync(CancellationToken token);
|
Task WaitAsync(CancellationToken token);
|
||||||
TaskResult GetLocalRunJobResult(AgentJobRequestMessage message);
|
|
||||||
Task ShutdownAsync();
|
Task ShutdownAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,11 +164,6 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public TaskResult GetLocalRunJobResult(AgentJobRequestMessage message)
|
|
||||||
{
|
|
||||||
return _localRunJobResult.Value[message.RequestId];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ShutdownAsync()
|
public async Task ShutdownAsync()
|
||||||
{
|
{
|
||||||
Trace.Info($"Shutting down JobDispatcher. Make sure all WorkerDispatcher has finished.");
|
Trace.Info($"Shutting down JobDispatcher. Make sure all WorkerDispatcher has finished.");
|
||||||
@@ -373,37 +367,29 @@ namespace GitHub.Runner.Listener
|
|||||||
ArgUtil.NotNullOrEmpty(pipeHandleOut, nameof(pipeHandleOut));
|
ArgUtil.NotNullOrEmpty(pipeHandleOut, nameof(pipeHandleOut));
|
||||||
ArgUtil.NotNullOrEmpty(pipeHandleIn, nameof(pipeHandleIn));
|
ArgUtil.NotNullOrEmpty(pipeHandleIn, nameof(pipeHandleIn));
|
||||||
|
|
||||||
if (HostContext.RunMode == RunMode.Normal)
|
// Save STDOUT from worker, worker will use STDOUT report unhandle exception.
|
||||||
|
processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
||||||
{
|
{
|
||||||
// Save STDOUT from worker, worker will use STDOUT report unhandle exception.
|
if (!string.IsNullOrEmpty(stdout.Data))
|
||||||
processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(stdout.Data))
|
lock (_outputLock)
|
||||||
{
|
{
|
||||||
lock (_outputLock)
|
workerOutput.Add(stdout.Data);
|
||||||
{
|
|
||||||
workerOutput.Add(stdout.Data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Save STDERR from worker, worker will use STDERR on crash.
|
// Save STDERR from worker, worker will use STDERR on crash.
|
||||||
processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(stderr.Data))
|
|
||||||
{
|
|
||||||
lock (_outputLock)
|
|
||||||
{
|
|
||||||
workerOutput.Add(stderr.Data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
{
|
||||||
processInvoker.OutputDataReceived += (object sender, ProcessDataReceivedEventArgs e) => Console.WriteLine(e.Data);
|
if (!string.IsNullOrEmpty(stderr.Data))
|
||||||
processInvoker.ErrorDataReceived += (object sender, ProcessDataReceivedEventArgs e) => Console.WriteLine(e.Data);
|
{
|
||||||
}
|
lock (_outputLock)
|
||||||
|
{
|
||||||
|
workerOutput.Add(stderr.Data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Start the child process.
|
// Start the child process.
|
||||||
HostContext.WritePerfCounter("StartingWorkerProcess");
|
HostContext.WritePerfCounter("StartingWorkerProcess");
|
||||||
@@ -730,11 +716,6 @@ namespace GitHub.Runner.Listener
|
|||||||
private async Task CompleteJobRequestAsync(int poolId, Pipelines.AgentJobRequestMessage message, Guid lockToken, TaskResult result, string detailInfo = null)
|
private async Task CompleteJobRequestAsync(int poolId, Pipelines.AgentJobRequestMessage message, Guid lockToken, TaskResult result, string detailInfo = null)
|
||||||
{
|
{
|
||||||
Trace.Entering();
|
Trace.Entering();
|
||||||
if (HostContext.RunMode == RunMode.Local)
|
|
||||||
{
|
|
||||||
_localRunJobResult.Value[message.RequestId] = result;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PlanUtil.GetFeatures(message.Plan).HasFlag(PlanFeatures.JobCompletedPlanEvent))
|
if (PlanUtil.GetFeatures(message.Plan).HasFlag(PlanFeatures.JobCompletedPlanEvent))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.4.0" />
|
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.4.0" />
|
||||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.4.0" />
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.4.0" />
|
||||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="4.4.0" />
|
<PackageReference Include="System.ServiceProcess.ServiceController" Version="4.4.0" />
|
||||||
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="3.19.4" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var runnerCertManager = HostContext.GetService<IRunnerCertificateManager>();
|
VssUtil.InitializeVssClientSettings(HostContext.UserAgent, HostContext.WebProxy);
|
||||||
VssUtil.InitializeVssClientSettings(HostContext.UserAgent, HostContext.WebProxy, runnerCertManager.VssClientCertificateManager);
|
|
||||||
|
|
||||||
_inConfigStage = true;
|
_inConfigStage = true;
|
||||||
_completedCommand.Reset();
|
_completedCommand.Reset();
|
||||||
|
|||||||
@@ -79,11 +79,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
|||||||
{
|
{
|
||||||
// Validate args.
|
// Validate args.
|
||||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||||
bool useSelfSignedCACert = false;
|
|
||||||
bool useClientCert = false;
|
|
||||||
string clientCertPrivateKeyAskPassFile = null;
|
|
||||||
bool acceptUntrustedCerts = false;
|
|
||||||
|
|
||||||
executionContext.Output($"Syncing repository: {repoFullName}");
|
executionContext.Output($"Syncing repository: {repoFullName}");
|
||||||
Uri repositoryUrl = new Uri($"https://github.com/{repoFullName}");
|
Uri repositoryUrl = new Uri($"https://github.com/{repoFullName}");
|
||||||
if (!repositoryUrl.IsAbsoluteUri)
|
if (!repositoryUrl.IsAbsoluteUri)
|
||||||
@@ -112,9 +107,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var runnerCert = executionContext.GetCertConfiguration();
|
|
||||||
acceptUntrustedCerts = runnerCert?.SkipServerCertificateValidation ?? false;
|
|
||||||
|
|
||||||
executionContext.Debug($"repository url={repositoryUrl}");
|
executionContext.Debug($"repository url={repositoryUrl}");
|
||||||
executionContext.Debug($"targetPath={targetPath}");
|
executionContext.Debug($"targetPath={targetPath}");
|
||||||
executionContext.Debug($"sourceBranch={sourceBranch}");
|
executionContext.Debug($"sourceBranch={sourceBranch}");
|
||||||
@@ -124,12 +116,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
|||||||
executionContext.Debug($"checkoutNestedSubmodules={checkoutNestedSubmodules}");
|
executionContext.Debug($"checkoutNestedSubmodules={checkoutNestedSubmodules}");
|
||||||
executionContext.Debug($"fetchDepth={fetchDepth}");
|
executionContext.Debug($"fetchDepth={fetchDepth}");
|
||||||
executionContext.Debug($"gitLfsSupport={gitLfsSupport}");
|
executionContext.Debug($"gitLfsSupport={gitLfsSupport}");
|
||||||
executionContext.Debug($"acceptUntrustedCerts={acceptUntrustedCerts}");
|
|
||||||
|
|
||||||
#if OS_WINDOWS
|
|
||||||
bool schannelSslBackend = StringUtil.ConvertToBoolean(executionContext.GetRunnerContext("gituseschannel"));
|
|
||||||
executionContext.Debug($"schannelSslBackend={schannelSslBackend}");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Initialize git command manager with additional environment variables.
|
// Initialize git command manager with additional environment variables.
|
||||||
Dictionary<string, string> gitEnv = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
Dictionary<string, string> gitEnv = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -164,54 +150,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
|||||||
|
|
||||||
// prepare askpass for client cert private key, if the repository's endpoint url match the runner config url
|
// prepare askpass for client cert private key, if the repository's endpoint url match the runner config url
|
||||||
var systemConnection = executionContext.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
var systemConnection = executionContext.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||||
if (runnerCert != null && Uri.Compare(repositoryUrl, systemConnection.Url, UriComponents.SchemeAndServer, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) == 0)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(runnerCert.CACertificateFile))
|
|
||||||
{
|
|
||||||
useSelfSignedCACert = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(runnerCert.ClientCertificateFile) &&
|
|
||||||
!string.IsNullOrEmpty(runnerCert.ClientCertificatePrivateKeyFile))
|
|
||||||
{
|
|
||||||
useClientCert = true;
|
|
||||||
|
|
||||||
// prepare askpass for client cert password
|
|
||||||
if (!string.IsNullOrEmpty(runnerCert.ClientCertificatePassword))
|
|
||||||
{
|
|
||||||
clientCertPrivateKeyAskPassFile = Path.Combine(executionContext.GetRunnerContext("temp"), $"{Guid.NewGuid()}.sh");
|
|
||||||
List<string> askPass = new List<string>();
|
|
||||||
askPass.Add("#!/bin/sh");
|
|
||||||
askPass.Add($"echo \"{runnerCert.ClientCertificatePassword}\"");
|
|
||||||
File.WriteAllLines(clientCertPrivateKeyAskPassFile, askPass);
|
|
||||||
|
|
||||||
#if !OS_WINDOWS
|
|
||||||
string toolPath = WhichUtil.Which("chmod", true);
|
|
||||||
string argLine = $"775 {clientCertPrivateKeyAskPassFile}";
|
|
||||||
executionContext.Command($"chmod {argLine}");
|
|
||||||
|
|
||||||
var processInvoker = new ProcessInvoker(executionContext);
|
|
||||||
processInvoker.OutputDataReceived += (object sender, ProcessDataReceivedEventArgs args) =>
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(args.Data))
|
|
||||||
{
|
|
||||||
executionContext.Output(args.Data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
processInvoker.ErrorDataReceived += (object sender, ProcessDataReceivedEventArgs args) =>
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(args.Data))
|
|
||||||
{
|
|
||||||
executionContext.Output(args.Data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
string workingDirectory = executionContext.GetRunnerContext("workspace");
|
|
||||||
await processInvoker.ExecuteAsync(workingDirectory, toolPath, argLine, null, true, CancellationToken.None);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the current contents of the root folder to see if there is already a repo
|
// Check the current contents of the root folder to see if there is already a repo
|
||||||
// If there is a repo, see if it matches the one we are expecting to be there based on the remote fetch url
|
// If there is a repo, see if it matches the one we are expecting to be there based on the remote fetch url
|
||||||
@@ -361,46 +299,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
|||||||
additionalFetchArgs.Add($"-c http.extraheader=\"AUTHORIZATION: {GenerateBasicAuthHeader(executionContext, accessToken)}\"");
|
additionalFetchArgs.Add($"-c http.extraheader=\"AUTHORIZATION: {GenerateBasicAuthHeader(executionContext, accessToken)}\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare ignore ssl cert error config for fetch.
|
|
||||||
if (acceptUntrustedCerts)
|
|
||||||
{
|
|
||||||
additionalFetchArgs.Add($"-c http.sslVerify=false");
|
|
||||||
additionalLfsFetchArgs.Add($"-c http.sslVerify=false");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare self-signed CA cert config for fetch from server.
|
|
||||||
if (useSelfSignedCACert)
|
|
||||||
{
|
|
||||||
executionContext.Debug($"Use self-signed certificate '{runnerCert.CACertificateFile}' for git fetch.");
|
|
||||||
additionalFetchArgs.Add($"-c http.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
|
||||||
additionalLfsFetchArgs.Add($"-c http.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare client cert config for fetch from server.
|
|
||||||
if (useClientCert)
|
|
||||||
{
|
|
||||||
executionContext.Debug($"Use client certificate '{runnerCert.ClientCertificateFile}' for git fetch.");
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
|
||||||
{
|
|
||||||
additionalFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
|
||||||
additionalLfsFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
additionalFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
|
||||||
additionalLfsFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if OS_WINDOWS
|
|
||||||
if (schannelSslBackend)
|
|
||||||
{
|
|
||||||
executionContext.Debug("Use SChannel SslBackend for git fetch.");
|
|
||||||
additionalFetchArgs.Add("-c http.sslbackend=\"schannel\"");
|
|
||||||
additionalLfsFetchArgs.Add("-c http.sslbackend=\"schannel\"");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
// Prepare gitlfs url for fetch and checkout
|
// Prepare gitlfs url for fetch and checkout
|
||||||
if (gitLfsSupport)
|
if (gitLfsSupport)
|
||||||
{
|
{
|
||||||
@@ -502,55 +400,12 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
|||||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.extraheader=\"AUTHORIZATION: {GenerateBasicAuthHeader(executionContext, accessToken)}\"");
|
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.extraheader=\"AUTHORIZATION: {GenerateBasicAuthHeader(executionContext, accessToken)}\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare ignore ssl cert error config for fetch.
|
|
||||||
if (acceptUntrustedCerts)
|
|
||||||
{
|
|
||||||
additionalSubmoduleUpdateArgs.Add($"-c http.sslVerify=false");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare self-signed CA cert config for submodule update.
|
|
||||||
if (useSelfSignedCACert)
|
|
||||||
{
|
|
||||||
executionContext.Debug($"Use self-signed CA certificate '{runnerCert.CACertificateFile}' for git submodule update.");
|
|
||||||
string authorityUrl = repositoryUrl.AbsoluteUri.Replace(repositoryUrl.PathAndQuery, string.Empty);
|
|
||||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare client cert config for submodule update.
|
|
||||||
if (useClientCert)
|
|
||||||
{
|
|
||||||
executionContext.Debug($"Use client certificate '{runnerCert.ClientCertificateFile}' for git submodule update.");
|
|
||||||
string authorityUrl = repositoryUrl.AbsoluteUri.Replace(repositoryUrl.PathAndQuery, string.Empty);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
|
||||||
{
|
|
||||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.{authorityUrl}.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.{authorityUrl}.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.{authorityUrl}.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#if OS_WINDOWS
|
|
||||||
if (schannelSslBackend)
|
|
||||||
{
|
|
||||||
executionContext.Debug("Use SChannel SslBackend for git submodule update.");
|
|
||||||
additionalSubmoduleUpdateArgs.Add("-c http.sslbackend=\"schannel\"");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
int exitCode_submoduleUpdate = await gitCommandManager.GitSubmoduleUpdate(executionContext, targetPath, fetchDepth, string.Join(" ", additionalSubmoduleUpdateArgs), checkoutNestedSubmodules, cancellationToken);
|
int exitCode_submoduleUpdate = await gitCommandManager.GitSubmoduleUpdate(executionContext, targetPath, fetchDepth, string.Join(" ", additionalSubmoduleUpdateArgs), checkoutNestedSubmodules, cancellationToken);
|
||||||
if (exitCode_submoduleUpdate != 0)
|
if (exitCode_submoduleUpdate != 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Git submodule update failed with exit code: {exitCode_submoduleUpdate}");
|
throw new InvalidOperationException($"Git submodule update failed with exit code: {exitCode_submoduleUpdate}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useClientCert && !string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
|
||||||
{
|
|
||||||
executionContext.Debug("Remove git.sslkey askpass file.");
|
|
||||||
IOUtil.DeleteFile(clientCertPrivateKeyAskPassFile);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> IsRepositoryOriginUrlMatch(RunnerActionPluginExecutionContext context, GitCliManager gitCommandManager, string repositoryPath, Uri expectedRepositoryOriginUrl)
|
private async Task<bool> IsRepositoryOriginUrlMatch(RunnerActionPluginExecutionContext context, GitCliManager gitCommandManager, string repositoryPath, Uri expectedRepositoryOriginUrl)
|
||||||
|
|||||||
@@ -65,11 +65,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
|||||||
// Validate args.
|
// Validate args.
|
||||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||||
Dictionary<string, string> configModifications = new Dictionary<string, string>();
|
Dictionary<string, string> configModifications = new Dictionary<string, string>();
|
||||||
bool useSelfSignedCACert = false;
|
|
||||||
bool useClientCert = false;
|
|
||||||
string clientCertPrivateKeyAskPassFile = null;
|
|
||||||
bool acceptUntrustedCerts = false;
|
|
||||||
|
|
||||||
executionContext.Output($"Syncing repository: {repoFullName}");
|
executionContext.Output($"Syncing repository: {repoFullName}");
|
||||||
Uri repositoryUrl = new Uri($"https://github.com/{repoFullName}");
|
Uri repositoryUrl = new Uri($"https://github.com/{repoFullName}");
|
||||||
if (!repositoryUrl.IsAbsoluteUri)
|
if (!repositoryUrl.IsAbsoluteUri)
|
||||||
@@ -98,9 +93,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var runnerCert = executionContext.GetCertConfiguration();
|
|
||||||
acceptUntrustedCerts = runnerCert?.SkipServerCertificateValidation ?? false;
|
|
||||||
|
|
||||||
executionContext.Debug($"repository url={repositoryUrl}");
|
executionContext.Debug($"repository url={repositoryUrl}");
|
||||||
executionContext.Debug($"targetPath={targetPath}");
|
executionContext.Debug($"targetPath={targetPath}");
|
||||||
executionContext.Debug($"sourceBranch={sourceBranch}");
|
executionContext.Debug($"sourceBranch={sourceBranch}");
|
||||||
@@ -110,12 +102,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
|||||||
executionContext.Debug($"checkoutNestedSubmodules={checkoutNestedSubmodules}");
|
executionContext.Debug($"checkoutNestedSubmodules={checkoutNestedSubmodules}");
|
||||||
executionContext.Debug($"fetchDepth={fetchDepth}");
|
executionContext.Debug($"fetchDepth={fetchDepth}");
|
||||||
executionContext.Debug($"gitLfsSupport={gitLfsSupport}");
|
executionContext.Debug($"gitLfsSupport={gitLfsSupport}");
|
||||||
executionContext.Debug($"acceptUntrustedCerts={acceptUntrustedCerts}");
|
|
||||||
|
|
||||||
#if OS_WINDOWS
|
|
||||||
bool schannelSslBackend = StringUtil.ConvertToBoolean(executionContext.GetRunnerContext("gituseschannel"));
|
|
||||||
executionContext.Debug($"schannelSslBackend={schannelSslBackend}");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Initialize git command manager with additional environment variables.
|
// Initialize git command manager with additional environment variables.
|
||||||
Dictionary<string, string> gitEnv = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
Dictionary<string, string> gitEnv = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -153,54 +139,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
|||||||
|
|
||||||
// prepare askpass for client cert private key, if the repository's endpoint url match the runner config url
|
// prepare askpass for client cert private key, if the repository's endpoint url match the runner config url
|
||||||
var systemConnection = executionContext.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
var systemConnection = executionContext.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||||
if (runnerCert != null && Uri.Compare(repositoryUrl, systemConnection.Url, UriComponents.SchemeAndServer, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) == 0)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(runnerCert.CACertificateFile))
|
|
||||||
{
|
|
||||||
useSelfSignedCACert = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(runnerCert.ClientCertificateFile) &&
|
|
||||||
!string.IsNullOrEmpty(runnerCert.ClientCertificatePrivateKeyFile))
|
|
||||||
{
|
|
||||||
useClientCert = true;
|
|
||||||
|
|
||||||
// prepare askpass for client cert password
|
|
||||||
if (!string.IsNullOrEmpty(runnerCert.ClientCertificatePassword))
|
|
||||||
{
|
|
||||||
clientCertPrivateKeyAskPassFile = Path.Combine(executionContext.GetRunnerContext("temp"), $"{Guid.NewGuid()}.sh");
|
|
||||||
List<string> askPass = new List<string>();
|
|
||||||
askPass.Add("#!/bin/sh");
|
|
||||||
askPass.Add($"echo \"{runnerCert.ClientCertificatePassword}\"");
|
|
||||||
File.WriteAllLines(clientCertPrivateKeyAskPassFile, askPass);
|
|
||||||
|
|
||||||
#if !OS_WINDOWS
|
|
||||||
string toolPath = WhichUtil.Which("chmod", true);
|
|
||||||
string argLine = $"775 {clientCertPrivateKeyAskPassFile}";
|
|
||||||
executionContext.Command($"chmod {argLine}");
|
|
||||||
|
|
||||||
var processInvoker = new ProcessInvoker(executionContext);
|
|
||||||
processInvoker.OutputDataReceived += (object sender, ProcessDataReceivedEventArgs args) =>
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(args.Data))
|
|
||||||
{
|
|
||||||
executionContext.Output(args.Data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
processInvoker.ErrorDataReceived += (object sender, ProcessDataReceivedEventArgs args) =>
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(args.Data))
|
|
||||||
{
|
|
||||||
executionContext.Output(args.Data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
string workingDirectory = executionContext.GetRunnerContext("workspace");
|
|
||||||
await processInvoker.ExecuteAsync(workingDirectory, toolPath, argLine, null, true, CancellationToken.None);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the current contents of the root folder to see if there is already a repo
|
// Check the current contents of the root folder to see if there is already a repo
|
||||||
// If there is a repo, see if it matches the one we are expecting to be there based on the remote fetch url
|
// If there is a repo, see if it matches the one we are expecting to be there based on the remote fetch url
|
||||||
@@ -355,46 +293,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
|||||||
throw new InvalidOperationException($"Git config failed with exit code: {exitCode_config}");
|
throw new InvalidOperationException($"Git config failed with exit code: {exitCode_config}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare ignore ssl cert error config for fetch.
|
|
||||||
if (acceptUntrustedCerts)
|
|
||||||
{
|
|
||||||
additionalFetchArgs.Add($"-c http.sslVerify=false");
|
|
||||||
additionalLfsFetchArgs.Add($"-c http.sslVerify=false");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare self-signed CA cert config for fetch from server.
|
|
||||||
if (useSelfSignedCACert)
|
|
||||||
{
|
|
||||||
executionContext.Debug($"Use self-signed certificate '{runnerCert.CACertificateFile}' for git fetch.");
|
|
||||||
additionalFetchArgs.Add($"-c http.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
|
||||||
additionalLfsFetchArgs.Add($"-c http.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare client cert config for fetch from server.
|
|
||||||
if (useClientCert)
|
|
||||||
{
|
|
||||||
executionContext.Debug($"Use client certificate '{runnerCert.ClientCertificateFile}' for git fetch.");
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
|
||||||
{
|
|
||||||
additionalFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
|
||||||
additionalLfsFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
additionalFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
|
||||||
additionalLfsFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if OS_WINDOWS
|
|
||||||
if (schannelSslBackend)
|
|
||||||
{
|
|
||||||
executionContext.Debug("Use SChannel SslBackend for git fetch.");
|
|
||||||
additionalFetchArgs.Add("-c http.sslbackend=\"schannel\"");
|
|
||||||
additionalLfsFetchArgs.Add("-c http.sslbackend=\"schannel\"");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
// Prepare gitlfs url for fetch and checkout
|
// Prepare gitlfs url for fetch and checkout
|
||||||
if (gitLfsSupport)
|
if (gitLfsSupport)
|
||||||
{
|
{
|
||||||
@@ -484,43 +382,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
|||||||
|
|
||||||
List<string> additionalSubmoduleUpdateArgs = new List<string>();
|
List<string> additionalSubmoduleUpdateArgs = new List<string>();
|
||||||
|
|
||||||
// Prepare ignore ssl cert error config for fetch.
|
|
||||||
if (acceptUntrustedCerts)
|
|
||||||
{
|
|
||||||
additionalSubmoduleUpdateArgs.Add($"-c http.sslVerify=false");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare self-signed CA cert config for submodule update.
|
|
||||||
if (useSelfSignedCACert)
|
|
||||||
{
|
|
||||||
executionContext.Debug($"Use self-signed CA certificate '{runnerCert.CACertificateFile}' for git submodule update.");
|
|
||||||
string authorityUrl = repositoryUrl.AbsoluteUri.Replace(repositoryUrl.PathAndQuery, string.Empty);
|
|
||||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare client cert config for submodule update.
|
|
||||||
if (useClientCert)
|
|
||||||
{
|
|
||||||
executionContext.Debug($"Use client certificate '{runnerCert.ClientCertificateFile}' for git submodule update.");
|
|
||||||
string authorityUrl = repositoryUrl.AbsoluteUri.Replace(repositoryUrl.PathAndQuery, string.Empty);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
|
||||||
{
|
|
||||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.{authorityUrl}.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.{authorityUrl}.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.{authorityUrl}.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#if OS_WINDOWS
|
|
||||||
if (schannelSslBackend)
|
|
||||||
{
|
|
||||||
executionContext.Debug("Use SChannel SslBackend for git submodule update.");
|
|
||||||
additionalSubmoduleUpdateArgs.Add("-c http.sslbackend=\"schannel\"");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
int exitCode_submoduleUpdate = await gitCommandManager.GitSubmoduleUpdate(executionContext, targetPath, fetchDepth, string.Join(" ", additionalSubmoduleUpdateArgs), checkoutNestedSubmodules, cancellationToken);
|
int exitCode_submoduleUpdate = await gitCommandManager.GitSubmoduleUpdate(executionContext, targetPath, fetchDepth, string.Join(" ", additionalSubmoduleUpdateArgs), checkoutNestedSubmodules, cancellationToken);
|
||||||
if (exitCode_submoduleUpdate != 0)
|
if (exitCode_submoduleUpdate != 0)
|
||||||
{
|
{
|
||||||
@@ -528,12 +389,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useClientCert && !string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
|
||||||
{
|
|
||||||
executionContext.Debug("Remove git.sslkey askpass file.");
|
|
||||||
IOUtil.DeleteFile(clientCertPrivateKeyAskPassFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set intra-task variable for post job cleanup
|
// Set intra-task variable for post job cleanup
|
||||||
executionContext.SetIntraActionState("repositoryPath", targetPath);
|
executionContext.SetIntraActionState("repositoryPath", targetPath);
|
||||||
executionContext.SetIntraActionState("modifiedgitconfig", JsonUtility.ToString(configModifications.Keys));
|
executionContext.SetIntraActionState("modifiedgitconfig", JsonUtility.ToString(configModifications.Keys));
|
||||||
|
|||||||
@@ -83,21 +83,6 @@ namespace GitHub.Runner.Sdk
|
|||||||
}
|
}
|
||||||
|
|
||||||
VssClientHttpRequestSettings.Default.UserAgent = headerValues;
|
VssClientHttpRequestSettings.Default.UserAgent = headerValues;
|
||||||
|
|
||||||
var certSetting = GetCertConfiguration();
|
|
||||||
if (certSetting != null)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(certSetting.ClientCertificateArchiveFile))
|
|
||||||
{
|
|
||||||
VssClientHttpRequestSettings.Default.ClientCertificateManager = new RunnerClientCertificateManager(certSetting.ClientCertificateArchiveFile, certSetting.ClientCertificatePassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (certSetting.SkipServerCertificateValidation)
|
|
||||||
{
|
|
||||||
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VssHttpMessageHandler.DefaultWebProxy = this.WebProxy;
|
VssHttpMessageHandler.DefaultWebProxy = this.WebProxy;
|
||||||
ServiceEndpoint systemConnection = this.Endpoints.FirstOrDefault(e => string.Equals(e.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
ServiceEndpoint systemConnection = this.Endpoints.FirstOrDefault(e => string.Equals(e.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||||
ArgUtil.NotNull(systemConnection, nameof(systemConnection));
|
ArgUtil.NotNull(systemConnection, nameof(systemConnection));
|
||||||
@@ -227,40 +212,6 @@ namespace GitHub.Runner.Sdk
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public RunnerCertificateSettings GetCertConfiguration()
|
|
||||||
{
|
|
||||||
bool skipCertValidation = StringUtil.ConvertToBoolean(GetRunnerContext("SkipCertValidation"));
|
|
||||||
string caFile = GetRunnerContext("CAInfo");
|
|
||||||
string clientCertFile = GetRunnerContext("ClientCert");
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(caFile) || !string.IsNullOrEmpty(clientCertFile) || skipCertValidation)
|
|
||||||
{
|
|
||||||
var certConfig = new RunnerCertificateSettings();
|
|
||||||
certConfig.SkipServerCertificateValidation = skipCertValidation;
|
|
||||||
certConfig.CACertificateFile = caFile;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(clientCertFile))
|
|
||||||
{
|
|
||||||
certConfig.ClientCertificateFile = clientCertFile;
|
|
||||||
string clientCertKey = GetRunnerContext("ClientCertKey");
|
|
||||||
string clientCertArchive = GetRunnerContext("ClientCertArchive");
|
|
||||||
string clientCertPassword = GetRunnerContext("ClientCertPassword");
|
|
||||||
|
|
||||||
certConfig.ClientCertificatePrivateKeyFile = clientCertKey;
|
|
||||||
certConfig.ClientCertificateArchiveFile = clientCertArchive;
|
|
||||||
certConfig.ClientCertificatePassword = clientCertPassword;
|
|
||||||
|
|
||||||
certConfig.VssClientCertificateManager = new RunnerClientCertificateManager(clientCertArchive, clientCertPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
return certConfig;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string Escape(string input)
|
private string Escape(string input)
|
||||||
{
|
{
|
||||||
foreach (var mapping in _commandEscapeMappings)
|
foreach (var mapping in _commandEscapeMappings)
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Sdk
|
|
||||||
{
|
|
||||||
public class RunnerCertificateSettings
|
|
||||||
{
|
|
||||||
public bool SkipServerCertificateValidation { get; set; }
|
|
||||||
public string CACertificateFile { get; set; }
|
|
||||||
public string ClientCertificateFile { get; set; }
|
|
||||||
public string ClientCertificatePrivateKeyFile { get; set; }
|
|
||||||
public string ClientCertificateArchiveFile { get; set; }
|
|
||||||
public string ClientCertificatePassword { get; set; }
|
|
||||||
public IVssClientCertificateManager VssClientCertificateManager { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RunnerClientCertificateManager : IVssClientCertificateManager
|
|
||||||
{
|
|
||||||
private readonly X509Certificate2Collection _clientCertificates = new X509Certificate2Collection();
|
|
||||||
public X509Certificate2Collection ClientCertificates => _clientCertificates;
|
|
||||||
|
|
||||||
public RunnerClientCertificateManager()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public RunnerClientCertificateManager(string clientCertificateArchiveFile, string clientCertificatePassword)
|
|
||||||
{
|
|
||||||
AddClientCertificate(clientCertificateArchiveFile, clientCertificatePassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddClientCertificate(string clientCertificateArchiveFile, string clientCertificatePassword)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(clientCertificateArchiveFile))
|
|
||||||
{
|
|
||||||
_clientCertificates.Add(new X509Certificate2(clientCertificateArchiveFile, clientCertificatePassword));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,7 @@ namespace GitHub.Runner.Sdk
|
|||||||
{
|
{
|
||||||
public static class VssUtil
|
public static class VssUtil
|
||||||
{
|
{
|
||||||
public static void InitializeVssClientSettings(ProductInfoHeaderValue additionalUserAgent, IWebProxy proxy, IVssClientCertificateManager clientCert)
|
public static void InitializeVssClientSettings(ProductInfoHeaderValue additionalUserAgent, IWebProxy proxy)
|
||||||
{
|
{
|
||||||
var headerValues = new List<ProductInfoHeaderValue>();
|
var headerValues = new List<ProductInfoHeaderValue>();
|
||||||
headerValues.Add(additionalUserAgent);
|
headerValues.Add(additionalUserAgent);
|
||||||
@@ -26,7 +26,6 @@ namespace GitHub.Runner.Sdk
|
|||||||
}
|
}
|
||||||
|
|
||||||
VssClientHttpRequestSettings.Default.UserAgent = headerValues;
|
VssClientHttpRequestSettings.Default.UserAgent = headerValues;
|
||||||
VssClientHttpRequestSettings.Default.ClientCertificateManager = clientCert;
|
|
||||||
VssHttpMessageHandler.DefaultWebProxy = proxy;
|
VssHttpMessageHandler.DefaultWebProxy = proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +82,7 @@ namespace GitHub.Runner.Sdk
|
|||||||
if (serviceEndpoint.Authorization.Scheme == EndpointAuthorizationSchemes.OAuth &&
|
if (serviceEndpoint.Authorization.Scheme == EndpointAuthorizationSchemes.OAuth &&
|
||||||
serviceEndpoint.Authorization.Parameters.TryGetValue(EndpointAuthorizationParameters.AccessToken, out accessToken))
|
serviceEndpoint.Authorization.Parameters.TryGetValue(EndpointAuthorizationParameters.AccessToken, out accessToken))
|
||||||
{
|
{
|
||||||
credentials = new VssCredentials(null, new VssOAuthAccessTokenCredential(accessToken), CredentialPromptType.DoNotPrompt);
|
credentials = new VssCredentials(new VssOAuthAccessTokenCredential(accessToken), CredentialPromptType.DoNotPrompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
return credentials;
|
return credentials;
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ namespace GitHub.Runner.Worker
|
|||||||
TaskResult? CommandResult { get; set; }
|
TaskResult? CommandResult { get; set; }
|
||||||
CancellationToken CancellationToken { get; }
|
CancellationToken CancellationToken { get; }
|
||||||
List<ServiceEndpoint> Endpoints { get; }
|
List<ServiceEndpoint> Endpoints { get; }
|
||||||
List<SecureFile> SecureFiles { get; }
|
|
||||||
|
|
||||||
PlanFeatures Features { get; }
|
PlanFeatures Features { get; }
|
||||||
Variables Variables { get; }
|
Variables Variables { get; }
|
||||||
@@ -136,7 +135,6 @@ namespace GitHub.Runner.Worker
|
|||||||
public Task ForceCompleted => _forceCompleted.Task;
|
public Task ForceCompleted => _forceCompleted.Task;
|
||||||
public CancellationToken CancellationToken => _cancellationTokenSource.Token;
|
public CancellationToken CancellationToken => _cancellationTokenSource.Token;
|
||||||
public List<ServiceEndpoint> Endpoints { get; private set; }
|
public List<ServiceEndpoint> Endpoints { get; private set; }
|
||||||
public List<SecureFile> SecureFiles { get; private set; }
|
|
||||||
public Variables Variables { get; private set; }
|
public Variables Variables { get; private set; }
|
||||||
public Dictionary<string, string> IntraActionState { get; private set; }
|
public Dictionary<string, string> IntraActionState { get; private set; }
|
||||||
public HashSet<string> OutputVariables => _outputvariables;
|
public HashSet<string> OutputVariables => _outputvariables;
|
||||||
@@ -257,7 +255,6 @@ namespace GitHub.Runner.Worker
|
|||||||
child.Features = Features;
|
child.Features = Features;
|
||||||
child.Variables = Variables;
|
child.Variables = Variables;
|
||||||
child.Endpoints = Endpoints;
|
child.Endpoints = Endpoints;
|
||||||
child.SecureFiles = SecureFiles;
|
|
||||||
if (intraActionState == null)
|
if (intraActionState == null)
|
||||||
{
|
{
|
||||||
child.IntraActionState = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
child.IntraActionState = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -549,9 +546,6 @@ namespace GitHub.Runner.Worker
|
|||||||
// Endpoints
|
// Endpoints
|
||||||
Endpoints = message.Resources.Endpoints;
|
Endpoints = message.Resources.Endpoints;
|
||||||
|
|
||||||
// SecureFiles
|
|
||||||
SecureFiles = message.Resources.SecureFiles;
|
|
||||||
|
|
||||||
// Variables
|
// Variables
|
||||||
Variables = new Variables(HostContext, message.Variables);
|
Variables = new Variables(HostContext, message.Variables);
|
||||||
|
|
||||||
@@ -616,44 +610,6 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
// PostJobSteps for job ExecutionContext
|
// PostJobSteps for job ExecutionContext
|
||||||
PostJobSteps = new Stack<IStep>();
|
PostJobSteps = new Stack<IStep>();
|
||||||
// // Certificate variables
|
|
||||||
// var agentCert = HostContext.GetService<IRunnerCertificateManager>();
|
|
||||||
// if (agentCert.SkipServerCertificateValidation)
|
|
||||||
// {
|
|
||||||
// SetRunnerContext("sslskipcertvalidation", bool.TrueString);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (!string.IsNullOrEmpty(agentCert.CACertificateFile))
|
|
||||||
// {
|
|
||||||
// SetRunnerContext("sslcainfo", agentCert.CACertificateFile);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (!string.IsNullOrEmpty(agentCert.ClientCertificateFile) &&
|
|
||||||
// !string.IsNullOrEmpty(agentCert.ClientCertificatePrivateKeyFile) &&
|
|
||||||
// !string.IsNullOrEmpty(agentCert.ClientCertificateArchiveFile))
|
|
||||||
// {
|
|
||||||
// SetRunnerContext("clientcertfile", agentCert.ClientCertificateFile);
|
|
||||||
// SetRunnerContext("clientcertprivatekey", agentCert.ClientCertificatePrivateKeyFile);
|
|
||||||
// SetRunnerContext("clientcertarchive", agentCert.ClientCertificateArchiveFile);
|
|
||||||
|
|
||||||
// if (!string.IsNullOrEmpty(agentCert.ClientCertificatePassword))
|
|
||||||
// {
|
|
||||||
// HostContext.SecretMasker.AddValue(agentCert.ClientCertificatePassword);
|
|
||||||
// SetRunnerContext("clientcertpassword", agentCert.ClientCertificatePassword);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Runtime option variables
|
|
||||||
// var runtimeOptions = HostContext.GetService<IConfigurationStore>().GetRunnerRuntimeOptions();
|
|
||||||
// if (runtimeOptions != null)
|
|
||||||
// {
|
|
||||||
// #if OS_WINDOWS
|
|
||||||
// if (runtimeOptions.GitUseSecureChannel)
|
|
||||||
// {
|
|
||||||
// SetRunnerContext("gituseschannel", runtimeOptions.GitUseSecureChannel.ToString());
|
|
||||||
// }
|
|
||||||
// #endif
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Job timeline record.
|
// Job timeline record.
|
||||||
InitializeTimelineRecord(
|
InitializeTimelineRecord(
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ namespace GitHub.Runner.Worker
|
|||||||
// Validate args.
|
// Validate args.
|
||||||
ArgUtil.NotNullOrEmpty(pipeIn, nameof(pipeIn));
|
ArgUtil.NotNullOrEmpty(pipeIn, nameof(pipeIn));
|
||||||
ArgUtil.NotNullOrEmpty(pipeOut, nameof(pipeOut));
|
ArgUtil.NotNullOrEmpty(pipeOut, nameof(pipeOut));
|
||||||
var runnerCertManager = HostContext.GetService<IRunnerCertificateManager>();
|
VssUtil.InitializeVssClientSettings(HostContext.UserAgent, HostContext.WebProxy);
|
||||||
VssUtil.InitializeVssClientSettings(HostContext.UserAgent, HostContext.WebProxy, runnerCertManager.VssClientCertificateManager);
|
|
||||||
var jobRunner = HostContext.CreateService<IJobRunner>();
|
var jobRunner = HostContext.CreateService<IJobRunner>();
|
||||||
|
|
||||||
using (var channel = HostContext.CreateService<IProcessChannel>())
|
using (var channel = HostContext.CreateService<IProcessChannel>())
|
||||||
@@ -178,15 +177,6 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add masks for secure file download tickets
|
|
||||||
foreach (SecureFile file in message.Resources.SecureFiles ?? new List<SecureFile>())
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(file.Ticket))
|
|
||||||
{
|
|
||||||
HostContext.SecretMasker.AddValue(file.Ticket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetCulture(Pipelines.AgentJobRequestMessage message)
|
private void SetCulture(Pipelines.AgentJobRequestMessage message)
|
||||||
|
|||||||
@@ -1,264 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Client
|
|
||||||
{
|
|
||||||
internal static class CookieUtility
|
|
||||||
{
|
|
||||||
public static readonly String AcsMetadataRetrievalExceptionText = "Unable to retrieve ACS Metadata from '{0}'";
|
|
||||||
public static readonly String FedAuthCookieName = "FedAuth";
|
|
||||||
public static readonly String WindowsLiveSignOutUrl = "https://login.live.com/uilogout.srf";
|
|
||||||
public static readonly Uri WindowsLiveCookieDomain = new Uri("https://login.live.com/");
|
|
||||||
|
|
||||||
public static CookieCollection GetFederatedCookies(Uri cookieDomainAndPath)
|
|
||||||
{
|
|
||||||
CookieCollection result = null;
|
|
||||||
|
|
||||||
Cookie cookie = GetCookieEx(cookieDomainAndPath, FedAuthCookieName).FirstOrDefault();
|
|
||||||
|
|
||||||
if (cookie != null)
|
|
||||||
{
|
|
||||||
result = new CookieCollection();
|
|
||||||
result.Add(cookie);
|
|
||||||
|
|
||||||
for (Int32 x = 1; x < 50; x++)
|
|
||||||
{
|
|
||||||
String cookieName = FedAuthCookieName + x;
|
|
||||||
cookie = GetCookieEx(cookieDomainAndPath, cookieName).FirstOrDefault();
|
|
||||||
|
|
||||||
if (cookie != null)
|
|
||||||
{
|
|
||||||
result.Add(cookie);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CookieCollection GetFederatedCookies(String[] token)
|
|
||||||
{
|
|
||||||
CookieCollection result = null;
|
|
||||||
|
|
||||||
if (token != null && token.Length > 0 && token[0] != null)
|
|
||||||
{
|
|
||||||
result = new CookieCollection();
|
|
||||||
result.Add(new Cookie(FedAuthCookieName, token[0]));
|
|
||||||
|
|
||||||
for (Int32 x = 1; x < token.Length; x++)
|
|
||||||
{
|
|
||||||
String cookieName = FedAuthCookieName + x;
|
|
||||||
|
|
||||||
if (token[x] != null)
|
|
||||||
{
|
|
||||||
Cookie cookie = new Cookie(cookieName, token[x]);
|
|
||||||
cookie.HttpOnly = true;
|
|
||||||
result.Add(cookie);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CookieCollection GetFederatedCookies(IHttpResponse webResponse)
|
|
||||||
{
|
|
||||||
CookieCollection result = null;
|
|
||||||
IEnumerable<String> cookies = null;
|
|
||||||
|
|
||||||
if (webResponse.Headers.TryGetValues("Set-Cookie", out cookies))
|
|
||||||
{
|
|
||||||
foreach (String cookie in cookies)
|
|
||||||
{
|
|
||||||
if (cookie != null && cookie.StartsWith(CookieUtility.FedAuthCookieName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
// Only take the security token field of the cookie, and discard the rest
|
|
||||||
String fedAuthToken = cookie.Split(';').FirstOrDefault();
|
|
||||||
Int32 index = fedAuthToken.IndexOf('=');
|
|
||||||
|
|
||||||
if (index > 0 && index < fedAuthToken.Length - 1)
|
|
||||||
{
|
|
||||||
String name = fedAuthToken.Substring(0, index);
|
|
||||||
String value = fedAuthToken.Substring(index + 1);
|
|
||||||
|
|
||||||
result = result ?? new CookieCollection();
|
|
||||||
result.Add(new Cookie(name, value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CookieCollection GetAllCookies(Uri cookieDomainAndPath)
|
|
||||||
{
|
|
||||||
CookieCollection result = null;
|
|
||||||
List<Cookie> cookies = GetCookieEx(cookieDomainAndPath, null);
|
|
||||||
foreach (Cookie cookie in cookies)
|
|
||||||
{
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
result = new CookieCollection();
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Add(cookie);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void DeleteFederatedCookies(Uri cookieDomainAndPath)
|
|
||||||
{
|
|
||||||
CookieCollection cookies = GetFederatedCookies(cookieDomainAndPath);
|
|
||||||
|
|
||||||
if (cookies != null)
|
|
||||||
{
|
|
||||||
foreach (Cookie cookie in cookies)
|
|
||||||
{
|
|
||||||
DeleteCookieEx(cookieDomainAndPath, cookie.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void DeleteWindowsLiveCookies()
|
|
||||||
{
|
|
||||||
DeleteAllCookies(WindowsLiveCookieDomain);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void DeleteAllCookies(Uri cookieDomainAndPath)
|
|
||||||
{
|
|
||||||
CookieCollection cookies = GetAllCookies(cookieDomainAndPath);
|
|
||||||
|
|
||||||
if (cookies != null)
|
|
||||||
{
|
|
||||||
foreach (Cookie cookie in cookies)
|
|
||||||
{
|
|
||||||
DeleteCookieEx(cookieDomainAndPath, cookie.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public const UInt32 INTERNET_COOKIE_HTTPONLY = 0x00002000;
|
|
||||||
|
|
||||||
[DllImport("wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
||||||
static extern bool InternetGetCookieEx(
|
|
||||||
String url, String cookieName, StringBuilder cookieData, ref Int32 size, UInt32 flags, IntPtr reserved);
|
|
||||||
|
|
||||||
[DllImport("wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
||||||
static extern bool InternetSetCookieEx(
|
|
||||||
String url, String cookieName, String cookieData, UInt32 flags, IntPtr reserved);
|
|
||||||
|
|
||||||
public static Boolean DeleteCookieEx(Uri cookiePath, String cookieName)
|
|
||||||
{
|
|
||||||
UInt32 flags = INTERNET_COOKIE_HTTPONLY;
|
|
||||||
|
|
||||||
String path = cookiePath.ToString();
|
|
||||||
if (!path.EndsWith("/", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
path = path + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime expiration = DateTime.UtcNow.AddYears(-1);
|
|
||||||
String cookieData = String.Format(CultureInfo.InvariantCulture, "{0}=0;expires={1};path=/;domain={2};httponly", cookieName, expiration.ToString("R"), cookiePath.Host);
|
|
||||||
|
|
||||||
return InternetSetCookieEx(path, null, cookieData, flags, IntPtr.Zero);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Boolean SetCookiesEx(
|
|
||||||
Uri cookiePath,
|
|
||||||
CookieCollection cookies)
|
|
||||||
{
|
|
||||||
String path = cookiePath.ToString();
|
|
||||||
if (!path.EndsWith("/", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
path = path + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
Boolean successful = true;
|
|
||||||
foreach (Cookie cookie in cookies)
|
|
||||||
{
|
|
||||||
// This means it doesn't expire
|
|
||||||
if (cookie.Expires.Year == 1)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String cookieData = String.Format(CultureInfo.InvariantCulture,
|
|
||||||
"{0}; path={1}; domain={2}; expires={3}; httponly",
|
|
||||||
cookie.Value,
|
|
||||||
cookie.Path,
|
|
||||||
cookie.Domain,
|
|
||||||
cookie.Expires.ToString("ddd, dd-MMM-yyyy HH:mm:ss 'GMT'"));
|
|
||||||
|
|
||||||
successful &= InternetSetCookieEx(path, cookie.Name, cookieData, INTERNET_COOKIE_HTTPONLY, IntPtr.Zero);
|
|
||||||
}
|
|
||||||
return successful;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<Cookie> GetCookieEx(Uri cookiePath, String cookieName)
|
|
||||||
{
|
|
||||||
UInt32 flags = INTERNET_COOKIE_HTTPONLY;
|
|
||||||
|
|
||||||
List<Cookie> cookies = new List<Cookie>();
|
|
||||||
Int32 size = 256;
|
|
||||||
StringBuilder cookieData = new StringBuilder(size);
|
|
||||||
String path = cookiePath.ToString();
|
|
||||||
if (!path.EndsWith("/", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
path = path + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!InternetGetCookieEx(path, cookieName, cookieData, ref size, flags, IntPtr.Zero))
|
|
||||||
{
|
|
||||||
if (size < 0)
|
|
||||||
{
|
|
||||||
return cookies;
|
|
||||||
}
|
|
||||||
|
|
||||||
cookieData = new StringBuilder(size);
|
|
||||||
|
|
||||||
if (!InternetGetCookieEx(path, cookieName, cookieData, ref size, flags, IntPtr.Zero))
|
|
||||||
{
|
|
||||||
return cookies;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cookieData.Length > 0)
|
|
||||||
{
|
|
||||||
String[] cookieSections = cookieData.ToString().Split(new char[] { ';' });
|
|
||||||
|
|
||||||
foreach (String cookieSection in cookieSections)
|
|
||||||
{
|
|
||||||
String[] cookieParts = cookieSection.Split(new char[] { '=' }, 2);
|
|
||||||
|
|
||||||
if (cookieParts.Length == 2)
|
|
||||||
{
|
|
||||||
Cookie cookie = new Cookie();
|
|
||||||
cookie.Name = cookieParts[0].TrimStart();
|
|
||||||
cookie.Value = cookieParts[1];
|
|
||||||
cookie.HttpOnly = true;
|
|
||||||
cookies.Add(cookie);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cookies;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Security;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Client
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Currently it is impossible to get whether prompting is allowed from the credential itself without reproducing the logic
|
|
||||||
/// used by VssClientCredentials. Since this is a stop gap solution to get Windows integrated authentication to work against
|
|
||||||
/// AAD via ADFS for now this class will only support that one, non-interactive flow. We need to assess how much we want to
|
|
||||||
/// invest in this legacy stack rather than recommending people move to the VssConnect API for future authentication needs.
|
|
||||||
/// </summary>
|
|
||||||
[Serializable]
|
|
||||||
public sealed class VssAadCredential : FederatedCredential
|
|
||||||
{
|
|
||||||
private string username;
|
|
||||||
private SecureString password;
|
|
||||||
|
|
||||||
public VssAadCredential()
|
|
||||||
: base(null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public VssAadCredential(VssAadToken initialToken)
|
|
||||||
: base(initialToken)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public VssAadCredential(string username)
|
|
||||||
: base(null)
|
|
||||||
{
|
|
||||||
this.username = username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public VssAadCredential(string username, string password)
|
|
||||||
: base(null)
|
|
||||||
{
|
|
||||||
this.username = username;
|
|
||||||
|
|
||||||
if (password != null)
|
|
||||||
{
|
|
||||||
this.password = new SecureString();
|
|
||||||
|
|
||||||
foreach (char character in password)
|
|
||||||
{
|
|
||||||
this.password.AppendChar(character);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public VssAadCredential(string username, SecureString password)
|
|
||||||
: base(null)
|
|
||||||
{
|
|
||||||
this.username = username;
|
|
||||||
this.password = password;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override VssCredentialsType CredentialType
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return VssCredentialsType.Aad;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal string Username
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return username;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal SecureString Password => password;
|
|
||||||
|
|
||||||
public override bool IsAuthenticationChallenge(IHttpResponse webResponse)
|
|
||||||
{
|
|
||||||
bool isNonAuthenticationChallenge = false;
|
|
||||||
return VssFederatedCredential.IsVssFederatedAuthenticationChallenge(webResponse, out isNonAuthenticationChallenge) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IssuedTokenProvider OnCreateTokenProvider(
|
|
||||||
Uri serverUrl,
|
|
||||||
IHttpResponse response)
|
|
||||||
{
|
|
||||||
if (response == null && base.InitialToken == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new VssAadTokenProvider(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
using GitHub.Services.WebApi.Internal;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Client
|
|
||||||
{
|
|
||||||
internal static class VssAadSettings
|
|
||||||
{
|
|
||||||
public const string DefaultAadInstance = "https://login.microsoftonline.com/";
|
|
||||||
|
|
||||||
public const string CommonTenant = "common";
|
|
||||||
|
|
||||||
// VSTS service principal.
|
|
||||||
public const string Resource = "499b84ac-1321-427f-aa17-267ca6975798";
|
|
||||||
|
|
||||||
// Visual Studio IDE client ID originally provisioned by Azure Tools.
|
|
||||||
public const string Client = "872cd9fa-d31f-45e0-9eab-6e460a02d1f1";
|
|
||||||
|
|
||||||
// AAD Production Application tenant.
|
|
||||||
private const string ApplicationTenantId = "f8cdef31-a31e-4b4a-93e4-5f571e91255a";
|
|
||||||
|
|
||||||
#if !NETSTANDARD
|
|
||||||
public static Uri NativeClientRedirectUri
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
Uri nativeClientRedirect = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string nativeRedirect = VssClientEnvironment.GetSharedConnectedUserValue<string>(VssConnectionParameterOverrideKeys.AadNativeClientRedirect);
|
|
||||||
if (!string.IsNullOrEmpty(nativeRedirect))
|
|
||||||
{
|
|
||||||
Uri.TryCreate(nativeRedirect, UriKind.RelativeOrAbsolute, out nativeClientRedirect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Debug.WriteLine(string.Format("NativeClientRedirectUri: {0}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
return nativeClientRedirect ?? new Uri("urn:ietf:wg:oauth:2.0:oob");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ClientId
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
string nativeRedirect = VssClientEnvironment.GetSharedConnectedUserValue<string>(VssConnectionParameterOverrideKeys.AadNativeClientIdentifier);
|
|
||||||
return nativeRedirect ?? VssAadSettings.Client;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public static string AadInstance
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
#if !NETSTANDARD
|
|
||||||
string aadInstance = VssClientEnvironment.GetSharedConnectedUserValue<string>(VssConnectionParameterOverrideKeys.AadInstance);
|
|
||||||
#else
|
|
||||||
string aadInstance = null;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(aadInstance))
|
|
||||||
{
|
|
||||||
aadInstance = DefaultAadInstance;
|
|
||||||
}
|
|
||||||
else if (!aadInstance.EndsWith("/"))
|
|
||||||
{
|
|
||||||
aadInstance = aadInstance + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
return aadInstance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !NETSTANDARD
|
|
||||||
/// <summary>
|
|
||||||
/// Application tenant either from a registry override or a constant
|
|
||||||
/// </summary>
|
|
||||||
public static string ApplicationTenant =>
|
|
||||||
VssClientEnvironment.GetSharedConnectedUserValue<string>(VssConnectionParameterOverrideKeys.AadApplicationTenant)
|
|
||||||
?? VssAadSettings.ApplicationTenantId;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.IdentityModel.Clients.ActiveDirectory;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Client
|
|
||||||
{
|
|
||||||
[Serializable]
|
|
||||||
public class VssAadToken : IssuedToken
|
|
||||||
{
|
|
||||||
private string accessToken;
|
|
||||||
private string accessTokenType;
|
|
||||||
|
|
||||||
private AuthenticationContext authenticationContext;
|
|
||||||
private UserCredential userCredential;
|
|
||||||
private VssAadTokenOptions options;
|
|
||||||
|
|
||||||
public VssAadToken(AuthenticationResult authentication)
|
|
||||||
{
|
|
||||||
// Prevent any attempt to store this token.
|
|
||||||
this.FromStorage = true;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(authentication.AccessToken))
|
|
||||||
{
|
|
||||||
this.Authenticated();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.accessToken = authentication.AccessToken;
|
|
||||||
this.accessTokenType = authentication.AccessTokenType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public VssAadToken(
|
|
||||||
string accessTokenType,
|
|
||||||
string accessToken)
|
|
||||||
{
|
|
||||||
// Prevent any attempt to store this token.
|
|
||||||
this.FromStorage = true;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(accessToken) && !string.IsNullOrWhiteSpace(accessTokenType))
|
|
||||||
{
|
|
||||||
this.Authenticated();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.accessToken = accessToken;
|
|
||||||
this.accessTokenType = accessTokenType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public VssAadToken(
|
|
||||||
AuthenticationContext authenticationContext,
|
|
||||||
UserCredential userCredential = null,
|
|
||||||
VssAadTokenOptions options = VssAadTokenOptions.None)
|
|
||||||
{
|
|
||||||
// Prevent any attempt to store this token.
|
|
||||||
this.FromStorage = true;
|
|
||||||
|
|
||||||
this.authenticationContext = authenticationContext;
|
|
||||||
this.userCredential = userCredential;
|
|
||||||
this.options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected internal override VssCredentialsType CredentialType
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return VssCredentialsType.Aad;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthenticationResult AcquireToken()
|
|
||||||
{
|
|
||||||
if (this.authenticationContext == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthenticationResult authenticationResult = null;
|
|
||||||
|
|
||||||
for (int index = 0; index < 3; index++)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (this.userCredential == null && !options.HasFlag(VssAadTokenOptions.AllowDialog))
|
|
||||||
{
|
|
||||||
authenticationResult = authenticationContext.AcquireTokenSilentAsync(VssAadSettings.Resource, VssAadSettings.Client).ConfigureAwait(false).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
authenticationResult = authenticationContext.AcquireTokenAsync(VssAadSettings.Resource, VssAadSettings.Client, this.userCredential).ConfigureAwait(false).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authenticationResult != null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception x)
|
|
||||||
{
|
|
||||||
System.Diagnostics.Debug.WriteLine("Failed to get ADFS token: " + x.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return authenticationResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override void ApplyTo(IHttpRequest request)
|
|
||||||
{
|
|
||||||
AuthenticationResult authenticationResult = AcquireToken();
|
|
||||||
if (authenticationResult != null)
|
|
||||||
{
|
|
||||||
request.Headers.SetValue(Common.Internal.HttpHeaders.Authorization, $"{authenticationResult.AccessTokenType} {authenticationResult.AccessToken}");
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(this.accessTokenType) && !string.IsNullOrEmpty(this.accessToken))
|
|
||||||
{
|
|
||||||
request.Headers.SetValue(Common.Internal.HttpHeaders.Authorization, $"{this.accessTokenType} {this.accessToken}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
public enum VssAadTokenOptions
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
AllowDialog = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.IdentityModel.Clients.ActiveDirectory;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Client
|
|
||||||
{
|
|
||||||
internal sealed class VssAadTokenProvider : IssuedTokenProvider
|
|
||||||
{
|
|
||||||
public VssAadTokenProvider(VssAadCredential credential)
|
|
||||||
: base(credential, null, null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool GetTokenIsInteractive
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private VssAadToken GetVssAadToken()
|
|
||||||
{
|
|
||||||
AuthenticationContext authenticationContext = new AuthenticationContext(string.Concat(VssAadSettings.AadInstance, VssAadSettings.CommonTenant));
|
|
||||||
UserCredential userCredential = null;
|
|
||||||
|
|
||||||
VssAadCredential credential = this.Credential as VssAadCredential;
|
|
||||||
|
|
||||||
if (credential?.Username != null)
|
|
||||||
{
|
|
||||||
#if NETSTANDARD
|
|
||||||
// UserPasswordCredential does not currently exist for ADAL 3.13.5 for any non-desktop build.
|
|
||||||
userCredential = new UserCredential(credential.Username);
|
|
||||||
#else
|
|
||||||
if (credential.Password != null)
|
|
||||||
{
|
|
||||||
userCredential = new UserPasswordCredential(credential.Username, credential.Password);
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
userCredential = new UserCredential(credential.Username);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
userCredential = new UserCredential();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new VssAadToken(authenticationContext, userCredential);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Temporary implementation since we don't have a good configuration story here at the moment.
|
|
||||||
/// </summary>
|
|
||||||
protected override Task<IssuedToken> OnGetTokenAsync(IssuedToken failedToken, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// If we have already tried to authenticate with an AAD token retrieved from Windows integrated authentication and it is not working, clear out state.
|
|
||||||
if (failedToken != null && failedToken.CredentialType == VssCredentialsType.Aad && failedToken.IsAuthenticated)
|
|
||||||
{
|
|
||||||
this.CurrentToken = null;
|
|
||||||
return Task.FromResult<IssuedToken>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Task.FromResult<IssuedToken>(GetVssAadToken());
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{ }
|
|
||||||
|
|
||||||
return Task.FromResult<IssuedToken>(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
using GitHub.Services.Common.Internal;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Client
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Provides federated authentication with a hosted <c>VssConnection</c> instance using cookies.
|
|
||||||
/// </summary>
|
|
||||||
[Serializable]
|
|
||||||
public sealed class VssFederatedCredential : FederatedCredential
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssFederatedCredential</c> instance.
|
|
||||||
/// </summary>
|
|
||||||
public VssFederatedCredential()
|
|
||||||
: this(true)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssFederatedCredential</c> instance.
|
|
||||||
/// </summary>
|
|
||||||
public VssFederatedCredential(Boolean useCache)
|
|
||||||
: this(useCache, null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssFederatedCredential</c> instance.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="initialToken">The initial token if available</param>
|
|
||||||
public VssFederatedCredential(VssFederatedToken initialToken)
|
|
||||||
: this(false, initialToken)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public VssFederatedCredential(
|
|
||||||
Boolean useCache,
|
|
||||||
VssFederatedToken initialToken)
|
|
||||||
: base(initialToken)
|
|
||||||
{
|
|
||||||
#if !NETSTANDARD
|
|
||||||
if (useCache)
|
|
||||||
{
|
|
||||||
Storage = new VssClientCredentialStorage();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public override VssCredentialsType CredentialType
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return VssCredentialsType.Federated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Boolean IsAuthenticationChallenge(IHttpResponse webResponse)
|
|
||||||
{
|
|
||||||
bool isNonAuthenticationChallenge = false;
|
|
||||||
return IsVssFederatedAuthenticationChallenge(webResponse, out isNonAuthenticationChallenge) ?? isNonAuthenticationChallenge;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IssuedTokenProvider OnCreateTokenProvider(
|
|
||||||
Uri serverUrl,
|
|
||||||
IHttpResponse response)
|
|
||||||
{
|
|
||||||
// The response is only null when attempting to determine the most appropriate token provider to
|
|
||||||
// use for the connection. The only way we should do anything here is if we have an initial token
|
|
||||||
// since that means we can present something without making a server call.
|
|
||||||
if (response == null && base.InitialToken == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri signInUrl = null;
|
|
||||||
String realm = String.Empty;
|
|
||||||
String issuer = String.Empty;
|
|
||||||
|
|
||||||
if (response != null)
|
|
||||||
{
|
|
||||||
var location = response.Headers.GetValues(HttpHeaders.Location).FirstOrDefault();
|
|
||||||
if (location == null)
|
|
||||||
{
|
|
||||||
location = response.Headers.GetValues(HttpHeaders.TfsFedAuthRedirect).FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!String.IsNullOrEmpty(location))
|
|
||||||
{
|
|
||||||
signInUrl = new Uri(location);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inform the server that we support the javascript notify "smart client" pattern for ACS auth
|
|
||||||
AddParameter(ref signInUrl, "protocol", "javascriptnotify");
|
|
||||||
|
|
||||||
// Do not automatically sign in with existing FedAuth cookie
|
|
||||||
AddParameter(ref signInUrl, "force", "1");
|
|
||||||
|
|
||||||
GetRealmAndIssuer(response, out realm, out issuer);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new VssFederatedTokenProvider(this, serverUrl, signInUrl, issuer, realm);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void GetRealmAndIssuer(
|
|
||||||
IHttpResponse response,
|
|
||||||
out String realm,
|
|
||||||
out String issuer)
|
|
||||||
{
|
|
||||||
realm = response.Headers.GetValues(HttpHeaders.TfsFedAuthRealm).FirstOrDefault();
|
|
||||||
issuer = response.Headers.GetValues(HttpHeaders.TfsFedAuthIssuer).FirstOrDefault();
|
|
||||||
|
|
||||||
if (!String.IsNullOrWhiteSpace(issuer))
|
|
||||||
{
|
|
||||||
issuer = new Uri(issuer).GetLeftPart(UriPartial.Authority);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Boolean? IsVssFederatedAuthenticationChallenge(
|
|
||||||
IHttpResponse webResponse,
|
|
||||||
out Boolean isNonAuthenticationChallenge)
|
|
||||||
{
|
|
||||||
isNonAuthenticationChallenge = false;
|
|
||||||
|
|
||||||
if (webResponse == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check to make sure that the redirect was issued from the Tfs service. We include the TfsServiceError
|
|
||||||
// header to avoid the possibility that a redirect from a non-tfs service is issued and we incorrectly
|
|
||||||
// launch the credentials UI.
|
|
||||||
if (webResponse.StatusCode == HttpStatusCode.Found ||
|
|
||||||
webResponse.StatusCode == HttpStatusCode.Redirect)
|
|
||||||
{
|
|
||||||
return webResponse.Headers.GetValues(HttpHeaders.Location).Any() && webResponse.Headers.GetValues(HttpHeaders.TfsFedAuthRealm).Any();
|
|
||||||
}
|
|
||||||
else if (webResponse.StatusCode == HttpStatusCode.Unauthorized)
|
|
||||||
{
|
|
||||||
return webResponse.Headers.GetValues(HttpHeaders.WwwAuthenticate).Any(x => x.StartsWith("TFS-Federated", StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
else if (webResponse.StatusCode == HttpStatusCode.Forbidden)
|
|
||||||
{
|
|
||||||
// This is not strictly an "authentication challenge" but it is a state the user can do something about so they can get access to the resource
|
|
||||||
// they are attempting to access. Specifically, the user will hit this when they need to update or create a profile required by business policy.
|
|
||||||
isNonAuthenticationChallenge = webResponse.Headers.GetValues(HttpHeaders.TfsFedAuthRedirect).Any();
|
|
||||||
if (isNonAuthenticationChallenge)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddParameter(ref Uri uri, String name, String value)
|
|
||||||
{
|
|
||||||
if (uri.Query.IndexOf(String.Concat(name, "="), StringComparison.OrdinalIgnoreCase) < 0)
|
|
||||||
{
|
|
||||||
UriBuilder builder = new UriBuilder(uri);
|
|
||||||
builder.Query = String.Concat(builder.Query.TrimStart('?'), "&", name, "=", value);
|
|
||||||
uri = builder.Uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Client
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Provides a cookie-based authentication token.
|
|
||||||
/// </summary>
|
|
||||||
[Serializable]
|
|
||||||
public sealed class VssFederatedToken : IssuedToken
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssFederatedToken</c> instance using the specified cookies.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="cookies"></param>
|
|
||||||
public VssFederatedToken(CookieCollection cookies)
|
|
||||||
{
|
|
||||||
ArgumentUtility.CheckForNull(cookies, "cookies");
|
|
||||||
m_cookies = cookies;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the CookieCollection contained within this token. For internal use only.
|
|
||||||
/// </summary>
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public CookieCollection CookieCollection
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return m_cookies;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected internal override VssCredentialsType CredentialType
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return VssCredentialsType.Federated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override void ApplyTo(IHttpRequest request)
|
|
||||||
{
|
|
||||||
// From http://www.ietf.org/rfc/rfc2109.txt:
|
|
||||||
// Note: For backward compatibility, the separator in the Cookie header
|
|
||||||
// is semi-colon (;) everywhere.
|
|
||||||
//
|
|
||||||
// HttpRequestHeaders uses comma as the default separator, so instead of returning
|
|
||||||
// a list of cookies, the method returns one semicolon separated string.
|
|
||||||
IEnumerable<String> values = request.Headers.GetValues(s_cookieHeader);
|
|
||||||
request.Headers.SetValue(s_cookieHeader, GetHeaderValue(values));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String GetHeaderValue(IEnumerable<String> cookieHeaders)
|
|
||||||
{
|
|
||||||
List<String> currentCookies = new List<String>();
|
|
||||||
if (cookieHeaders != null)
|
|
||||||
{
|
|
||||||
foreach (String value in cookieHeaders)
|
|
||||||
{
|
|
||||||
currentCookies.AddRange(value.Split(';').Select(x => x.Trim()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentCookies.RemoveAll(x => String.IsNullOrEmpty(x));
|
|
||||||
|
|
||||||
foreach (Cookie cookie in m_cookies)
|
|
||||||
{
|
|
||||||
// Remove all existing cookies that match the name of the cookie we are going to add.
|
|
||||||
currentCookies.RemoveAll(x => String.Equals(x.Substring(0, x.IndexOf('=')), cookie.Name, StringComparison.OrdinalIgnoreCase));
|
|
||||||
currentCookies.Add(String.Concat(cookie.Name, "=", cookie.Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
return String.Join("; ", currentCookies);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CookieCollection m_cookies;
|
|
||||||
private static readonly String s_cookieHeader = HttpRequestHeader.Cookie.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Client
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Provides authentication for internet identities using single-sign-on cookies.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class VssFederatedTokenProvider : IssuedTokenProvider, ISupportSignOut
|
|
||||||
{
|
|
||||||
public VssFederatedTokenProvider(
|
|
||||||
VssFederatedCredential credential,
|
|
||||||
Uri serverUrl,
|
|
||||||
Uri signInUrl,
|
|
||||||
String issuer,
|
|
||||||
String realm)
|
|
||||||
: base(credential, serverUrl, signInUrl)
|
|
||||||
{
|
|
||||||
Issuer = issuer;
|
|
||||||
Realm = realm;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override String AuthenticationScheme
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return "TFS-Federated";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override String AuthenticationParameter
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (String.IsNullOrEmpty(this.Issuer) && String.IsNullOrEmpty(this.Realm))
|
|
||||||
{
|
|
||||||
return String.Empty;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return String.Format(CultureInfo.InvariantCulture, "issuer=\"{0}\", realm=\"{1}\"", this.Issuer, this.Realm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the federated credential from which this provider was created.
|
|
||||||
/// </summary>
|
|
||||||
public new VssFederatedCredential Credential
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return (VssFederatedCredential)base.Credential;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether or not a call to get token will require interactivity.
|
|
||||||
/// </summary>
|
|
||||||
public override Boolean GetTokenIsInteractive
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return this.CurrentToken == null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the issuer for the token provider.
|
|
||||||
/// </summary>
|
|
||||||
public String Issuer
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
private set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the realm for the token provider.
|
|
||||||
/// </summary>
|
|
||||||
public String Realm
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
private set;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected internal override Boolean IsAuthenticationChallenge(IHttpResponse webResponse)
|
|
||||||
{
|
|
||||||
if (!base.IsAuthenticationChallenge(webResponse))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This means we were proactively constructed without any connection information. In this case
|
|
||||||
// we return false to ensure that a new provider is reconstructed with all appropriate configuration
|
|
||||||
// to retrieve a new token.
|
|
||||||
if (this.SignInUrl == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String realm, issuer;
|
|
||||||
VssFederatedCredential.GetRealmAndIssuer(webResponse, out realm, out issuer);
|
|
||||||
|
|
||||||
return this.Realm.Equals(realm, StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
this.Issuer.Equals(issuer, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IssuedToken OnValidatingToken(
|
|
||||||
IssuedToken token,
|
|
||||||
IHttpResponse webResponse)
|
|
||||||
{
|
|
||||||
// If the response has Set-Cookie headers, attempt to retrieve the FedAuth cookie from the response
|
|
||||||
// and replace the current token with the new FedAuth cookie. Note that the server only reissues the
|
|
||||||
// FedAuth cookie if it is issued for more than an hour.
|
|
||||||
CookieCollection fedAuthCookies = CookieUtility.GetFederatedCookies(webResponse);
|
|
||||||
|
|
||||||
if (fedAuthCookies != null)
|
|
||||||
{
|
|
||||||
// The reissued token should have the same user information as the previous one.
|
|
||||||
VssFederatedToken federatedToken = new VssFederatedToken(fedAuthCookies)
|
|
||||||
{
|
|
||||||
Properties = token.Properties,
|
|
||||||
UserId = token.UserId,
|
|
||||||
UserName = token.UserName
|
|
||||||
};
|
|
||||||
|
|
||||||
token = federatedToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SignOut(Uri signOutUrl, Uri replyToUrl, String identityProvider)
|
|
||||||
{
|
|
||||||
// The preferred implementation is to follow the signOutUrl with a browser and kill the browser whenever it
|
|
||||||
// arrives at the replyToUrl (or if it bombs out somewhere along the way).
|
|
||||||
// This will work for all Web-based identity providers (Live, Google, Yahoo, Facebook) supported by ACS provided that
|
|
||||||
// the TFS server has registered sign-out urls (in the TF Registry) for each of these.
|
|
||||||
// This is the long-term approach that should be pursued and probably the approach recommended to other
|
|
||||||
// clients which don't have direct access to the cookie store (TEE?)
|
|
||||||
|
|
||||||
// In the short term we are simply going to delete the TFS cookies and the Windows Live cookies that are exposed to this
|
|
||||||
// session. This has the drawback of not properly signing out of Live (you'd still be signed in to e.g. Hotmail, Xbox, MSN, etc.)
|
|
||||||
// but will allow the user to re-enter their live credentials and sign-in again to TFS.
|
|
||||||
// The other drawback is that the clients will have to be updated again when we pursue the implementation outlined above.
|
|
||||||
|
|
||||||
CookieUtility.DeleteFederatedCookies(replyToUrl);
|
|
||||||
if (!String.IsNullOrEmpty(identityProvider) && identityProvider.Equals("Windows Live ID", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
CookieUtility.DeleteWindowsLiveCookies();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using GitHub.Services.Common.Internal;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Common
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Provides a credential for basic authentication against a Visual Studio Service.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class VssBasicCredential : FederatedCredential
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssBasicCredential</c> instance with no token specified.
|
|
||||||
/// </summary>
|
|
||||||
public VssBasicCredential()
|
|
||||||
: this((VssBasicToken)null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssBasicCredential</c> instance with the specified user name and password.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userName">The user name</param>
|
|
||||||
/// <param name="password">The password</param>
|
|
||||||
public VssBasicCredential(
|
|
||||||
string userName,
|
|
||||||
string password)
|
|
||||||
: this(new VssBasicToken(new NetworkCredential(userName, password)))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssBasicCredential</c> instance with the specified token.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="initialToken">An optional token which, if present, should be used before obtaining a new token</param>
|
|
||||||
public VssBasicCredential(ICredentials initialToken)
|
|
||||||
: this(new VssBasicToken(initialToken))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssBasicCredential</c> instance with the specified token.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="initialToken">An optional token which, if present, should be used before obtaining a new token</param>
|
|
||||||
public VssBasicCredential(VssBasicToken initialToken)
|
|
||||||
: base(initialToken)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override VssCredentialsType CredentialType
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return VssCredentialsType.Basic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool IsAuthenticationChallenge(IHttpResponse webResponse)
|
|
||||||
{
|
|
||||||
if (webResponse == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (webResponse.StatusCode != HttpStatusCode.Found &&
|
|
||||||
webResponse.StatusCode != HttpStatusCode.Redirect &&
|
|
||||||
webResponse.StatusCode != HttpStatusCode.Unauthorized)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return webResponse.Headers.GetValues(HttpHeaders.WwwAuthenticate).Any(x => x.StartsWith("Basic", StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IssuedTokenProvider OnCreateTokenProvider(
|
|
||||||
Uri serverUrl,
|
|
||||||
IHttpResponse response)
|
|
||||||
{
|
|
||||||
if (serverUrl.Scheme != "https")
|
|
||||||
{
|
|
||||||
String unsafeBasicAuthEnv = Environment.GetEnvironmentVariable("VSS_ALLOW_UNSAFE_BASICAUTH") ?? "false";
|
|
||||||
if (!Boolean.TryParse(unsafeBasicAuthEnv, out Boolean unsafeBasicAuth) || !unsafeBasicAuth)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(CommonResources.BasicAuthenticationRequiresSsl());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new BasicAuthTokenProvider(this, serverUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Common
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Provides a token for basic authentication of internet identities.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class VssBasicToken : IssuedToken
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>BasicAuthToken</c> instance with the specified token value.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="credentials">The credentials which should be used for authentication</param>
|
|
||||||
public VssBasicToken(ICredentials credentials)
|
|
||||||
{
|
|
||||||
m_credentials = credentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal ICredentials Credentials
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return m_credentials;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected internal override VssCredentialsType CredentialType
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return VssCredentialsType.Basic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override void ApplyTo(IHttpRequest request)
|
|
||||||
{
|
|
||||||
var basicCredential = m_credentials.GetCredential(request.RequestUri, "Basic");
|
|
||||||
if (basicCredential != null)
|
|
||||||
{
|
|
||||||
request.Headers.SetValue(Internal.HttpHeaders.Authorization, "Basic " + FormatBasicAuthHeader(basicCredential));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String FormatBasicAuthHeader(NetworkCredential credential)
|
|
||||||
{
|
|
||||||
String authHeader = String.Empty;
|
|
||||||
if (!String.IsNullOrEmpty(credential.Domain))
|
|
||||||
{
|
|
||||||
authHeader = String.Format(CultureInfo.InvariantCulture, "{0}\\{1}:{2}", credential.Domain, credential.UserName, credential.Password);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
authHeader = String.Format(CultureInfo.InvariantCulture, "{0}:{1}", credential.UserName, credential.Password);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Convert.ToBase64String(VssHttpRequestSettings.Encoding.GetBytes(authHeader));
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly ICredentials m_credentials;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Common
|
|
||||||
{
|
|
||||||
internal sealed class BasicAuthTokenProvider : IssuedTokenProvider
|
|
||||||
{
|
|
||||||
public BasicAuthTokenProvider(
|
|
||||||
VssBasicCredential credential,
|
|
||||||
Uri serverUrl)
|
|
||||||
: base(credential, serverUrl, serverUrl)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override String AuthenticationScheme
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return "Basic";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public new VssBasicCredential Credential
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return (VssBasicCredential)base.Credential;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Boolean GetTokenIsInteractive
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return base.CurrentToken == null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -51,47 +51,7 @@ namespace GitHub.Services.Common
|
|||||||
/// Initializes a new <c>VssCredentials</c> instance with default credentials.
|
/// Initializes a new <c>VssCredentials</c> instance with default credentials.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public VssCredentials()
|
public VssCredentials()
|
||||||
: this(true)
|
: this(null)
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssCredentials</c> instance with default credentials if specified.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="useDefaultCredentials">True to use default windows credentials; otherwise, false</param>
|
|
||||||
public VssCredentials(bool useDefaultCredentials)
|
|
||||||
: this(new WindowsCredential(useDefaultCredentials))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssCredentials</c> instance with the specified windows credential.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="windowsCredential">The windows credential to use for authentication</param>
|
|
||||||
public VssCredentials(WindowsCredential windowsCredential)
|
|
||||||
: this(windowsCredential, null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssCredentials</c> instance with the specified windows credential.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="windowsCredential">The windows credential to use for authentication</param>
|
|
||||||
/// <param name="promptType">CredentialPromptType.PromptIfNeeded if interactive prompts are allowed, otherwise CredentialProptType.DoNotPrompt</param>
|
|
||||||
public VssCredentials(
|
|
||||||
WindowsCredential windowsCredential,
|
|
||||||
CredentialPromptType promptType)
|
|
||||||
: this(windowsCredential, null, promptType)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssCredentials</c> instance with the specified issued token credential and
|
|
||||||
/// default windows credential.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="federatedCredential">The federated credential to use for authentication</param>
|
|
||||||
public VssCredentials(FederatedCredential federatedCredential)
|
|
||||||
: this(new WindowsCredential(), federatedCredential)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,12 +59,9 @@ namespace GitHub.Services.Common
|
|||||||
/// Initializes a new <c>VssCredentials</c> instance with the specified windows and issued token
|
/// Initializes a new <c>VssCredentials</c> instance with the specified windows and issued token
|
||||||
/// credential.
|
/// credential.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="windowsCredential">The windows credential to use for authentication</param>
|
|
||||||
/// <param name="federatedCredential">The federated credential to use for authentication</param>
|
/// <param name="federatedCredential">The federated credential to use for authentication</param>
|
||||||
public VssCredentials(
|
public VssCredentials(FederatedCredential federatedCredential)
|
||||||
WindowsCredential windowsCredential,
|
: this(federatedCredential, EnvironmentUserInteractive
|
||||||
FederatedCredential federatedCredential)
|
|
||||||
: this(windowsCredential, federatedCredential, EnvironmentUserInteractive
|
|
||||||
? CredentialPromptType.PromptIfNeeded : CredentialPromptType.DoNotPrompt)
|
? CredentialPromptType.PromptIfNeeded : CredentialPromptType.DoNotPrompt)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -113,14 +70,12 @@ namespace GitHub.Services.Common
|
|||||||
/// Initializes a new <c>VssCredentials</c> instance with the specified windows and issued token
|
/// Initializes a new <c>VssCredentials</c> instance with the specified windows and issued token
|
||||||
/// credential.
|
/// credential.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="windowsCredential">The windows credential to use for authentication</param>
|
|
||||||
/// <param name="federatedCredential">The federated credential to use for authentication</param>
|
/// <param name="federatedCredential">The federated credential to use for authentication</param>
|
||||||
/// <param name="promptType">CredentialPromptType.PromptIfNeeded if interactive prompts are allowed, otherwise CredentialProptType.DoNotPrompt</param>
|
/// <param name="promptType">CredentialPromptType.PromptIfNeeded if interactive prompts are allowed, otherwise CredentialProptType.DoNotPrompt</param>
|
||||||
public VssCredentials(
|
public VssCredentials(
|
||||||
WindowsCredential windowsCredential,
|
|
||||||
FederatedCredential federatedCredential,
|
FederatedCredential federatedCredential,
|
||||||
CredentialPromptType promptType)
|
CredentialPromptType promptType)
|
||||||
: this(windowsCredential, federatedCredential, promptType, null)
|
: this(federatedCredential, promptType, null)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,16 +83,14 @@ namespace GitHub.Services.Common
|
|||||||
/// Initializes a new <c>VssCredentials</c> instance with the specified windows and issued token
|
/// Initializes a new <c>VssCredentials</c> instance with the specified windows and issued token
|
||||||
/// credential.
|
/// credential.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="windowsCredential">The windows credential to use for authentication</param>
|
|
||||||
/// <param name="federatedCredential">The federated credential to use for authentication</param>
|
/// <param name="federatedCredential">The federated credential to use for authentication</param>
|
||||||
/// <param name="promptType">CredentialPromptType.PromptIfNeeded if interactive prompts are allowed; otherwise, CredentialProptType.DoNotPrompt</param>
|
/// <param name="promptType">CredentialPromptType.PromptIfNeeded if interactive prompts are allowed; otherwise, CredentialProptType.DoNotPrompt</param>
|
||||||
/// <param name="scheduler">An optional <c>TaskScheduler</c> to ensure credentials prompting occurs on the UI thread</param>
|
/// <param name="scheduler">An optional <c>TaskScheduler</c> to ensure credentials prompting occurs on the UI thread</param>
|
||||||
public VssCredentials(
|
public VssCredentials(
|
||||||
WindowsCredential windowsCredential,
|
|
||||||
FederatedCredential federatedCredential,
|
FederatedCredential federatedCredential,
|
||||||
CredentialPromptType promptType,
|
CredentialPromptType promptType,
|
||||||
TaskScheduler scheduler)
|
TaskScheduler scheduler)
|
||||||
: this(windowsCredential, federatedCredential, promptType, scheduler, null)
|
: this(federatedCredential, promptType, scheduler, null)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,13 +98,11 @@ namespace GitHub.Services.Common
|
|||||||
/// Initializes a new <c>VssCredentials</c> instance with the specified windows and issued token
|
/// Initializes a new <c>VssCredentials</c> instance with the specified windows and issued token
|
||||||
/// credential.
|
/// credential.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="windowsCredential">The windows credential to use for authentication</param>
|
|
||||||
/// <param name="federatedCredential">The federated credential to use for authentication</param>
|
/// <param name="federatedCredential">The federated credential to use for authentication</param>
|
||||||
/// <param name="promptType">CredentialPromptType.PromptIfNeeded if interactive prompts are allowed; otherwise, CredentialProptType.DoNotPrompt</param>
|
/// <param name="promptType">CredentialPromptType.PromptIfNeeded if interactive prompts are allowed; otherwise, CredentialProptType.DoNotPrompt</param>
|
||||||
/// <param name="scheduler">An optional <c>TaskScheduler</c> to ensure credentials prompting occurs on the UI thread</param>
|
/// <param name="scheduler">An optional <c>TaskScheduler</c> to ensure credentials prompting occurs on the UI thread</param>
|
||||||
/// <param name="credentialPrompt">An optional <c>IVssCredentialPrompt</c> to perform prompting for credentials</param>
|
/// <param name="credentialPrompt">An optional <c>IVssCredentialPrompt</c> to perform prompting for credentials</param>
|
||||||
public VssCredentials(
|
public VssCredentials(
|
||||||
WindowsCredential windowsCredential,
|
|
||||||
FederatedCredential federatedCredential,
|
FederatedCredential federatedCredential,
|
||||||
CredentialPromptType promptType,
|
CredentialPromptType promptType,
|
||||||
TaskScheduler scheduler,
|
TaskScheduler scheduler,
|
||||||
@@ -172,13 +123,6 @@ namespace GitHub.Services.Common
|
|||||||
scheduler = TaskScheduler.Default;
|
scheduler = TaskScheduler.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (windowsCredential != null)
|
|
||||||
{
|
|
||||||
m_windowsCredential = windowsCredential;
|
|
||||||
m_windowsCredential.Scheduler = scheduler;
|
|
||||||
m_windowsCredential.Prompt = credentialPrompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (federatedCredential != null)
|
if (federatedCredential != null)
|
||||||
{
|
{
|
||||||
m_federatedCredential = federatedCredential;
|
m_federatedCredential = federatedCredential;
|
||||||
@@ -199,16 +143,6 @@ namespace GitHub.Services.Common
|
|||||||
return new VssCredentials(credential);
|
return new VssCredentials(credential);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implicitly converts a <c>WindowsCredential</c> instance into a <c>VssCredentials</c> instance.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="credential">The windows credential instance</param>
|
|
||||||
/// <returns>A new <c>VssCredentials</c> instance which wraps the specified credential</returns>
|
|
||||||
public static implicit operator VssCredentials(WindowsCredential credential)
|
|
||||||
{
|
|
||||||
return new VssCredentials(credential);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether or not interactive prompts are allowed.
|
/// Gets or sets a value indicating whether or not interactive prompts are allowed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -240,17 +174,6 @@ namespace GitHub.Services.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the windows credential to use for NTLM authentication with the server.
|
|
||||||
/// </summary>
|
|
||||||
public WindowsCredential Windows
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return m_windowsCredential;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A pluggable credential store.
|
/// A pluggable credential store.
|
||||||
/// Simply assign a storage implementation to this property
|
/// Simply assign a storage implementation to this property
|
||||||
@@ -267,11 +190,6 @@ namespace GitHub.Services.Common
|
|||||||
{
|
{
|
||||||
m_credentialStorage = value;
|
m_credentialStorage = value;
|
||||||
|
|
||||||
if (m_windowsCredential != null)
|
|
||||||
{
|
|
||||||
m_windowsCredential.Storage = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_federatedCredential != null)
|
if (m_federatedCredential != null)
|
||||||
{
|
{
|
||||||
m_federatedCredential.Storage = value;
|
m_federatedCredential.Storage = value;
|
||||||
@@ -327,20 +245,6 @@ namespace GitHub.Services.Common
|
|||||||
VssHttpEventSource.Log.IssuedTokenProviderCreated(traceActivity, tokenProvider);
|
VssHttpEventSource.Log.IssuedTokenProviderCreated(traceActivity, tokenProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (m_windowsCredential != null && m_windowsCredential.IsAuthenticationChallenge(webResponse))
|
|
||||||
{
|
|
||||||
if (tokenProvider != null)
|
|
||||||
{
|
|
||||||
VssHttpEventSource.Log.IssuedTokenProviderRemoved(traceActivity, tokenProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenProvider = m_windowsCredential.CreateTokenProvider(serverUrl, webResponse, failedToken);
|
|
||||||
|
|
||||||
if (tokenProvider != null)
|
|
||||||
{
|
|
||||||
VssHttpEventSource.Log.IssuedTokenProviderCreated(traceActivity, tokenProvider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_currentProvider = tokenProvider;
|
m_currentProvider = tokenProvider;
|
||||||
}
|
}
|
||||||
@@ -356,7 +260,7 @@ namespace GitHub.Services.Common
|
|||||||
/// <param name="provider">Stores the active token provider, if one exists</param>
|
/// <param name="provider">Stores the active token provider, if one exists</param>
|
||||||
/// <returns>True if a token provider was found, false otherwise</returns>
|
/// <returns>True if a token provider was found, false otherwise</returns>
|
||||||
public bool TryGetTokenProvider(
|
public bool TryGetTokenProvider(
|
||||||
Uri serverUrl,
|
Uri serverUrl,
|
||||||
out IssuedTokenProvider provider)
|
out IssuedTokenProvider provider)
|
||||||
{
|
{
|
||||||
ArgumentUtility.CheckForNull(serverUrl, "serverUrl");
|
ArgumentUtility.CheckForNull(serverUrl, "serverUrl");
|
||||||
@@ -371,11 +275,6 @@ namespace GitHub.Services.Common
|
|||||||
m_currentProvider = m_federatedCredential.CreateTokenProvider(serverUrl, null, null);
|
m_currentProvider = m_federatedCredential.CreateTokenProvider(serverUrl, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_currentProvider == null && m_windowsCredential != null)
|
|
||||||
{
|
|
||||||
m_currentProvider = m_windowsCredential.CreateTokenProvider(serverUrl, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_currentProvider != null)
|
if (m_currentProvider != null)
|
||||||
{
|
{
|
||||||
VssHttpEventSource.Log.IssuedTokenProviderCreated(VssTraceActivity.Current, m_currentProvider);
|
VssHttpEventSource.Log.IssuedTokenProviderCreated(VssTraceActivity.Current, m_currentProvider);
|
||||||
@@ -401,11 +300,6 @@ namespace GitHub.Services.Common
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isChallenge = false;
|
bool isChallenge = false;
|
||||||
if (m_windowsCredential != null)
|
|
||||||
{
|
|
||||||
isChallenge = m_windowsCredential.IsAuthenticationChallenge(webResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isChallenge && m_federatedCredential != null)
|
if (!isChallenge && m_federatedCredential != null)
|
||||||
{
|
{
|
||||||
isChallenge = m_federatedCredential.IsAuthenticationChallenge(webResponse);
|
isChallenge = m_federatedCredential.IsAuthenticationChallenge(webResponse);
|
||||||
@@ -415,8 +309,8 @@ namespace GitHub.Services.Common
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal void SignOut(
|
internal void SignOut(
|
||||||
Uri serverUrl,
|
Uri serverUrl,
|
||||||
Uri serviceLocation,
|
Uri serviceLocation,
|
||||||
string identityProvider)
|
string identityProvider)
|
||||||
{
|
{
|
||||||
// Remove the token in the storage and the current token provider. Note that we don't
|
// Remove the token in the storage and the current token provider. Note that we don't
|
||||||
@@ -450,110 +344,6 @@ namespace GitHub.Services.Common
|
|||||||
tokenProviderWithSignOut.SignOut(serviceLocation, serverUrl, identityProvider);
|
tokenProviderWithSignOut.SignOut(serviceLocation, serverUrl, identityProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !NETSTANDARD
|
|
||||||
/// <summary>
|
|
||||||
/// Loads stored credentials for the specified server if found. If no credentials are found in the windows
|
|
||||||
/// credential store for the specified server and options then default credentials are returned.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverUrl">The server location</param>
|
|
||||||
/// <param name="requireExactMatch">A value indicating whether or not an exact or partial match of the server is required</param>
|
|
||||||
/// <returns>A credentials object populated with stored credentials for the server if found</returns>
|
|
||||||
public static VssCredentials LoadCachedCredentials(
|
|
||||||
Uri serverUrl,
|
|
||||||
bool requireExactMatch)
|
|
||||||
{
|
|
||||||
return LoadCachedCredentials(null, serverUrl, requireExactMatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Loads stored credentials for the specified server if found. If no credentials are found for the specified server and options then default credentials are returned.
|
|
||||||
/// This overload assumes that the credentials are to be stored under the TFS server's registry root
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="featureRegistryKeyword">An optional application name for isolated credential storage in the registry</param>
|
|
||||||
/// <param name="serverUrl">The server location</param>
|
|
||||||
/// <param name="requireExactMatch">A value indicating whether or not an exact or partial match of the server is required</param>
|
|
||||||
/// <returns>A credentials object populated with stored credentials for the server if found</returns>
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public static VssCredentials LoadCachedCredentials(
|
|
||||||
string featureRegistryKeyword,
|
|
||||||
Uri serverUrl,
|
|
||||||
bool requireExactMatch)
|
|
||||||
{
|
|
||||||
ArgumentUtility.CheckForNull(serverUrl, "serverUrl");
|
|
||||||
|
|
||||||
bool uriKnownToCachedProvider = false;
|
|
||||||
VssCredentials cred = LoadCachedCredentialsFromRegisteredProviders(serverUrl, out uriKnownToCachedProvider);
|
|
||||||
|
|
||||||
// If one of the registered credential providers had the target URI in its cache but failed to return a valid credential it means
|
|
||||||
// we should have had a cred but something went wrong (user canceled, user failed, auth source unavailable, etc.). In that case
|
|
||||||
// we Do Not want to carry on with the fallback to the VS registry/windows store credential caches. Even if that worked to get a
|
|
||||||
// credential it would put the user in a bad state (having an active, authenticated connection with an unexpected credential type).
|
|
||||||
if (cred == null && !uriKnownToCachedProvider)
|
|
||||||
{
|
|
||||||
WindowsCredential windowsCredential = null;
|
|
||||||
FederatedCredential federatedCredential = null;
|
|
||||||
CredentialsCacheManager credentialsCacheManager = new CredentialsCacheManager();
|
|
||||||
TfsCredentialCacheEntry cacheEntry = credentialsCacheManager.GetCredentials(featureRegistryKeyword, serverUrl, requireExactMatch, null);
|
|
||||||
if (cacheEntry != null)
|
|
||||||
{
|
|
||||||
if (cacheEntry.NonInteractive)
|
|
||||||
{
|
|
||||||
switch (cacheEntry.Type)
|
|
||||||
{
|
|
||||||
case CachedCredentialsType.ServiceIdentity:
|
|
||||||
VssServiceIdentityToken initialToken = null;
|
|
||||||
string initialTokenValue = ReadAuthorizationToken(cacheEntry.Attributes);
|
|
||||||
if (!string.IsNullOrEmpty(initialTokenValue))
|
|
||||||
{
|
|
||||||
initialToken = new VssServiceIdentityToken(initialTokenValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the issued token credential using the stored token if it exists
|
|
||||||
federatedCredential = new VssServiceIdentityCredential(cacheEntry.Credentials.UserName,
|
|
||||||
cacheEntry.Credentials.Password,
|
|
||||||
initialToken);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CachedCredentialsType.Windows:
|
|
||||||
windowsCredential = new WindowsCredential(cacheEntry.Credentials);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cred = new VssCredentials(windowsCredential ?? new WindowsCredential(true), federatedCredential, CredentialPromptType.DoNotPrompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cred ?? new VssCredentials(new WindowsCredential(true), null, CredentialPromptType.DoNotPrompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public static VssCredentials LoadCachedCredentialsFromRegisteredProviders(Uri serverUri, out bool knownUri)
|
|
||||||
{
|
|
||||||
LoadRegisteredCachedVssCredentialProviders();
|
|
||||||
bool uriKnownByAnyProvider = false;
|
|
||||||
VssCredentials cred = null;
|
|
||||||
foreach (var pair in m_loadedCachedVssCredentialProviders)
|
|
||||||
{
|
|
||||||
bool uriKnownToProvider = false;
|
|
||||||
cred = pair.Value?.GetCachedCredentials(serverUri, out uriKnownToProvider);
|
|
||||||
if (cred != null || uriKnownToProvider)
|
|
||||||
{
|
|
||||||
uriKnownByAnyProvider |= uriKnownToProvider;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
knownUri = uriKnownByAnyProvider;
|
|
||||||
return cred;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void LoadRegisteredCachedVssCredentialProviders()
|
|
||||||
{
|
|
||||||
CredentialsProviderRegistryHelper.LoadCachedVssCredentialProviders(ref m_loadedCachedVssCredentialProviders);
|
|
||||||
}
|
|
||||||
private static ConcurrentDictionary<string, ICachedVssCredentialProvider> m_loadedCachedVssCredentialProviders = new ConcurrentDictionary<string, ICachedVssCredentialProvider>();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
public static void WriteAuthorizationToken(
|
public static void WriteAuthorizationToken(
|
||||||
string token,
|
string token,
|
||||||
@@ -604,7 +394,6 @@ namespace GitHub.Services.Common
|
|||||||
private object m_thisLock;
|
private object m_thisLock;
|
||||||
private CredentialPromptType m_promptType;
|
private CredentialPromptType m_promptType;
|
||||||
private IssuedTokenProvider m_currentProvider;
|
private IssuedTokenProvider m_currentProvider;
|
||||||
protected WindowsCredential m_windowsCredential;
|
|
||||||
protected FederatedCredential m_federatedCredential;
|
protected FederatedCredential m_federatedCredential;
|
||||||
private IVssCredentialStorage m_credentialStorage;
|
private IVssCredentialStorage m_credentialStorage;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Common
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Provides federated authentication as a service identity with a Visual Studio Service.
|
|
||||||
/// </summary>
|
|
||||||
[Serializable]
|
|
||||||
public sealed class VssServiceIdentityCredential : FederatedCredential
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssServiceIdentityCredential</c> instance with the specified user name and password.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userName">The user name</param>
|
|
||||||
/// <param name="password">The password</param>
|
|
||||||
public VssServiceIdentityCredential(
|
|
||||||
string userName,
|
|
||||||
string password)
|
|
||||||
: this(userName, password, null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssServiceIdentityCredential</c> instance with the specified user name and password. The
|
|
||||||
/// provided token, if not null, will be used before attempting authentication with the credentials.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userName">The user name</param>
|
|
||||||
/// <param name="password">The password</param>
|
|
||||||
/// <param name="initialToken">An optional token which, if present, should be used before obtaining a new token</param>
|
|
||||||
public VssServiceIdentityCredential(
|
|
||||||
string userName,
|
|
||||||
string password,
|
|
||||||
VssServiceIdentityToken initialToken)
|
|
||||||
: this(userName, password, initialToken, null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssServiceIdentityCredential</c> instance with the specified access token.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="token">A token which may be used for authorization as the desired service identity</param>
|
|
||||||
public VssServiceIdentityCredential(VssServiceIdentityToken token)
|
|
||||||
: this(null, null, token, null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssServiceIdentityCredential</c> instance with the specified user name and password. The
|
|
||||||
/// provided token, if not null, will be used before attempting authentication with the credentials.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userName">The user name</param>
|
|
||||||
/// <param name="password">The password</param>
|
|
||||||
/// <param name="initialToken">An optional token which, if present, should be used before obtaining a new token</param>
|
|
||||||
/// <param name="innerHandler">An optional HttpMessageHandler which if passed will be passed along to the TokenProvider when executing OnCreateTokenProvider </param>
|
|
||||||
public VssServiceIdentityCredential(
|
|
||||||
string userName,
|
|
||||||
string password,
|
|
||||||
VssServiceIdentityToken initialToken,
|
|
||||||
DelegatingHandler innerHandler)
|
|
||||||
: base(initialToken)
|
|
||||||
{
|
|
||||||
m_userName = userName;
|
|
||||||
m_password = password;
|
|
||||||
m_innerHandler = innerHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override VssCredentialsType CredentialType
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return VssCredentialsType.ServiceIdentity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user name.
|
|
||||||
/// </summary>
|
|
||||||
public String UserName
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return m_userName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the password.
|
|
||||||
/// </summary>
|
|
||||||
internal String Password
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return m_password;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool IsAuthenticationChallenge(IHttpResponse webResponse)
|
|
||||||
{
|
|
||||||
if (webResponse == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (webResponse.StatusCode == HttpStatusCode.Found ||
|
|
||||||
webResponse.StatusCode == HttpStatusCode.Redirect ||
|
|
||||||
webResponse.StatusCode == HttpStatusCode.Unauthorized)
|
|
||||||
{
|
|
||||||
var authRealm = webResponse.Headers.GetValues(Internal.HttpHeaders.TfsFedAuthRealm).FirstOrDefault();
|
|
||||||
var authIssuer = webResponse.Headers.GetValues(Internal.HttpHeaders.TfsFedAuthIssuer).FirstOrDefault();
|
|
||||||
var wwwAuthenticate = webResponse.Headers.GetValues(Internal.HttpHeaders.WwwAuthenticate);
|
|
||||||
if (!String.IsNullOrEmpty(authIssuer) && !String.IsNullOrEmpty(authRealm))
|
|
||||||
{
|
|
||||||
return webResponse.StatusCode != HttpStatusCode.Unauthorized || wwwAuthenticate.Any(x => x.StartsWith("TFS-Federated", StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override string GetAuthenticationChallenge(IHttpResponse webResponse)
|
|
||||||
{
|
|
||||||
var authRealm = webResponse.Headers.GetValues(Internal.HttpHeaders.TfsFedAuthRealm).FirstOrDefault();
|
|
||||||
var authIssuer = webResponse.Headers.GetValues(Internal.HttpHeaders.TfsFedAuthIssuer).FirstOrDefault();
|
|
||||||
return string.Format(CultureInfo.InvariantCulture, "TFS-Federated realm={0}, issuer={1}", authRealm, authIssuer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a provider for retrieving security tokens for the provided credentials.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>An issued token provider for the current credential</returns>
|
|
||||||
protected override IssuedTokenProvider OnCreateTokenProvider(
|
|
||||||
Uri serverUrl,
|
|
||||||
IHttpResponse response)
|
|
||||||
{
|
|
||||||
// The response is only null when attempting to determine the most appropriate token provider to
|
|
||||||
// use for the connection. The only way we should do anything here is if we have an initial token
|
|
||||||
// since that means we can present something without making a server call.
|
|
||||||
if (response == null && base.InitialToken == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri signInUrl = null;
|
|
||||||
String realm = string.Empty;
|
|
||||||
if (response != null)
|
|
||||||
{
|
|
||||||
realm = response.Headers.GetValues(Internal.HttpHeaders.TfsFedAuthRealm).FirstOrDefault();
|
|
||||||
signInUrl = new Uri(new Uri(response.Headers.GetValues(Internal.HttpHeaders.TfsFedAuthIssuer).FirstOrDefault()).GetLeftPart(UriPartial.Authority));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new VssServiceIdentityTokenProvider(this, serverUrl, signInUrl, realm, m_innerHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly String m_userName;
|
|
||||||
private readonly String m_password;
|
|
||||||
|
|
||||||
[NonSerialized]
|
|
||||||
private readonly DelegatingHandler m_innerHandler = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using GitHub.Services.Common.Internal;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Common
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Provides simple web token used for OAuth authentication.
|
|
||||||
/// </summary>
|
|
||||||
[Serializable]
|
|
||||||
public sealed class VssServiceIdentityToken : IssuedToken
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>VssServiceIdentityToken</c> instance with the specified token value.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="token">The token value as a string</param>
|
|
||||||
public VssServiceIdentityToken(string token)
|
|
||||||
{
|
|
||||||
ArgumentUtility.CheckStringForNullOrEmpty(token, "token");
|
|
||||||
|
|
||||||
m_token = token;
|
|
||||||
//.ValidFrom = DateTime.UtcNow;
|
|
||||||
|
|
||||||
// Read out the expiration time for the ValidTo field if we can find it
|
|
||||||
Dictionary<string, string> nameValues;
|
|
||||||
if (TryGetNameValues(token, out nameValues))
|
|
||||||
{
|
|
||||||
string expiresOnValue;
|
|
||||||
if (nameValues.TryGetValue(c_expiresName, out expiresOnValue))
|
|
||||||
{
|
|
||||||
// The time is represented as standard epoch
|
|
||||||
// base.ValidTo = s_epoch.AddSeconds(Convert.ToUInt64(expiresOnValue, CultureInfo.CurrentCulture));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String Token
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return m_token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected internal override VssCredentialsType CredentialType
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return VssCredentialsType.ServiceIdentity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override void ApplyTo(IHttpRequest request)
|
|
||||||
{
|
|
||||||
request.Headers.SetValue(Internal.HttpHeaders.Authorization, "WRAP access_token=\"" + m_token + "\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static VssServiceIdentityToken ExtractToken(string responseValue)
|
|
||||||
{
|
|
||||||
// Extract the actual token string
|
|
||||||
string token = UriUtility.UrlDecode(responseValue
|
|
||||||
.Split('&')
|
|
||||||
.Single(value => value.StartsWith("wrap_access_token=", StringComparison.OrdinalIgnoreCase))
|
|
||||||
.Split('=')[1], VssHttpRequestSettings.Encoding);
|
|
||||||
|
|
||||||
return new VssServiceIdentityToken(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool TryGetNameValues(
|
|
||||||
string token,
|
|
||||||
out Dictionary<string, string> tokenValues)
|
|
||||||
{
|
|
||||||
tokenValues = null;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(token))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenValues =
|
|
||||||
token
|
|
||||||
.Split('&')
|
|
||||||
.Aggregate(
|
|
||||||
new Dictionary<string, string>(),
|
|
||||||
(dict, rawNameValue) =>
|
|
||||||
{
|
|
||||||
if (rawNameValue == string.Empty)
|
|
||||||
{
|
|
||||||
return dict;
|
|
||||||
}
|
|
||||||
|
|
||||||
string[] nameValue = rawNameValue.Split('=');
|
|
||||||
|
|
||||||
if (nameValue.Length != 2)
|
|
||||||
{
|
|
||||||
return dict;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dict.ContainsKey(nameValue[0]) == true)
|
|
||||||
{
|
|
||||||
return dict;
|
|
||||||
}
|
|
||||||
|
|
||||||
dict.Add(UriUtility.UrlDecode(nameValue[0]), UriUtility.UrlDecode(nameValue[1]));
|
|
||||||
return dict;
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string m_token;
|
|
||||||
private const string c_expiresName = "ExpiresOn";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
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
|
|
||||||
{
|
|
||||||
internal sealed class VssServiceIdentityTokenProvider : IssuedTokenProvider
|
|
||||||
{
|
|
||||||
public VssServiceIdentityTokenProvider(
|
|
||||||
VssServiceIdentityCredential credential,
|
|
||||||
Uri serverUrl,
|
|
||||||
Uri signInUrl,
|
|
||||||
string realm,
|
|
||||||
DelegatingHandler innerHandler)
|
|
||||||
: this(credential, serverUrl, signInUrl, realm)
|
|
||||||
{
|
|
||||||
m_innerHandler = innerHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
public VssServiceIdentityTokenProvider(
|
|
||||||
VssServiceIdentityCredential credential,
|
|
||||||
Uri serverUrl,
|
|
||||||
Uri signInUrl,
|
|
||||||
string realm)
|
|
||||||
: base(credential, serverUrl, signInUrl)
|
|
||||||
{
|
|
||||||
Realm = realm;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string AuthenticationParameter
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(this.Realm) && this.SignInUrl == null)
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return string.Format(CultureInfo.InvariantCulture, "issuer=\"{0}\", realm=\"{1}\"", this.SignInUrl, this.Realm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override String AuthenticationScheme
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return "TFS-Federated";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the simple web token credential from which this provider was created.
|
|
||||||
/// </summary>
|
|
||||||
public new VssServiceIdentityCredential Credential
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return (VssServiceIdentityCredential)base.Credential;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether or not a call to get token will require interactivity.
|
|
||||||
/// </summary>
|
|
||||||
public override Boolean GetTokenIsInteractive
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the realm for the token provider.
|
|
||||||
/// </summary>
|
|
||||||
public String Realm
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected internal override bool IsAuthenticationChallenge(IHttpResponse webResponse)
|
|
||||||
{
|
|
||||||
if (!base.IsAuthenticationChallenge(webResponse))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This means we were proactively constructed without any connection information. In this case
|
|
||||||
// we return false to ensure that a new provider is reconstructed with all appropriate configuration
|
|
||||||
// to retrieve a new token.
|
|
||||||
if (this.SignInUrl == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string authRealm = webResponse.Headers.GetValues(HttpHeaders.TfsFedAuthRealm).FirstOrDefault();
|
|
||||||
string authIssuer = webResponse.Headers.GetValues(HttpHeaders.TfsFedAuthIssuer).FirstOrDefault();
|
|
||||||
Uri signInUrl = new Uri(new Uri(authIssuer).GetLeftPart(UriPartial.Authority), UriKind.Absolute);
|
|
||||||
|
|
||||||
// Make sure that the values match our stored values. This way if the values change we will be thrown
|
|
||||||
// away and a new instance with correct values will be constructed.
|
|
||||||
return this.Realm.Equals(authRealm, StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
Uri.Compare(this.SignInUrl, signInUrl, UriComponents.AbsoluteUri, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Issues a request to synchronously retrieve a token for the associated credential.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="failedToken"></param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected override async Task<IssuedToken> OnGetTokenAsync(
|
|
||||||
IssuedToken failedToken,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(this.Credential.UserName) ||
|
|
||||||
string.IsNullOrEmpty(this.Credential.Password))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
VssTraceActivity traceActivity = VssTraceActivity.Current;
|
|
||||||
using (HttpClient client = new HttpClient(CreateMessageHandler(), false))
|
|
||||||
{
|
|
||||||
client.BaseAddress = this.SignInUrl;
|
|
||||||
|
|
||||||
KeyValuePair<string, string>[] values = new KeyValuePair<string, string>[]
|
|
||||||
{
|
|
||||||
new KeyValuePair<string, string>("wrap_name", this.Credential.UserName),
|
|
||||||
new KeyValuePair<string, string>("wrap_password", this.Credential.Password),
|
|
||||||
new KeyValuePair<string, string>("wrap_scope", this.Realm),
|
|
||||||
};
|
|
||||||
|
|
||||||
Uri url = new Uri("WRAPv0.9/", UriKind.Relative);
|
|
||||||
FormUrlEncodedContent content = new FormUrlEncodedContent(values);
|
|
||||||
using (HttpResponseMessage response = await client.PostAsync(url, content, cancellationToken).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
string responseValue = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
return VssServiceIdentityToken.ExtractToken(responseValue);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
VssHttpEventSource.Log.AuthenticationError(traceActivity, this, responseValue);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpMessageHandler CreateMessageHandler()
|
|
||||||
{
|
|
||||||
var retryOptions = new VssHttpRetryOptions()
|
|
||||||
{
|
|
||||||
RetryableStatusCodes =
|
|
||||||
{
|
|
||||||
VssNetworkHelper.TooManyRequests,
|
|
||||||
HttpStatusCode.InternalServerError,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
HttpMessageHandler innerHandler;
|
|
||||||
|
|
||||||
if (m_innerHandler != null)
|
|
||||||
{
|
|
||||||
if (m_innerHandler.InnerHandler == null)
|
|
||||||
{
|
|
||||||
m_innerHandler.InnerHandler = new HttpClientHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
innerHandler = m_innerHandler;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
innerHandler = new HttpClientHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inherit proxy setting from VssHttpMessageHandler
|
|
||||||
var httpClientHandler = innerHandler as HttpClientHandler;
|
|
||||||
if (httpClientHandler != null && VssHttpMessageHandler.DefaultWebProxy != null)
|
|
||||||
{
|
|
||||||
httpClientHandler.Proxy = VssHttpMessageHandler.DefaultWebProxy;
|
|
||||||
httpClientHandler.UseProxy = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new VssHttpRetryMessageHandler(retryOptions, innerHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DelegatingHandler m_innerHandler = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Common
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Provides a credential for windows authentication against a Visual Studio Service.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class WindowsCredential : IssuedTokenCredential
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>WindowsCredential</c> instance using a default user interface provider implementation
|
|
||||||
/// and the default network credentials.
|
|
||||||
/// </summary>
|
|
||||||
public WindowsCredential()
|
|
||||||
: this(true)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>WindowsCredential</c> instance using a default user interface provider implementation
|
|
||||||
/// and the default network credentials, if specified.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="useDefaultCredentials">True if the default credentials should be used; otherwise, false</param>
|
|
||||||
public WindowsCredential(bool useDefaultCredentials)
|
|
||||||
: this(useDefaultCredentials ? CredentialCache.DefaultCredentials : null)
|
|
||||||
{
|
|
||||||
UseDefaultCredentials = useDefaultCredentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>WindowsCredential</c> instance using a default user interface provider implementation
|
|
||||||
/// and the specified network credentials.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="credentials">The windows credentials which should be used for authentication</param>
|
|
||||||
public WindowsCredential(ICredentials credentials)
|
|
||||||
: this(null)
|
|
||||||
{
|
|
||||||
m_credentials = credentials;
|
|
||||||
UseDefaultCredentials = credentials == CredentialCache.DefaultCredentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new <c>WindowsCredential</c> instance using the specified initial token.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="initialToken">An optional token which, if present, should be used before obtaining a new token</param>
|
|
||||||
public WindowsCredential(WindowsToken initialToken)
|
|
||||||
: base(initialToken)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the credentials associated with this windows credential.
|
|
||||||
/// </summary>
|
|
||||||
public ICredentials Credentials
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return m_credentials;
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
m_credentials = value;
|
|
||||||
UseDefaultCredentials = Credentials == CredentialCache.DefaultCredentials;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override VssCredentialsType CredentialType
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return VssCredentialsType.Windows;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating what value was passed to WindowsCredential(bool useDefaultCredentials) constructor
|
|
||||||
/// </summary>
|
|
||||||
public Boolean UseDefaultCredentials
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
private set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Boolean IsAuthenticationChallenge(IHttpResponse webResponse)
|
|
||||||
{
|
|
||||||
if (webResponse == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (webResponse.StatusCode == HttpStatusCode.Unauthorized &&
|
|
||||||
webResponse.Headers.GetValues(Internal.HttpHeaders.WwwAuthenticate).Any(x => AuthenticationSchemeValid(x)))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (webResponse.StatusCode == HttpStatusCode.ProxyAuthenticationRequired &&
|
|
||||||
webResponse.Headers.GetValues(Internal.HttpHeaders.ProxyAuthenticate).Any(x => AuthenticationSchemeValid(x)))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IssuedTokenProvider OnCreateTokenProvider(
|
|
||||||
Uri serverUrl,
|
|
||||||
IHttpResponse response)
|
|
||||||
{
|
|
||||||
// If we have no idea what kind of credentials we are supposed to be using, don't play a windows token on
|
|
||||||
// the first request.
|
|
||||||
if (response == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_credentials != null)
|
|
||||||
{
|
|
||||||
this.InitialToken = new WindowsToken(m_credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new WindowsTokenProvider(this, serverUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean AuthenticationSchemeValid(String authenticateHeader)
|
|
||||||
{
|
|
||||||
return authenticateHeader.StartsWith("Basic", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
authenticateHeader.StartsWith("Digest", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
authenticateHeader.StartsWith("Negotiate", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
authenticateHeader.StartsWith("Ntlm", StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ICredentials m_credentials;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Common
|
|
||||||
{
|
|
||||||
public sealed class WindowsToken : IssuedToken, ICredentials
|
|
||||||
{
|
|
||||||
internal WindowsToken(ICredentials credentials)
|
|
||||||
{
|
|
||||||
this.Credentials = credentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ICredentials Credentials
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected internal override VssCredentialsType CredentialType
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return VssCredentialsType.Windows;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override void ApplyTo(IHttpRequest request)
|
|
||||||
{
|
|
||||||
// Special-cased by the caller because we implement ICredentials
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkCredential ICredentials.GetCredential(
|
|
||||||
Uri uri,
|
|
||||||
String authType)
|
|
||||||
{
|
|
||||||
return this.Credentials?.GetCredential(uri, authType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Common
|
|
||||||
{
|
|
||||||
internal sealed class WindowsTokenProvider : IssuedTokenProvider
|
|
||||||
{
|
|
||||||
public WindowsTokenProvider(
|
|
||||||
WindowsCredential credential,
|
|
||||||
Uri serverUrl)
|
|
||||||
: base(credential, serverUrl, serverUrl)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override String AuthenticationScheme
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return String.Format(CultureInfo.InvariantCulture, "{0} {1} {2} {3}", AuthenticationSchemes.Negotiate, AuthenticationSchemes.Ntlm, AuthenticationSchemes.Digest, AuthenticationSchemes.Basic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public new WindowsCredential Credential
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return (WindowsCredential)base.Credential;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Boolean GetTokenIsInteractive
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return base.CurrentToken == null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,9 +5,6 @@ using System.Globalization;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
#if !NETSTANDARD
|
|
||||||
using System.Diagnostics.Eventing;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace GitHub.Services.Common.Diagnostics
|
namespace GitHub.Services.Common.Diagnostics
|
||||||
{
|
{
|
||||||
@@ -838,13 +835,6 @@ namespace GitHub.Services.Common.Diagnostics
|
|||||||
[NonEvent]
|
[NonEvent]
|
||||||
private void SetActivityId(VssTraceActivity activity)
|
private void SetActivityId(VssTraceActivity activity)
|
||||||
{
|
{
|
||||||
#if !NETSTANDARD
|
|
||||||
if (activity != null)
|
|
||||||
{
|
|
||||||
Guid activityId = activity.Id;
|
|
||||||
EventProvider.SetActivityId(ref activityId);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[NonEvent]
|
[NonEvent]
|
||||||
@@ -876,15 +866,6 @@ namespace GitHub.Services.Common.Diagnostics
|
|||||||
Action<Int32, String> writeEvent)
|
Action<Int32, String> writeEvent)
|
||||||
{
|
{
|
||||||
writeEvent(param0, message);
|
writeEvent(param0, message);
|
||||||
#if !NETSTANDARD
|
|
||||||
if (EventProvider.GetLastWriteEventError() == EventProvider.WriteEventErrorCode.EventTooBig)
|
|
||||||
{
|
|
||||||
foreach (String messagePart in SplitMessage(message))
|
|
||||||
{
|
|
||||||
writeEvent(param0, messagePart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[NonEvent]
|
[NonEvent]
|
||||||
@@ -895,15 +876,6 @@ namespace GitHub.Services.Common.Diagnostics
|
|||||||
Action<VssCredentialsType, Int32, String> writeEvent)
|
Action<VssCredentialsType, Int32, String> writeEvent)
|
||||||
{
|
{
|
||||||
writeEvent(param0, param1, message);
|
writeEvent(param0, param1, message);
|
||||||
#if !NETSTANDARD
|
|
||||||
if (EventProvider.GetLastWriteEventError() == EventProvider.WriteEventErrorCode.EventTooBig)
|
|
||||||
{
|
|
||||||
foreach (String messagePart in SplitMessage(message))
|
|
||||||
{
|
|
||||||
writeEvent(param0, param1, messagePart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[NonEvent]
|
[NonEvent]
|
||||||
@@ -914,23 +886,10 @@ namespace GitHub.Services.Common.Diagnostics
|
|||||||
Action<VssHttpMethod, String, String> writeEvent)
|
Action<VssHttpMethod, String, String> writeEvent)
|
||||||
{
|
{
|
||||||
writeEvent(param0, param1, message);
|
writeEvent(param0, param1, message);
|
||||||
#if !NETSTANDARD
|
|
||||||
if (EventProvider.GetLastWriteEventError() == EventProvider.WriteEventErrorCode.EventTooBig)
|
|
||||||
{
|
|
||||||
foreach (String messagePart in SplitMessage(message))
|
|
||||||
{
|
|
||||||
writeEvent(param0, param1, messagePart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[NonEvent]
|
[NonEvent]
|
||||||
#if !NETSTANDARD
|
|
||||||
private unsafe void WriteEvent(
|
|
||||||
#else
|
|
||||||
private new unsafe void WriteEvent(
|
private new unsafe void WriteEvent(
|
||||||
#endif
|
|
||||||
Int32 eventId,
|
Int32 eventId,
|
||||||
Int32 param0,
|
Int32 param0,
|
||||||
String param1)
|
String param1)
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
#if !NETSTANDARD
|
|
||||||
using System.Runtime.Remoting.Messaging;
|
|
||||||
#endif
|
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
namespace GitHub.Services.Common.Diagnostics
|
namespace GitHub.Services.Common.Diagnostics
|
||||||
@@ -38,22 +35,11 @@ namespace GitHub.Services.Common.Diagnostics
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static VssTraceActivity Current
|
public static VssTraceActivity Current
|
||||||
{
|
{
|
||||||
#if !NETSTANDARD
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return CallContext.LogicalGetData(VssTraceActivity.PropertyName) as VssTraceActivity;
|
|
||||||
}
|
|
||||||
private set
|
|
||||||
{
|
|
||||||
CallContext.LogicalSetData(VssTraceActivity.PropertyName, value);
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
set { }
|
set { }
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -8,84 +8,28 @@ namespace GitHub.Services.Common.Internal
|
|||||||
public static class HttpHeaders
|
public static class HttpHeaders
|
||||||
{
|
{
|
||||||
public const String ActivityId = "ActivityId";
|
public const String ActivityId = "ActivityId";
|
||||||
public const String ETag = "ETag";
|
|
||||||
public const String TfsVersion = "X-TFS-Version";
|
|
||||||
public const String TfsRedirect = "X-TFS-Redirect";
|
|
||||||
public const String TfsException = "X-TFS-Exception";
|
|
||||||
public const String TfsServiceError = "X-TFS-ServiceError";
|
public const String TfsServiceError = "X-TFS-ServiceError";
|
||||||
public const String TfsSessionHeader = "X-TFS-Session";
|
public const String TfsSessionHeader = "X-TFS-Session";
|
||||||
public const String TfsSoapException = "X-TFS-SoapException";
|
|
||||||
public const String TfsFedAuthRealm = "X-TFS-FedAuthRealm";
|
public const String TfsFedAuthRealm = "X-TFS-FedAuthRealm";
|
||||||
public const String TfsFedAuthIssuer = "X-TFS-FedAuthIssuer";
|
public const String TfsFedAuthIssuer = "X-TFS-FedAuthIssuer";
|
||||||
public const String TfsFedAuthRedirect = "X-TFS-FedAuthRedirect";
|
public const String TfsFedAuthRedirect = "X-TFS-FedAuthRedirect";
|
||||||
public const String VssAuthorizationEndpoint = "X-VSS-AuthorizationEndpoint";
|
|
||||||
public const String VssPageHandlers = "X-VSS-PageHandlers";
|
|
||||||
public const String VssE2EID = "X-VSS-E2EID";
|
public const String VssE2EID = "X-VSS-E2EID";
|
||||||
public const String VssOrchestrationId = "X-VSS-OrchestrationId";
|
|
||||||
public const String AuditCorrelationId = "X-VSS-Audit-CorrelationId";
|
|
||||||
public const String VssOriginUserAgent = "X-VSS-OriginUserAgent";
|
|
||||||
|
|
||||||
// Internal Headers that we use in our client.
|
|
||||||
public const string TfsInstanceHeader = "X-TFS-Instance";
|
|
||||||
public const string TfsVersionOneHeader = "X-VersionControl-Instance";
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Tfs")]
|
|
||||||
public const string TfsImpersonate = "X-TFS-Impersonate";
|
|
||||||
public const string TfsSubjectDescriptorImpersonate = "X-TFS-SubjectDescriptorImpersonate";
|
|
||||||
|
|
||||||
public const string MsContinuationToken = "X-MS-ContinuationToken";
|
|
||||||
public const String VssUserData = "X-VSS-UserData";
|
public const String VssUserData = "X-VSS-UserData";
|
||||||
public const String VssAgentHeader = "X-VSS-Agent";
|
public const String VssAgentHeader = "X-VSS-Agent";
|
||||||
public const String VssAuthenticateError = "X-VSS-AuthenticateError";
|
public const String VssAuthenticateError = "X-VSS-AuthenticateError";
|
||||||
public const string VssReauthenticationAction = "X-VSS-ReauthenticationAction";
|
|
||||||
public const string RequestedWith = "X-Requested-With";
|
|
||||||
|
|
||||||
public const String VssRateLimitResource = "X-RateLimit-Resource";
|
|
||||||
public const String VssRateLimitDelay = "X-RateLimit-Delay";
|
public const String VssRateLimitDelay = "X-RateLimit-Delay";
|
||||||
public const String VssRateLimitLimit = "X-RateLimit-Limit";
|
|
||||||
public const String VssRateLimitRemaining = "X-RateLimit-Remaining";
|
|
||||||
public const String VssRateLimitReset = "X-RateLimit-Reset";
|
public const String VssRateLimitReset = "X-RateLimit-Reset";
|
||||||
public const String RetryAfter = "Retry-After";
|
|
||||||
|
|
||||||
public const String VssGlobalMessage = "X-VSS-GlobalMessage";
|
|
||||||
|
|
||||||
public const String VssRequestRouted = "X-VSS-RequestRouted";
|
|
||||||
public const String VssUseRequestRouting = "X-VSS-UseRequestRouting";
|
|
||||||
|
|
||||||
public const string VssResourceTenant = "X-VSS-ResourceTenant";
|
|
||||||
public const String VssOverridePrompt = "X-VSS-OverridePrompt";
|
|
||||||
|
|
||||||
public const String VssOAuthS2STargetService = "X-VSS-S2STargetService";
|
|
||||||
public const String VssHostOfflineError = "X-VSS-HostOfflineError";
|
public const String VssHostOfflineError = "X-VSS-HostOfflineError";
|
||||||
|
|
||||||
public const string VssForceMsaPassThrough = "X-VSS-ForceMsaPassThrough";
|
|
||||||
public const string VssRequestPriority = "X-VSS-RequestPriority";
|
public const string VssRequestPriority = "X-VSS-RequestPriority";
|
||||||
|
|
||||||
// This header represents set of ';' delimited mappings (usually one) that are considered by DetermineAccessMapping API
|
|
||||||
public const string VssClientAccessMapping = "X-VSS-ClientAccessMapping";
|
|
||||||
|
|
||||||
// This header is used to download artifacts anonymously.
|
|
||||||
// N.B. Some resources secured with download tickets (e.g. TFVC files) are still retrieved with the download
|
|
||||||
// ticket in the query string.
|
|
||||||
public const string VssDownloadTicket = "X-VSS-DownloadTicket";
|
|
||||||
|
|
||||||
public const string IfModifiedSince = "If-Modified-Since";
|
|
||||||
public const string Authorization = "Authorization";
|
public const string Authorization = "Authorization";
|
||||||
public const string Location = "Location";
|
|
||||||
public const string ProxyAuthenticate = "Proxy-Authenticate";
|
public const string ProxyAuthenticate = "Proxy-Authenticate";
|
||||||
public const string WwwAuthenticate = "WWW-Authenticate";
|
public const string WwwAuthenticate = "WWW-Authenticate";
|
||||||
|
|
||||||
public const string AfdIncomingRouteKey = "X-FD-RouteKey";
|
|
||||||
public const string AfdOutgoingRouteKey = "X-AS-RouteKey";
|
|
||||||
public const string AfdIncomingEndpointList = "X-FD-RouteKeyApplicationEndpointList";
|
|
||||||
public const string AfdOutgoingEndpointList = "X-AS-RouteKeyApplicationEndpointList";
|
|
||||||
public const string AfdResponseRef = "X-MSEdge-Ref";
|
public const string AfdResponseRef = "X-MSEdge-Ref";
|
||||||
public const string AfdIncomingClientIp = "X-FD-ClientIP";
|
|
||||||
public const string AfdIncomingSocketIp = "X-FD-SocketIP";
|
|
||||||
public const string AfdIncomingRef = "X-FD-Ref";
|
|
||||||
public const string AfdIncomingEventId = "X-FD-EventId";
|
|
||||||
public const string AfdIncomingEdgeEnvironment = "X-FD-EdgeEnvironment";
|
|
||||||
public const string AfdOutgoingQualityOfResponse = "X-AS-QualityOfResponse";
|
|
||||||
public const string AfdOutgoingClientIp = "X-MSEdge-ClientIP";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,553 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.ConstrainedExecution;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace GitHub.Services.Common
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Provides path normalization/expansion for absolute, relative and UNC-style paths
|
|
||||||
/// and supports paths that contain more than 248 characters.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This utility class can be used in place of the .NET Path and Directory classes
|
|
||||||
/// that throw System.IO.PathTooLongException when paths are longer than 248 characters
|
|
||||||
/// </remarks>
|
|
||||||
public static class LongPathUtility
|
|
||||||
{
|
|
||||||
private static Regex AbsolutePathRegEx = new Regex(@"^([a-zA-Z]:\\|\\\\)", RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
|
||||||
private const int ERROR_FILE_NOT_FOUND = 2;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a list of directory names under the path specified, and optionally all subdirectories
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The directory to search</param>
|
|
||||||
/// <param name="recursiveSearch">Specifies whether the search operation should include only the currect directory or all subdirectories</param>
|
|
||||||
/// <returns>A list of all subdirectories</returns>
|
|
||||||
public static IEnumerable<string> EnumerateDirectories(string path, bool recursiveSearch)
|
|
||||||
{
|
|
||||||
var directoryPaths = new List<string>();
|
|
||||||
EnumerateDirectoriesInternal(directoryPaths, path, recursiveSearch);
|
|
||||||
return directoryPaths;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a list of file names under the path specified, and optionally within all subdirectories.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The directory to search</param>
|
|
||||||
/// <param name="recursiveSearch">Specifies whether the search operation should include only the current directory or all subdirectories</param>
|
|
||||||
/// <returns>
|
|
||||||
/// A list of full file names(including path) contained in the directory specified that match the specified search pattern.</returns>
|
|
||||||
public static IEnumerable<string> EnumerateFiles(string path, bool recursiveSearch)
|
|
||||||
{
|
|
||||||
return EnumerateFiles(path, "*", recursiveSearch);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns an enumerable collection of file names that match a search pattern in a specified path,
|
|
||||||
/// and optionally searches subdirectories.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The directory to search</param>
|
|
||||||
/// <param name="matchPattern">The search string to match against the names of the files</param>
|
|
||||||
/// <param name="recursiveSearch">Specifies whether the search operation should include only the current directory or all subdirectories</param>
|
|
||||||
/// <returns>
|
|
||||||
/// A list of full file names(including path) contained in the directory specified (and subdirectories optionally) that match the specified pattern.
|
|
||||||
/// </returns>
|
|
||||||
public static IEnumerable<string> EnumerateFiles(string path, string matchPattern, bool recursiveSearch)
|
|
||||||
{
|
|
||||||
if (!DirectoryExists(path))
|
|
||||||
{
|
|
||||||
throw new DirectoryNotFoundException($"The path '{path}' is not a valid directory.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var filePaths = new List<string>();
|
|
||||||
EnumerateFilesInternal(filePaths, path, matchPattern, recursiveSearch);
|
|
||||||
return filePaths;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns true/false whether the file exists. This method inspects the
|
|
||||||
/// file system attributes and supports files without extensions (ex: DIRS, Sources). This method
|
|
||||||
/// supports file paths that are longer than 260 characters.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filePath">The file path to inspect</param>
|
|
||||||
/// <returns>
|
|
||||||
/// True if the file exists or false if not
|
|
||||||
/// </returns>
|
|
||||||
public static bool FileExists(string filePath)
|
|
||||||
{
|
|
||||||
return FileOrDirectoryExists(filePath, isDirectory: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns true/false whether the directory exists. This method inspects the
|
|
||||||
/// file system attributes and supports files without extensions (ex: DIRS, Sources). This method
|
|
||||||
/// supports file paths that are longer than 260 characters.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="directoryPath">The file path to inspect</param>
|
|
||||||
/// <returns>
|
|
||||||
/// True if the directory exists or false if not
|
|
||||||
/// </returns>
|
|
||||||
public static bool DirectoryExists(string directoryPath)
|
|
||||||
{
|
|
||||||
return FileOrDirectoryExists(directoryPath, isDirectory: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool FileOrDirectoryExists(string filePath, bool isDirectory)
|
|
||||||
{
|
|
||||||
if (String.IsNullOrWhiteSpace(filePath))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("A path to the file is required and cannot be null, empty or whitespace", "filePath");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool pathExists = false;
|
|
||||||
|
|
||||||
// File names may or may not include an extension (ex: DIRS, Sources). We have to look at the attributes
|
|
||||||
// on the file system object in order to distinguish a directory from a file
|
|
||||||
var attributes = (FlagsAndAttributes)NativeMethods.GetFileAttributes(filePath);
|
|
||||||
|
|
||||||
if (attributes != FlagsAndAttributes.InvalidFileAttributes)
|
|
||||||
{
|
|
||||||
bool pathIsDirectory = (attributes & FlagsAndAttributes.Directory) == FlagsAndAttributes.Directory;
|
|
||||||
|
|
||||||
if (pathIsDirectory == isDirectory)
|
|
||||||
{
|
|
||||||
pathExists = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pathExists;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the fully expanded/normalized path. This method supports paths that are
|
|
||||||
/// longer than 248 characters.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The file or directory path</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string GetFullNormalizedPath(string path)
|
|
||||||
{
|
|
||||||
if (String.IsNullOrWhiteSpace(path))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("A path is required and cannot be null, empty or whitespace", "path");
|
|
||||||
}
|
|
||||||
|
|
||||||
string outPath = path;
|
|
||||||
|
|
||||||
// We need the length of the absolute path in order to prepare a buffer of
|
|
||||||
// the correct size
|
|
||||||
uint bufferSize = NativeMethods.GetFullPathName(path, 0, null, null);
|
|
||||||
int lastWin32Error = Marshal.GetLastWin32Error();
|
|
||||||
|
|
||||||
if (bufferSize > 0)
|
|
||||||
{
|
|
||||||
var absolutePath = new StringBuilder((int)bufferSize);
|
|
||||||
uint length = NativeMethods.GetFullPathName(path, bufferSize, absolutePath, null);
|
|
||||||
lastWin32Error = Marshal.GetLastWin32Error();
|
|
||||||
|
|
||||||
if (length > 0)
|
|
||||||
{
|
|
||||||
outPath = absolutePath.ToString();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Path resolution failed
|
|
||||||
throw new Win32Exception(
|
|
||||||
lastWin32Error,
|
|
||||||
String.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"Path normalization/expansion failed. The path length was not returned by the Kernel32 subsystem for '{0}'.",
|
|
||||||
path
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Path resolution failed and the path length could not
|
|
||||||
// be determined
|
|
||||||
throw new Win32Exception(
|
|
||||||
lastWin32Error,
|
|
||||||
String.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"Path normalization/expansion failed. A full path was not returned by the Kernel32 subsystem for '{0}'.",
|
|
||||||
path
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return outPath != null ? outPath.TrimEnd('\\') : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether the specified path is an absolute path or not.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path to be tested.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// <c>true</c> if the path is absolute; otherwise, <c>false</c>.
|
|
||||||
/// </returns>
|
|
||||||
public static bool IsAbsolutePath(string path)
|
|
||||||
{
|
|
||||||
return LongPathUtility.AbsolutePathRegEx.Match(path).Success;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string RemoveExtendedLengthPathPrefix(string inPath)
|
|
||||||
{
|
|
||||||
string outPath = inPath;
|
|
||||||
|
|
||||||
if (!String.IsNullOrWhiteSpace(inPath))
|
|
||||||
{
|
|
||||||
if (inPath.StartsWith("\\", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
// ex: \\?\UNC\server\share to \\server\share
|
|
||||||
outPath = inPath.Replace(@"\\?\UNC", @"\");
|
|
||||||
|
|
||||||
// ex: \\?\c:\windows to c:\windows
|
|
||||||
outPath = outPath.Replace(@"\\?\", String.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return outPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CombinePaths(string pathA, string pathB)
|
|
||||||
{
|
|
||||||
if (pathA == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("pathA");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathB == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("pathB");
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Path class does not suffer from the 248/260 character limitation
|
|
||||||
// that the File and Directory classes do.
|
|
||||||
return Path.Combine(
|
|
||||||
pathA.TrimEnd('\\'),
|
|
||||||
pathB.TrimStart('\\')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ConvertToExtendedLengthPath(string path)
|
|
||||||
{
|
|
||||||
string extendedLengthPath = GetFullNormalizedPath(path);
|
|
||||||
|
|
||||||
if (!String.IsNullOrWhiteSpace(extendedLengthPath))
|
|
||||||
{
|
|
||||||
//no need to modify- it's already unicode
|
|
||||||
if (!extendedLengthPath.StartsWith(@"\\?", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
// ex: \\server\share
|
|
||||||
if (extendedLengthPath.StartsWith(@"\\", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
// make it \\?\UNC\server\share
|
|
||||||
extendedLengthPath = String.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
@"\\?\UNC{0}",
|
|
||||||
extendedLengthPath.Substring(1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else //not unicode already, and not UNC
|
|
||||||
{
|
|
||||||
extendedLengthPath = String.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
@"\\?\{0}",
|
|
||||||
extendedLengthPath
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return extendedLengthPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<string> EnumerateDirectoriesInPath(string path)
|
|
||||||
{
|
|
||||||
SafeFindHandle handle = null;
|
|
||||||
var findData = new FindData();
|
|
||||||
var childDirectories = new List<string>();
|
|
||||||
|
|
||||||
using (handle = NativeMethods.FindFirstFile(CombinePaths(ConvertToExtendedLengthPath(path), "*"), findData))
|
|
||||||
{
|
|
||||||
if (!handle.IsInvalid)
|
|
||||||
{
|
|
||||||
bool searchComplete = false;
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
// skip the dot directories
|
|
||||||
if (!findData.fileName.Equals(@".") && !findData.fileName.Equals(@".."))
|
|
||||||
{
|
|
||||||
if ((findData.fileAttributes & (int)FileAttributes.Directory) != 0)
|
|
||||||
{
|
|
||||||
childDirectories.Add(RemoveExtendedLengthPathPrefix(CombinePaths(path, findData.fileName)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (NativeMethods.FindNextFile(handle, findData))
|
|
||||||
{
|
|
||||||
if (handle.IsInvalid)
|
|
||||||
{
|
|
||||||
throw new Win32Exception(
|
|
||||||
Marshal.GetLastWin32Error(),
|
|
||||||
String.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"Enumerating subdirectories for path '{0}' failed.",
|
|
||||||
path
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
searchComplete = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} while (!searchComplete);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return childDirectories;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<string> EnumerateFilesInPath(string path, string matchPattern)
|
|
||||||
{
|
|
||||||
SafeFindHandle handle = null;
|
|
||||||
var findData = new FindData();
|
|
||||||
var fullFilePaths = new List<string>();
|
|
||||||
|
|
||||||
using (handle = NativeMethods.FindFirstFile(CombinePaths(ConvertToExtendedLengthPath(path), matchPattern), findData))
|
|
||||||
{
|
|
||||||
int lastWin32Error = Marshal.GetLastWin32Error();
|
|
||||||
|
|
||||||
if (handle.IsInvalid)
|
|
||||||
{
|
|
||||||
if (lastWin32Error != ERROR_FILE_NOT_FOUND)
|
|
||||||
{
|
|
||||||
throw new Win32Exception(
|
|
||||||
lastWin32Error,
|
|
||||||
String.Format(CultureInfo.InvariantCulture, "Enumerating files for path '{0}' failed.", path)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bool searchComplete = false;
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
// skip the dot directories
|
|
||||||
if (!findData.fileName.Equals(@".") && !findData.fileName.Equals(@".."))
|
|
||||||
{
|
|
||||||
if ((findData.fileAttributes & (int)FileAttributes.Directory) == 0)
|
|
||||||
{
|
|
||||||
fullFilePaths.Add(RemoveExtendedLengthPathPrefix(CombinePaths(path, findData.fileName)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (NativeMethods.FindNextFile(handle, findData))
|
|
||||||
{
|
|
||||||
lastWin32Error = Marshal.GetLastWin32Error();
|
|
||||||
|
|
||||||
if (handle.IsInvalid)
|
|
||||||
{
|
|
||||||
throw new Win32Exception(
|
|
||||||
lastWin32Error,
|
|
||||||
String.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"Enumerating subdirectories for path '{0}' failed.",
|
|
||||||
path
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
searchComplete = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} while (!searchComplete);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fullFilePaths;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void EnumerateFilesInternal(List<string> filePaths, string path, string matchPattern, bool recursiveSearch)
|
|
||||||
{
|
|
||||||
var fullFilePaths = EnumerateFilesInPath(path, matchPattern);
|
|
||||||
if (fullFilePaths.Any())
|
|
||||||
{
|
|
||||||
lock (filePaths)
|
|
||||||
{
|
|
||||||
filePaths.AddRange(fullFilePaths);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recursiveSearch)
|
|
||||||
{
|
|
||||||
var directorySearchPaths = EnumerateDirectoriesInPath(path);
|
|
||||||
if (directorySearchPaths.Any())
|
|
||||||
{
|
|
||||||
Parallel.ForEach(
|
|
||||||
directorySearchPaths,
|
|
||||||
(searchPath) =>
|
|
||||||
{
|
|
||||||
EnumerateFilesInternal(filePaths, searchPath, matchPattern, recursiveSearch);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void EnumerateDirectoriesInternal(List<string> directoryPaths, string path, bool recursiveSearch)
|
|
||||||
{
|
|
||||||
var directorySearchPaths = EnumerateDirectoriesInPath(path);
|
|
||||||
if (directorySearchPaths.Any())
|
|
||||||
{
|
|
||||||
lock (directoryPaths)
|
|
||||||
{
|
|
||||||
directoryPaths.AddRange(directorySearchPaths);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recursiveSearch)
|
|
||||||
{
|
|
||||||
// This will not ensure that the directory paths are added to the list
|
|
||||||
// in alphabetical order but does provide performance 2 - 4 times better than the
|
|
||||||
// canonical Directory.GetDirectories() method.
|
|
||||||
Parallel.ForEach(
|
|
||||||
directorySearchPaths,
|
|
||||||
(searchPath) =>
|
|
||||||
{
|
|
||||||
EnumerateDirectoriesInternal(directoryPaths, searchPath, recursiveSearch);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Kernel32.dll native interop methods for use with utility file/path parsing
|
|
||||||
/// operations
|
|
||||||
/// </summary>
|
|
||||||
private static class NativeMethods
|
|
||||||
{
|
|
||||||
private const string Kernel32Dll = "kernel32.dll";
|
|
||||||
|
|
||||||
[DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
public static extern bool FindClose(IntPtr hFindFile);
|
|
||||||
|
|
||||||
[SuppressMessage("Microsoft.Globalization", "CA2101:SpecifyMarshalingForPInvokeStringArguments", MessageId = "FindData.alternateFileName")]
|
|
||||||
[SuppressMessage("Microsoft.Globalization", "CA2101:SpecifyMarshalingForPInvokeStringArguments", MessageId = "FindData.fileName")]
|
|
||||||
[DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)]
|
|
||||||
public static extern SafeFindHandle FindFirstFile(
|
|
||||||
[MarshalAs(UnmanagedType.LPTStr)]
|
|
||||||
string fileName,
|
|
||||||
[In, Out] FindData findFileData
|
|
||||||
);
|
|
||||||
[SuppressMessage("Microsoft.Globalization", "CA2101:SpecifyMarshalingForPInvokeStringArguments", MessageId = "FindData.alternateFileName")]
|
|
||||||
[SuppressMessage("Microsoft.Globalization", "CA2101:SpecifyMarshalingForPInvokeStringArguments", MessageId = "FindData.fileName")]
|
|
||||||
[DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
public static extern bool FindNextFile(SafeFindHandle hFindFile, [In, Out] FindData lpFindFileData);
|
|
||||||
|
|
||||||
[DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)]
|
|
||||||
public static extern int GetFileAttributes(string lpFileName);
|
|
||||||
|
|
||||||
[DllImport(Kernel32Dll, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true, SetLastError = true)]
|
|
||||||
public static extern uint GetFullPathName(
|
|
||||||
[MarshalAs(UnmanagedType.LPTStr)]
|
|
||||||
string lpFileName,
|
|
||||||
uint nBufferLength,
|
|
||||||
[Out]
|
|
||||||
StringBuilder lpBuffer,
|
|
||||||
StringBuilder lpFilePart
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
//for mapping to the WIN32_FIND_DATA native structure
|
|
||||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter", Justification = "Reviewed.")]
|
|
||||||
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate", Justification = "Reviewed.")]
|
|
||||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
||||||
private sealed class FindData
|
|
||||||
{
|
|
||||||
// NOTE:
|
|
||||||
// Although it may seem correct to Marshal the string members of this class as UnmanagedType.LPWStr, they
|
|
||||||
// must explicitly remain UnmanagedType.ByValTStr with the size constraints noted. Otherwise we end up with
|
|
||||||
// COM Interop exceptions while trying to marshal the data across the PInvoke boundaries. We thus require the StyleCop
|
|
||||||
// suppressions on the NativeMethods.FindNextFile() method above.
|
|
||||||
public int fileAttributes;
|
|
||||||
public System.Runtime.InteropServices.ComTypes.FILETIME creationTime;
|
|
||||||
public System.Runtime.InteropServices.ComTypes.FILETIME lastAccessTime;
|
|
||||||
public System.Runtime.InteropServices.ComTypes.FILETIME lastWriteTime;
|
|
||||||
public int nFileSizeHigh;
|
|
||||||
public int nFileSizeLow;
|
|
||||||
public int dwReserved0;
|
|
||||||
public int dwReserved1;
|
|
||||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
|
|
||||||
public string fileName;
|
|
||||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
|
|
||||||
public string alternateFileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
//A Win32 safe find handle in which a return value of -1 indicates it's invalid
|
|
||||||
private sealed class SafeFindHandle : Microsoft.Win32.SafeHandles.SafeHandleMinusOneIsInvalid
|
|
||||||
{
|
|
||||||
public SafeFindHandle()
|
|
||||||
: base(true)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
|
|
||||||
protected override bool ReleaseHandle()
|
|
||||||
{
|
|
||||||
return NativeMethods.FindClose(handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
private enum FlagsAndAttributes : uint
|
|
||||||
{
|
|
||||||
None = 0x00000000,
|
|
||||||
Readonly = 0x00000001,
|
|
||||||
Hidden = 0x00000002,
|
|
||||||
System = 0x00000004,
|
|
||||||
Directory = 0x00000010,
|
|
||||||
Archive = 0x00000020,
|
|
||||||
Device = 0x00000040,
|
|
||||||
Normal = 0x00000080,
|
|
||||||
Temporary = 0x00000100,
|
|
||||||
SparseFile = 0x00000200,
|
|
||||||
ReparsePoint = 0x00000400,
|
|
||||||
Compressed = 0x00000800,
|
|
||||||
Offline = 0x00001000,
|
|
||||||
NotContentIndexed = 0x00002000,
|
|
||||||
Encrypted = 0x00004000,
|
|
||||||
Write_Through = 0x80000000,
|
|
||||||
Overlapped = 0x40000000,
|
|
||||||
NoBuffering = 0x20000000,
|
|
||||||
RandomAccess = 0x10000000,
|
|
||||||
SequentialScan = 0x08000000,
|
|
||||||
DeleteOnClose = 0x04000000,
|
|
||||||
BackupSemantics = 0x02000000,
|
|
||||||
PosixSemantics = 0x01000000,
|
|
||||||
OpenReparsePoint = 0x00200000,
|
|
||||||
OpenNoRecall = 0x00100000,
|
|
||||||
FirstPipeInstance = 0x00080000,
|
|
||||||
|
|
||||||
InvalidFileAttributes = 0xFFFFFFFF // Returned by GetFileAttributes on Non existant path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -218,7 +218,6 @@ namespace GitHub.Services.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NETSTANDARD
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Portable compliant way to get a constructor with specified arguments. This will return a constructor that is public or private as long as the arguments match. NULL will be returned if there is no match.
|
/// Portable compliant way to get a constructor with specified arguments. This will return a constructor that is public or private as long as the arguments match. NULL will be returned if there is no match.
|
||||||
/// Note that it will pick the first one it finds that matches, which is not necesarily the best match.
|
/// Note that it will pick the first one it finds that matches, which is not necesarily the best match.
|
||||||
@@ -263,7 +262,6 @@ namespace GitHub.Services.Common
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
private static PropertyInfo GetPublicInstancePropertyInfo(Type type, string name)
|
private static PropertyInfo GetPublicInstancePropertyInfo(Type type, string name)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,18 +1,4 @@
|
|||||||
// ************************************************************************************************
|
using System;
|
||||||
// Microsoft Team Foundation
|
|
||||||
//
|
|
||||||
// Microsoft Confidential
|
|
||||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
//
|
|
||||||
// File: VssStringComparer.cs
|
|
||||||
// Area: Team Foundation
|
|
||||||
// Classes: VssStringComparer
|
|
||||||
// Contents: The Team Foundation string comparison class provides inner classes
|
|
||||||
// that are used to provide semantic-specific Equals and Compare methods
|
|
||||||
// and a semantic-specific StringComparer instance. New semantics should
|
|
||||||
// be added on an as-needed basis.
|
|
||||||
// ************************************************************************************************
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace GitHub.Services.Common
|
namespace GitHub.Services.Common
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ namespace GitHub.Services.Common.Internal
|
|||||||
{
|
{
|
||||||
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
#if !NETSTANDARD
|
|
||||||
[CLSCompliant(false)]
|
|
||||||
#endif
|
|
||||||
public static class XmlUtility
|
public static class XmlUtility
|
||||||
{
|
{
|
||||||
internal static FileStream OpenFile(String path, FileShare sharing, Boolean saveFile)
|
internal static FileStream OpenFile(String path, FileShare sharing, Boolean saveFile)
|
||||||
|
|||||||
@@ -5,16 +5,6 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
|
|
||||||
namespace GitHub.Services.Common
|
namespace GitHub.Services.Common
|
||||||
{
|
{
|
||||||
public static class AdminConstants
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Each incoming web request is assigned a server process id, this constant defines
|
|
||||||
/// an element within the Context.Items[] to hold that value.
|
|
||||||
/// </summary>
|
|
||||||
public const String ServerProcessID = "serverProcessID";
|
|
||||||
public const String ApplicationName = "ApplicationName";
|
|
||||||
}
|
|
||||||
|
|
||||||
[GenerateSpecificConstants]
|
[GenerateSpecificConstants]
|
||||||
public static class IdentityConstants
|
public static class IdentityConstants
|
||||||
{
|
{
|
||||||
@@ -352,370 +342,6 @@ namespace GitHub.Services.Common
|
|||||||
public static readonly ISet<string> WhiteListedProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
public static readonly ISet<string> WhiteListedProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class DirectoryRoleConstants
|
|
||||||
{
|
|
||||||
/// Name of the directory role that represents "Company Administrator/Global Admin"
|
|
||||||
public const string CompanyAdministrator = "Company Administrator";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used with Registration entries
|
|
||||||
[GenerateSpecificConstants]
|
|
||||||
public static class ToolNames
|
|
||||||
{
|
|
||||||
public const string Framework = "Framework";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string VersionControl = "VersionControl";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string WorkItemTracking = "WorkItemTracking";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string RemoteWorkItemTracking = "RemoteWorkItemTracking";
|
|
||||||
public const string CoreServices = "vstfs";
|
|
||||||
public const string Warehouse = "Reports";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string TeamBuild = "Build";
|
|
||||||
public const string ProxyServer = "ps";
|
|
||||||
public const string TeamFoundation = "vstfs";
|
|
||||||
public const string SharePoint = "Wss";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string TestManagement = "TestManagement";
|
|
||||||
public const string LabManagement = "LabManagement";
|
|
||||||
public const string ReleaseManagement = "ReleaseManagement";
|
|
||||||
public const string SyncService = "SyncService";
|
|
||||||
public const string TestRig = "TestRig";
|
|
||||||
public const string TSWebAccess = "TSWebAccess";
|
|
||||||
public const string ProjectServer = "ProjectServer";
|
|
||||||
public const string DeploymentRig = "DeploymentRig";
|
|
||||||
public const string TeamProjects = "TeamProjects"; // contains specific project registration entries (project portal, process guidance and doc url)
|
|
||||||
public const string Discussion = "Discussion";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string Requirements = "Requirements";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string Hyperlink = "Hyperlink";
|
|
||||||
public const string Classification = "Classification";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string Legacy = "Legacy";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string CodeSense = "CodeSense";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string Git = "Git";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string CodeReview = "CodeReview";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string ProjectDownload = "ProjectDownload";
|
|
||||||
public const string DistributedTask = "DistributedTask";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string Wiki = "Wiki";
|
|
||||||
|
|
||||||
public const string Search = "Search";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string GitHub = "GitHub";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Artifact types
|
|
||||||
[GenerateSpecificConstants]
|
|
||||||
public static class ArtifactTypeNames
|
|
||||||
{
|
|
||||||
public const string Project = "TeamProject";
|
|
||||||
public const string Node = "Node";
|
|
||||||
public const string Collector = "Collector";
|
|
||||||
public const string TestResult = "TestResult";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string TcmResult = "TcmResult";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string TcmResultAttachment = "TcmResultAttachment";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string TcmTest = "TcmTest";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string Build = "Build";
|
|
||||||
public const string BuildAgent = "Agent";
|
|
||||||
public const string BuildDefinition = "Definition";
|
|
||||||
public const string BuildController = "Controller";
|
|
||||||
public const string BuildGroup = "Group";
|
|
||||||
public const string BuildRequest = "Request";
|
|
||||||
public const string BuildServiceHost = "ServiceHost";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string VersionedItem = "VersionedItem";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string LatestItemVersion = "LatestItemVersion";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string Changeset = "Changeset";
|
|
||||||
public const string Label = "Label";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string Shelveset = "Shelveset";
|
|
||||||
public const string ShelvedItem = "ShelvedItem";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string WorkItem = "WorkItem";
|
|
||||||
public const string Query = "Query";
|
|
||||||
public const string Results = "Results";
|
|
||||||
public const string LabEnvironment = "LabEnvironment";
|
|
||||||
public const string LabTemplate = "LabTemplate";
|
|
||||||
public const string LabSystem = "LabSystem";
|
|
||||||
public const string TeamProjectHostGroup = "TeamProjectHostGroup";
|
|
||||||
public const string TeamProjectLibraryShare = "TeamProjectLibraryShare";
|
|
||||||
public const string TeamProjectCollectionLibraryShare = "TeamProjectCollectionLibraryShare";
|
|
||||||
public const string TeamProjectCollectionHostGroup = "TeamProjectCollectionHostGroup";
|
|
||||||
public const string TestMachine = "TestMachine";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string Storyboard = "Storyboard";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string Commit = "Commit";
|
|
||||||
public const string LaunchLatestVersionedItem = "LaunchLatestVersionedItem";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string CodeReviewId = "CodeReviewId";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string CodeReviewSdkId = "ReviewId";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string PullRequestId = "PullRequestId";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string ProjectDownloadProject = "Project";
|
|
||||||
/// <summary>
|
|
||||||
/// A Git Ref
|
|
||||||
/// </summary>
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string Ref = "Ref";
|
|
||||||
|
|
||||||
public const string TaskAgentPoolMaintenance = "PoolMaintenance";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string WikiPage = "WikiPage";
|
|
||||||
|
|
||||||
// GitHub
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string PullRequest = "PullRequest";
|
|
||||||
[GenerateConstant]
|
|
||||||
public const string Issue = "Issue";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constant strings used in Notifications
|
|
||||||
/// </summary>
|
|
||||||
public static class NotificationConstants
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Macro used in subscriptions which will be replaced by the project name when evaluated
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.MyProjectNameMacro in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String MyProjectNameMacro = "@@MyProjectName@@";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Macro used in subscriptions which will be replaced by the subscriber's Display Name when evaluated
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.MyDisplayNameMacro in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String MyDisplayNameMacro = "@@MyDisplayName@@";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Macro used in subscriptions which will be replaced by the subscriber's Unique User Name when evaluated
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.MyUniqueNameMacro in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String MyUniqueNameMacro = "@@MyUniqueName@@";
|
|
||||||
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.SingleQuoteNameMacro in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String SingleQuoteNameMacro = "@@SQBDQ@@"; //SingleQuoteBetweenDoubleQuotes
|
|
||||||
|
|
||||||
[Obsolete]
|
|
||||||
public const String SingleQuoteValue = "\"'\""; //"'"
|
|
||||||
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.DoubleQuoteNameMacro in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String DoubleQuoteNameMacro = "@@DQBSQ@@"; //DoubleQuoteBetweenSingleQuotes
|
|
||||||
|
|
||||||
[Obsolete]
|
|
||||||
public const String DoubleQuoteValue = "'\"'"; //'"'
|
|
||||||
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.SingleQuoteCharMacro in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String SingleQuoteCharMacro = "@@SingleQuote@@";
|
|
||||||
|
|
||||||
[Obsolete]
|
|
||||||
public const String SingleQuoteCharValue = "'";
|
|
||||||
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.DoubleQuoteCharMacro in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String DoubleQuoteCharMacro = "@@DoubleQuote@@";
|
|
||||||
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.DoubleQuoteCharValue in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String DoubleQuoteCharValue = "\"";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Token used in subscription addresses to identify dynamic delivery targets computed from the source event
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.DynamicTargetsToken in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String DynamicTargetsToken = "@@";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// TeamFoundationIdentity property name for a user's custom list of Email addresses to receive notifications at
|
|
||||||
/// </summary>
|
|
||||||
public const String CustomNotificationAddressesIdentityProperty = "CustomNotificationAddresses";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// TeamFoundationIdentity propery name for a user's confirmed Email address to receive notifications. This is used in Hosted environments only.
|
|
||||||
/// </summary>
|
|
||||||
public const string ConfirmedNotificationAddressIdentityProperty = "ConfirmedNotificationAddress";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the WorkItemChangedEvent
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.LegacyNames.WorkItemChangedEvent in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const string WorkItemChangedEventTypeName = "WorkItemChangedEvent";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the BuildStatusChangedEvent type
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.LegacyNames.BuildStatusChangeEvent in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String BuildStatusChangeEventName = "BuildStatusChangeEvent";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the BuildCompletedEvent type
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.LegacyNames.BuildCompletedEvent in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String BuildCompletedEventName = "BuildCompletedEvent";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the CheckinEvent type
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.LegacyNames.CheckinEvent in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String CheckinEventName = "CheckinEvent";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the CodeReviewChangedEvent type
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.LegacyNames.CodeReviewChangedEvent in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String CodeReviewChangedEventName = "CodeReviewChangedEvent";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the GitPushEvent type
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.LegacyNames.GitPushEvent in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String GitPushEventName = "GitPushEvent";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the GitPullRequestEvent type
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.LegacyNames.GitPullRequestEvent in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String GitPullRequestEventName = "GitPullRequestEvent";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The relative path to the alerts admin web page
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationUrlConstants.AlertsPageRelativePath in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String AlertsPageRelativePath = "{0}#id={1}&showteams={2}";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The alerts page name
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationUrlConstants.AlertsPage in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String AlertsPage = "_Alerts";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The admin alerts page
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationUrlConstants.AlertsAdminPage in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String AlertsAdminPage = "_admin/_Alerts";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Property used to keep track of how many confirmations were sent for this user. Used to limit the number
|
|
||||||
/// of confirmations a single user is allowed to send out for their account.
|
|
||||||
/// The value is updated and monitored by the SendEmailConfirmationJob.
|
|
||||||
/// </summary>
|
|
||||||
public const string EmailConfirmationSendDates = "EmailConfirmationSendDates";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prefix to denote that identity field value have been processed
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.ProcessedFlagCharacter in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const Char ProcessedFlagCharacter = (Char)7;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prefix to denote that identity field value have been processed and converted to TFID
|
|
||||||
/// </summary>
|
|
||||||
/// [Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.ProcessedTfIdFlagCharacter in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const Char ProcessedTfIdFlagCharacter = (Char)11;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prefix to denote that this is the start of displayname value for this identity field
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.DisplayNameFlagCharacter in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const Char DisplayNameFlagCharacter = '|';
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prefix to denote that this is the start of TFID value for this identity field
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.TfIdFlagCharacter in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const Char TfIdFlagCharacter = '%';
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional Feature flag to enable escaping Regex expressions when creating Notification subscriptions.
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.FeatureFlags.AllowUserRegexInMatchConditionFeatureFlag in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const string AllowUserRegexInMatchConditionFeatureFlag = "VisualStudio.Services.Notifications.AllowUserRegexInMatchCondition";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The MDM scope name for the notification job
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.MDMConstants.MDMNotificationJobScope in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const string MDMNotificationJobScope = "NotificationJob";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event processing delay KPI name
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.MDMConstants.EventProcessingDelayKPI in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const string EventProcessingDelayKPI = "EventProcessingDelayInMs";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event processing delay KPI description
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.MDMConstants.EventProcessingDelayKPIDesc in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const string EventProcessingDelayKPIDesc = "Time taken to start processing an event";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The MDM scope name for the delivery job
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.MDMConstants.MDMDeliveryJobscope in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const string MDMDeliveryJobscope = "NotificationDeliveryJob";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Notification delivery delay KPI name
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.MDMConstants.DeliveryDelayKPI in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const string DeliveryDelayKPI = "NotificationDeliveryDelayInMs";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Notification delivery delay with retries KPI name
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.MDMConstants.DeliveryDelayWithRetriesKPI in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const string DeliveryDelayWithRetriesKPI = "NotificationDeliveryDelayWithRetriesInMs";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Total time taken between the event creation till the notification delivery
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.MDMConstants.TotalProcessingTimeKPI in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const string TotalProcessingTimeKPI = "EventProcessingTimeInMs";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Total time taken between the event creation till the notification delivery
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.MDMConstants.TotalProcessingTimeWithRetriesKPI in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const string TotalProcessingTimeWithRetriesKPI = "EventProcessingTimeWithRetriesInMs";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Notification delivery delay KPI description
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.MDMConstants.DeliveryDelayKPIDesc in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const string DeliveryDelayKPIDesc = "Time taken to start deliverying a notification";
|
|
||||||
|
|
||||||
// caching key for our notification bridge interface
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.BridgeKey in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const String BridgeKey = "@NotifBridge";
|
|
||||||
|
|
||||||
// delivery retry count registryKey
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.RetryCountRegistryKey in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const string RetryCountRegistryKey = "NotificationRetryCount";
|
|
||||||
|
|
||||||
// delivery retry count default value
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.RetryCountDefaultValue in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public const Int32 RetryCountDefaultValue = 5;
|
|
||||||
|
|
||||||
// the collection scope Guid
|
|
||||||
[Obsolete("Moved to GitHub.Services.Notifications.Common.NotificationFrameworkConstants.CollectionScope in assembly MS.VS.Services.Notifications.WebApi")]
|
|
||||||
public static Guid CollectionScope = new Guid("00000000-0000-636f-6c6c-656374696f6e");
|
|
||||||
}
|
|
||||||
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
public class LocationSecurityConstants
|
public class LocationSecurityConstants
|
||||||
{
|
{
|
||||||
@@ -732,16 +358,6 @@ namespace GitHub.Services.Common
|
|||||||
public const Int32 AllPermissions = Read | Write;
|
public const Int32 AllPermissions = Read | Write;
|
||||||
}
|
}
|
||||||
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public class SecuritySecurityConstants
|
|
||||||
{
|
|
||||||
public static readonly Guid NamespaceId = new Guid("9A82C708-BFBE-4F31-984C-E860C2196781");
|
|
||||||
public const char Separator = '/';
|
|
||||||
public const String RootToken = "";
|
|
||||||
|
|
||||||
public const int Read = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
public class GraphSecurityConstants
|
public class GraphSecurityConstants
|
||||||
{
|
{
|
||||||
@@ -753,158 +369,6 @@ namespace GitHub.Services.Common
|
|||||||
public const int ReadByPersonalIdentifier = 2;
|
public const int ReadByPersonalIdentifier = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public static class TeamProjectSecurityConstants
|
|
||||||
{
|
|
||||||
public static readonly Guid NamespaceId = new Guid("52D39943-CB85-4d7f-8FA8-C6BAAC873819");
|
|
||||||
|
|
||||||
// Existed in Orcas
|
|
||||||
public static readonly Int32 GenericRead = 1;
|
|
||||||
public static readonly Int32 GenericWrite = 2;
|
|
||||||
public static readonly Int32 Delete = 4;
|
|
||||||
public static readonly Int32 PublishTestResults = 8;
|
|
||||||
public static readonly Int32 AdministerBuild = 16;
|
|
||||||
public static readonly Int32 StartBuild = 32;
|
|
||||||
public static readonly Int32 EditBuildStatus = 64;
|
|
||||||
public static readonly Int32 UpdateBuild = 128;
|
|
||||||
public static readonly Int32 DeleteTestResults = 256;
|
|
||||||
public static readonly Int32 ViewTestResults = 512;
|
|
||||||
|
|
||||||
// Dev10 Beta1
|
|
||||||
public static readonly Int32 ManageTestEnvironments = 2048;
|
|
||||||
|
|
||||||
// Dev10 Beta2
|
|
||||||
public static readonly Int32 ManageTestConfigurations = 4096;
|
|
||||||
|
|
||||||
// Dev14 Update 2 / VSO (M91)
|
|
||||||
public static readonly Int32 WorkItemDelete = 8192;
|
|
||||||
|
|
||||||
// Dev14 Update 2 / VSO (M92)
|
|
||||||
public static readonly Int32 WorkItemMove = 16384;
|
|
||||||
|
|
||||||
// Dev14 Update 2 / VSO (M94)
|
|
||||||
public static readonly Int32 WorkItemPermanentlyDelete = 32768;
|
|
||||||
|
|
||||||
// Dev15 / VSO (M99)
|
|
||||||
public static readonly Int32 Rename = 65536;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The permission required for setting project properties.
|
|
||||||
/// Introduced in Dev15 Update 2 / VSO (M116).
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Int32 ManageProperties = 131072;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The permission required for setting system project properties.
|
|
||||||
/// Introduced in Dev15 Update 2 / VSO (M116).
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This permission was excluded from AllPermissions to avoid being unintentionally granted.
|
|
||||||
/// </remarks>
|
|
||||||
public static readonly Int32 ManageSystemProperties = 262144;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The permission required for bypassing the project property cache.
|
|
||||||
/// Introduced in Dev16 / VSO (M118).
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This permission was excluded from AllPermissions to avoid being unintentionally granted.
|
|
||||||
/// </remarks>
|
|
||||||
public static readonly Int32 BypassPropertyCache = 524288;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The permission required for bypassing the rules while updating work items.
|
|
||||||
/// Introduced in Dev16 / VSO (M126).
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Int32 BypassRules= 1048576;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The permission required for suppressing notifications for work item updates.
|
|
||||||
/// Introduced in Dev16 / VSO (M126).
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Int32 SuppressNotifications= 2097152;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The permission required for updating project visibility.
|
|
||||||
/// Introduced in Dev16 / VSO (M131).
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Int32 UpdateVisibility = 4194304;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The permission required for changing the process of the team project
|
|
||||||
/// Introduced in Dev17 / VSO (M136).
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Int32 ChangeProjectsProcess = 8388608;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The permission required for granting access to backlog management. For stakeholder, this would disabled for private project and enabled for public project.
|
|
||||||
/// Introduced in Dev17 / VSO (M137).
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This permission was excluded from AllPermissions to avoid being unintentionally granted.
|
|
||||||
/// </remarks>
|
|
||||||
public static readonly Int32 AgileToolsBacklogManagement = 16777216;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The permission required for granting access to backlog management. For stakeholder, this is always disabled.
|
|
||||||
/// Introduced in Dev17 / VSO (M150).
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This permission was excluded from AllPermissions to avoid being unintentionally granted.
|
|
||||||
/// </remarks>
|
|
||||||
public static readonly Int32 AgileToolsPlans = 33554432;
|
|
||||||
|
|
||||||
public static readonly Int32 AllPermissions =
|
|
||||||
GenericRead |
|
|
||||||
GenericWrite |
|
|
||||||
Delete |
|
|
||||||
PublishTestResults |
|
|
||||||
AdministerBuild |
|
|
||||||
StartBuild |
|
|
||||||
EditBuildStatus |
|
|
||||||
UpdateBuild |
|
|
||||||
DeleteTestResults |
|
|
||||||
ViewTestResults |
|
|
||||||
ManageTestEnvironments |
|
|
||||||
ManageTestConfigurations |
|
|
||||||
WorkItemDelete |
|
|
||||||
WorkItemMove |
|
|
||||||
WorkItemPermanentlyDelete |
|
|
||||||
Rename |
|
|
||||||
ManageProperties |
|
|
||||||
BypassRules |
|
|
||||||
SuppressNotifications |
|
|
||||||
UpdateVisibility |
|
|
||||||
ChangeProjectsProcess;
|
|
||||||
|
|
||||||
public const String ProjectTokenPrefix = "$PROJECT:";
|
|
||||||
|
|
||||||
public static String GetToken(String projectUri)
|
|
||||||
{
|
|
||||||
if (String.IsNullOrEmpty(projectUri) || !projectUri.StartsWith(ProjectTokenPrefix, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (projectUri == null)
|
|
||||||
{
|
|
||||||
projectUri = String.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProjectTokenPrefix + projectUri + ":";
|
|
||||||
}
|
|
||||||
|
|
||||||
return projectUri + ":";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public static class ContentValidationSecurityConstants
|
|
||||||
{
|
|
||||||
public static readonly Guid NamespaceId = new Guid("B1982126-CB90-4479-BDFD-CBF193241CB8");
|
|
||||||
public static readonly string ViolationsToken = "Violations";
|
|
||||||
|
|
||||||
public const int Read = 1;
|
|
||||||
public const int Write = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum WinHttpErrorCode
|
public enum WinHttpErrorCode
|
||||||
{
|
{
|
||||||
WINHTTP_ERROR_BASE = 12000,
|
WINHTTP_ERROR_BASE = 12000,
|
||||||
|
|||||||
@@ -124,9 +124,6 @@ namespace GitHub.Services.Common
|
|||||||
LogException = (bool)info.GetValue("m_logException", typeof(bool));
|
LogException = (bool)info.GetValue("m_logException", typeof(bool));
|
||||||
ReportException = (bool)info.GetValue("m_reportException", typeof(bool));
|
ReportException = (bool)info.GetValue("m_reportException", typeof(bool));
|
||||||
ErrorCode = (int)info.GetValue("m_errorCode", typeof(int));
|
ErrorCode = (int)info.GetValue("m_errorCode", typeof(int));
|
||||||
#if !NETSTANDARD
|
|
||||||
LogLevel = (EventLogEntryType)info.GetValue("m_logLevel", typeof(EventLogEntryType));
|
|
||||||
#endif
|
|
||||||
EventId = (int)info.GetValue("m_eventId", typeof(int));
|
EventId = (int)info.GetValue("m_eventId", typeof(int));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,9 +134,6 @@ namespace GitHub.Services.Common
|
|||||||
info.AddValue("m_logException", LogException);
|
info.AddValue("m_logException", LogException);
|
||||||
info.AddValue("m_reportException", ReportException);
|
info.AddValue("m_reportException", ReportException);
|
||||||
info.AddValue("m_errorCode", ErrorCode);
|
info.AddValue("m_errorCode", ErrorCode);
|
||||||
#if !NETSTANDARD
|
|
||||||
info.AddValue("m_logLevel", LogLevel);
|
|
||||||
#endif
|
|
||||||
info.AddValue("m_eventId", EventId);
|
info.AddValue("m_eventId", EventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,22 +151,6 @@ namespace GitHub.Services.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !NETSTANDARD
|
|
||||||
/// <summary>The event log entry type to use when logging the exception</summary>
|
|
||||||
/// <value>One of the event log entry types: </value>
|
|
||||||
public EventLogEntryType LogLevel
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return m_logLevel;
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
m_logLevel = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/// <summary>A user-defined error code.</summary>
|
/// <summary>A user-defined error code.</summary>
|
||||||
public int ErrorCode
|
public int ErrorCode
|
||||||
{
|
{
|
||||||
@@ -279,10 +257,6 @@ namespace GitHub.Services.Common
|
|||||||
private bool m_reportException;
|
private bool m_reportException;
|
||||||
private int m_errorCode;
|
private int m_errorCode;
|
||||||
|
|
||||||
#if !NETSTANDARD
|
|
||||||
private EventLogEntryType m_logLevel = EventLogEntryType.Warning;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private int m_eventId = DefaultExceptionEventId;
|
private int m_eventId = DefaultExceptionEventId;
|
||||||
|
|
||||||
//From EventLog.cs in Framework.
|
//From EventLog.cs in Framework.
|
||||||
|
|||||||
@@ -33,13 +33,7 @@ namespace GitHub.Services.Common
|
|||||||
public VssHttpMessageHandler(
|
public VssHttpMessageHandler(
|
||||||
VssCredentials credentials,
|
VssCredentials credentials,
|
||||||
VssHttpRequestSettings settings)
|
VssHttpRequestSettings settings)
|
||||||
: this(credentials, settings,
|
: this(credentials, settings, new HttpClientHandler())
|
||||||
#if !NETSTANDARD
|
|
||||||
new WebRequestHandler()
|
|
||||||
#else
|
|
||||||
new HttpClientHandler()
|
|
||||||
#endif
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,13 +70,7 @@ namespace GitHub.Services.Common
|
|||||||
m_transportHandler = transportHandler;
|
m_transportHandler = transportHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NETSTANDARD
|
|
||||||
//.Net Core does not recognize CredentialCache.DefaultCredentials if we wrap them with CredentialWrapper
|
|
||||||
bool isDefaultCredentials = credentials != null && credentials.Windows != null && credentials.Windows.UseDefaultCredentials;
|
|
||||||
ApplySettings(m_transportHandler, isDefaultCredentials ? CredentialCache.DefaultCredentials : m_credentialWrapper, this.Settings);
|
|
||||||
#else
|
|
||||||
ApplySettings(m_transportHandler, m_credentialWrapper, this.Settings);
|
ApplySettings(m_transportHandler, m_credentialWrapper, this.Settings);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -139,35 +127,6 @@ namespace GitHub.Services.Common
|
|||||||
var traceInfo = VssHttpMessageHandlerTraceInfo.GetTraceInfo(request);
|
var traceInfo = VssHttpMessageHandlerTraceInfo.GetTraceInfo(request);
|
||||||
traceInfo?.TraceHandlerStartTime();
|
traceInfo?.TraceHandlerStartTime();
|
||||||
|
|
||||||
#if !NETSTANDARD
|
|
||||||
// This action is deferred from ApplySettings because we want don't want to do it if we aren't
|
|
||||||
// talking to an HTTPS endpoint.
|
|
||||||
if (!m_appliedClientCertificatesToTransportHandler &&
|
|
||||||
request.RequestUri.Scheme == "https")
|
|
||||||
{
|
|
||||||
WebRequestHandler webRequestHandler = m_transportHandler as WebRequestHandler;
|
|
||||||
if (webRequestHandler != null &&
|
|
||||||
this.Settings.ClientCertificateManager != null &&
|
|
||||||
this.Settings.ClientCertificateManager.ClientCertificates != null &&
|
|
||||||
this.Settings.ClientCertificateManager.ClientCertificates.Count > 0)
|
|
||||||
{
|
|
||||||
webRequestHandler.ClientCertificates.AddRange(this.Settings.ClientCertificateManager.ClientCertificates);
|
|
||||||
}
|
|
||||||
m_appliedClientCertificatesToTransportHandler = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_appliedServerCertificateValidationCallbackToTransportHandler &&
|
|
||||||
request.RequestUri.Scheme == "https")
|
|
||||||
{
|
|
||||||
WebRequestHandler webRequestHandler = m_transportHandler as WebRequestHandler;
|
|
||||||
if (webRequestHandler != null &&
|
|
||||||
this.Settings.ServerCertificateValidationCallback != null)
|
|
||||||
{
|
|
||||||
webRequestHandler.ServerCertificateValidationCallback = this.Settings.ServerCertificateValidationCallback;
|
|
||||||
}
|
|
||||||
m_appliedServerCertificateValidationCallbackToTransportHandler = true;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
if (!m_appliedClientCertificatesToTransportHandler &&
|
if (!m_appliedClientCertificatesToTransportHandler &&
|
||||||
request.RequestUri.Scheme == "https")
|
request.RequestUri.Scheme == "https")
|
||||||
{
|
{
|
||||||
@@ -201,7 +160,6 @@ namespace GitHub.Services.Common
|
|||||||
{
|
{
|
||||||
request.Version = HttpVersion.Version11;
|
request.Version = HttpVersion.Version11;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
IssuedToken token = null;
|
IssuedToken token = null;
|
||||||
IssuedTokenProvider provider;
|
IssuedTokenProvider provider;
|
||||||
@@ -540,16 +498,11 @@ namespace GitHub.Services.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IWebProxy s_defaultWebProxy =
|
// setting this to WebRequest.DefaultWebProxy in NETSTANDARD is causing a System.PlatformNotSupportedException
|
||||||
#if !NETSTANDARD
|
//.in System.Net.SystemWebProxy.IsBypassed. Comment in IsBypassed method indicates ".NET Core and .NET Native
|
||||||
WebRequest.DefaultWebProxy;
|
// code will handle this exception and call into WinInet/WinHttp as appropriate to use the system proxy."
|
||||||
#else
|
// This needs to be investigated further.
|
||||||
// setting this to WebRequest.DefaultWebProxy in NETSTANDARD is causing a System.PlatformNotSupportedException
|
private static IWebProxy s_defaultWebProxy = null;
|
||||||
//.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.
|
|
||||||
null;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// 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.
|
||||||
@@ -585,13 +538,9 @@ namespace GitHub.Services.Common
|
|||||||
private bool m_appliedServerCertificateValidationCallbackToTransportHandler;
|
private bool m_appliedServerCertificateValidationCallbackToTransportHandler;
|
||||||
private readonly HttpMessageHandler m_transportHandler;
|
private readonly HttpMessageHandler m_transportHandler;
|
||||||
|
|
||||||
#if NETSTANDARD
|
|
||||||
//.Net Core does not attempt NTLM schema on Linux, unless ICredentials is a CredentialCache instance
|
//.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
|
//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
|
private sealed class CredentialWrapper : CredentialCache, ICredentials
|
||||||
#else
|
|
||||||
private sealed class CredentialWrapper : ICredentials
|
|
||||||
#endif
|
|
||||||
{
|
{
|
||||||
public ICredentials InnerCredentials
|
public ICredentials InnerCredentials
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,9 +44,7 @@ namespace GitHub.Services.Common
|
|||||||
this.SuppressFedAuthRedirects = true;
|
this.SuppressFedAuthRedirects = true;
|
||||||
this.ClientCertificateManager = null;
|
this.ClientCertificateManager = null;
|
||||||
this.ServerCertificateValidationCallback = null;
|
this.ServerCertificateValidationCallback = null;
|
||||||
#if NETSTANDARD
|
|
||||||
this.UseHttp11 = false;
|
this.UseHttp11 = false;
|
||||||
#endif
|
|
||||||
|
|
||||||
// If different, we'll also add CurrentCulture to the request headers,
|
// If different, we'll also add CurrentCulture to the request headers,
|
||||||
// but UICulture was added first, so it gets first preference
|
// but UICulture was added first, so it gets first preference
|
||||||
@@ -99,9 +97,7 @@ namespace GitHub.Services.Common
|
|||||||
this.ClientCertificateManager = copy.ClientCertificateManager;
|
this.ClientCertificateManager = copy.ClientCertificateManager;
|
||||||
this.ServerCertificateValidationCallback = copy.ServerCertificateValidationCallback;
|
this.ServerCertificateValidationCallback = copy.ServerCertificateValidationCallback;
|
||||||
this.MaxRetryRequest = copy.MaxRetryRequest;
|
this.MaxRetryRequest = copy.MaxRetryRequest;
|
||||||
#if NETSTANDARD
|
|
||||||
this.UseHttp11 = copy.UseHttp11;
|
this.UseHttp11 = copy.UseHttp11;
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -144,7 +140,6 @@ namespace GitHub.Services.Common
|
|||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NETSTANDARD
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The .NET Core 2.1 runtime switched its HTTP default from HTTP 1.1 to HTTP 2.
|
/// 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.
|
/// This causes problems with some versions of the Curl handler on Linux.
|
||||||
@@ -156,7 +151,6 @@ namespace GitHub.Services.Common
|
|||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the maximum size allowed for response content buffering.
|
/// Gets or sets the maximum size allowed for response content buffering.
|
||||||
@@ -266,15 +260,6 @@ namespace GitHub.Services.Common
|
|||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !NETSTANDARD
|
|
||||||
/// <summary>
|
|
||||||
/// Optional implementation used to validate server certificate validation
|
|
||||||
/// </summary>
|
|
||||||
public RemoteCertificateValidationCallback ServerCertificateValidationCallback
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional implementation used to validate server certificate validation
|
/// Optional implementation used to validate server certificate validation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -283,7 +268,6 @@ namespace GitHub.Services.Common
|
|||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of times to retry a request that has an ambient failure
|
/// Number of times to retry a request that has an ambient failure
|
||||||
@@ -359,13 +343,11 @@ namespace GitHub.Services.Common
|
|||||||
request.Headers.Add(Internal.HttpHeaders.VssAgentHeader, this.AgentId);
|
request.Headers.Add(Internal.HttpHeaders.VssAgentHeader, this.AgentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NETSTANDARD
|
|
||||||
// Content is being sent as chunked by default in dotnet5.4, which differs than the .net 4.5 behaviour.
|
// Content is being sent as chunked by default in dotnet5.4, which differs than the .net 4.5 behaviour.
|
||||||
if (request.Content != null && !request.Content.Headers.ContentLength.HasValue && !request.Headers.TransferEncodingChunked.HasValue)
|
if (request.Content != null && !request.Content.Headers.ContentLength.HasValue && !request.Headers.TransferEncodingChunked.HasValue)
|
||||||
{
|
{
|
||||||
request.Content.Headers.ContentLength = request.Content.ReadAsByteArrayAsync().Result.Length;
|
request.Content.Headers.ContentLength = request.Content.ReadAsByteArrayAsync().Result.Length;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,21 +216,13 @@ namespace GitHub.Services.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if !NETSTANDARD
|
|
||||||
else if (ex is System.Data.Services.Client.DataServiceRequestException ||
|
|
||||||
ex is System.Data.Services.Client.DataServiceClientException)
|
|
||||||
{
|
|
||||||
// WCF exceptions
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the HttpStatusCode which represents a throttling error.
|
/// Gets the HttpStatusCode which represents a throttling error.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const HttpStatusCode TooManyRequests = (HttpStatusCode)429;
|
public const HttpStatusCode TooManyRequests = (HttpStatusCode)429;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,30 +10,14 @@ namespace GitHub.Services.Common
|
|||||||
// See Toolsets\Version\Version.props for more details.
|
// See Toolsets\Version\Version.props for more details.
|
||||||
public const String MajorVersion = "16";
|
public const String MajorVersion = "16";
|
||||||
public const String MinorVersion = "0";
|
public const String MinorVersion = "0";
|
||||||
public const String BuildVersion = "65000";
|
|
||||||
public const String PatchVersion = "0";
|
|
||||||
public const String ProductVersion = MajorVersion + "." + MinorVersion;
|
public const String ProductVersion = MajorVersion + "." + MinorVersion;
|
||||||
|
|
||||||
// Assembly version (i.e. strong name)
|
// Assembly version (i.e. strong name)
|
||||||
public const String AssemblyMajorVersion = "16";
|
public const String AssemblyMajorVersion = "16";
|
||||||
public const String AssemblyMinorVersion = "0";
|
|
||||||
public const String AssemblyBuildVersion = "0";
|
|
||||||
public const String AssemblyPatchVersion = "0";
|
|
||||||
public const String AssemblyVersion = AssemblyMajorVersion + "." + AssemblyMinorVersion + "." + AssemblyBuildVersion + "." + AssemblyPatchVersion;
|
|
||||||
|
|
||||||
// File version
|
|
||||||
public const String FileMajorVersion = "16";
|
|
||||||
public const String FileMinorVersion = "255";
|
|
||||||
public const String FileBuildVersion = "65000";
|
|
||||||
public const String FilePatchVersion = "0";
|
|
||||||
public const String FileVersion = FileMajorVersion + "." + FileMinorVersion + "." + FileBuildVersion + "." + FilePatchVersion;
|
|
||||||
|
|
||||||
// Derived versions
|
// Derived versions
|
||||||
public const String TfsMajorVersion = "8";
|
public const String TfsMajorVersion = "8";
|
||||||
public const String TfsMinorVersion = "0";
|
public const String TfsMinorVersion = "0";
|
||||||
public const String TfsProductVersion = TfsMajorVersion + "." + TfsMinorVersion;
|
public const String TfsProductVersion = TfsMajorVersion + "." + TfsMinorVersion;
|
||||||
|
|
||||||
// On-premises TFS install folder
|
|
||||||
public const String TfsInstallDirectory = "Azure DevOps Server 2019";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
using GitHub.Services.Common.Internal;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
using GitHub.Services.WebApi.Patch;
|
|
||||||
using GitHub.Services.WebApi.Patch.Json;
|
|
||||||
|
|
||||||
namespace GitHub.Core.WebApi
|
|
||||||
{
|
|
||||||
[GenerateAllConstants]
|
|
||||||
public enum ProjectState
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Project is in the process of being deleted.
|
|
||||||
/// </summary>
|
|
||||||
[EnumMember]
|
|
||||||
Deleting = 2,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Project is in the process of being created.
|
|
||||||
/// </summary>
|
|
||||||
[EnumMember]
|
|
||||||
New = 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Project is completely created and ready to use.
|
|
||||||
/// </summary>
|
|
||||||
[EnumMember]
|
|
||||||
WellFormed = 1,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Project has been queued for creation, but the process has not yet started.
|
|
||||||
/// </summary>
|
|
||||||
[EnumMember]
|
|
||||||
CreatePending = 3,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// All projects regardless of state.
|
|
||||||
/// </summary>
|
|
||||||
[EnumMember]
|
|
||||||
All = -1, // Used for filtering.
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Project has not been changed.
|
|
||||||
/// </summary>
|
|
||||||
[EnumMember]
|
|
||||||
Unchanged = -2, // Used for updating projects.
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Project has been deleted.
|
|
||||||
/// </summary>
|
|
||||||
[EnumMember]
|
|
||||||
Deleted = 4, // Used for the project history.
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ProjectVisibility // Stored as a TINYINT
|
|
||||||
{
|
|
||||||
[ClientInternalUseOnly]
|
|
||||||
Unchanged = -1, // Used for updating projects.
|
|
||||||
/// <summary>
|
|
||||||
/// The project is only visible to users with explicit access.
|
|
||||||
/// </summary>
|
|
||||||
Private = 0,
|
|
||||||
/// <summary>
|
|
||||||
/// Enterprise level project visibility
|
|
||||||
/// </summary>
|
|
||||||
[ClientInternalUseOnly(omitFromTypeScriptDeclareFile: false)]
|
|
||||||
Organization = 1,
|
|
||||||
/// <summary>
|
|
||||||
/// The project is visible to all.
|
|
||||||
/// </summary>
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
Public = 2,
|
|
||||||
[ClientInternalUseOnly]
|
|
||||||
SystemPrivate = 3 // Soft-deleted projects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
|
|
||||||
namespace GitHub.Core.WebApi
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a shallow reference to a TeamProject.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
|
||||||
public class TeamProjectReference : ISecuredObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Default constructor to ensure we set up the project state correctly for serialization.
|
|
||||||
/// </summary>
|
|
||||||
public TeamProjectReference()
|
|
||||||
{
|
|
||||||
State = ProjectState.Unchanged;
|
|
||||||
Visibility = ProjectVisibility.Unchanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Project identifier.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 0, EmitDefaultValue = false)]
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Project abbreviation.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 1, EmitDefaultValue = false)]
|
|
||||||
public string Abbreviation { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Project name.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 2, EmitDefaultValue = false)]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The project's description (if any).
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 3, EmitDefaultValue = false)]
|
|
||||||
public string Description { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Url to the full version of the object.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 4, EmitDefaultValue = false)]
|
|
||||||
public string Url { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Project state.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 5)]
|
|
||||||
public ProjectState State { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Project revision.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 6, EmitDefaultValue = false)]
|
|
||||||
public Int64 Revision { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Project visibility.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 7)]
|
|
||||||
public ProjectVisibility Visibility { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Url to default team identity image.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 8, EmitDefaultValue = false)]
|
|
||||||
public String DefaultTeamImageUrl { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Project last update time.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(Order = 9)]
|
|
||||||
public DateTime LastUpdateTime { get; set; }
|
|
||||||
|
|
||||||
#region ISecuredObject
|
|
||||||
Guid ISecuredObject.NamespaceId => NamespaceId;
|
|
||||||
|
|
||||||
int ISecuredObject.RequiredPermissions => RequiredPermissions;
|
|
||||||
|
|
||||||
string ISecuredObject.GetToken()
|
|
||||||
{
|
|
||||||
return GetToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual Guid NamespaceId => TeamProjectSecurityConstants.NamespaceId;
|
|
||||||
|
|
||||||
protected virtual int RequiredPermissions => TeamProjectSecurityConstants.GenericRead;
|
|
||||||
|
|
||||||
protected virtual string GetToken()
|
|
||||||
{
|
|
||||||
// WE DON'T CARE THIS FOR NOW
|
|
||||||
return TeamProjectSecurityConstants.GetToken(Id.ToString("D"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Common.Contracts
|
|
||||||
{
|
|
||||||
[DataContract]
|
|
||||||
public class AuthorizationHeader : BaseSecuredObject
|
|
||||||
{
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String Name { get; set; }
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String Value { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Common.Contracts
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Represents binding of data source for the service endpoint request.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
|
||||||
public class DataSourceBindingBase : BaseSecuredObject
|
|
||||||
{
|
|
||||||
public DataSourceBindingBase()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected DataSourceBindingBase(DataSourceBindingBase inputDefinitionToClone)
|
|
||||||
: this(inputDefinitionToClone, null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected DataSourceBindingBase(DataSourceBindingBase inputDefinitionToClone, ISecuredObject securedObject)
|
|
||||||
: base(securedObject)
|
|
||||||
{
|
|
||||||
this.DataSourceName = inputDefinitionToClone.DataSourceName;
|
|
||||||
this.EndpointId = inputDefinitionToClone.EndpointId;
|
|
||||||
this.Target = inputDefinitionToClone.Target;
|
|
||||||
this.ResultTemplate = inputDefinitionToClone.ResultTemplate;
|
|
||||||
this.EndpointUrl = inputDefinitionToClone.EndpointUrl;
|
|
||||||
this.ResultSelector = inputDefinitionToClone.ResultSelector;
|
|
||||||
this.RequestVerb = inputDefinitionToClone.RequestVerb;
|
|
||||||
this.RequestContent = inputDefinitionToClone.RequestContent;
|
|
||||||
this.CallbackContextTemplate = inputDefinitionToClone.CallbackContextTemplate;
|
|
||||||
this.CallbackRequiredTemplate = inputDefinitionToClone.CallbackRequiredTemplate;
|
|
||||||
this.InitialContextTemplate = inputDefinitionToClone.InitialContextTemplate;
|
|
||||||
inputDefinitionToClone.Parameters.Copy(this.Parameters);
|
|
||||||
this.CloneHeaders(inputDefinitionToClone.Headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the name of the data source.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public string DataSourceName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the parameters for the data source.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public Dictionary<string, string> Parameters
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_parameters == null)
|
|
||||||
{
|
|
||||||
m_parameters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
return m_parameters;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public DataSourceBindingBase Clone(ISecuredObject securedObject)
|
|
||||||
{
|
|
||||||
return new DataSourceBindingBase(this, securedObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CloneHeaders(List<AuthorizationHeader> headers)
|
|
||||||
{
|
|
||||||
if (headers == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.Headers = headers.Select(header => new AuthorizationHeader { Name = header.Name, Value = header.Value }).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the endpoint Id.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String EndpointId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the target of the data source.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String Target { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the result template.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String ResultTemplate { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets http request verb
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String RequestVerb { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets http request body
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String RequestContent { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the url of the service endpoint.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String EndpointUrl { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the result selector.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String ResultSelector { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pagination format supported by this data source(ContinuationToken/SkipTop).
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String CallbackContextTemplate { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subsequent calls needed?
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String CallbackRequiredTemplate { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Defines the initial value of the query params
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String InitialContextTemplate { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the authorization headers.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public List<AuthorizationHeader> Headers { get; set; }
|
|
||||||
|
|
||||||
private Dictionary<String, String> m_parameters;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.DistributedTask.WebApi;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Common.Contracts
|
|
||||||
{
|
|
||||||
[DataContract]
|
|
||||||
public class ProcessParameters : BaseSecuredObject
|
|
||||||
{
|
|
||||||
public ProcessParameters()
|
|
||||||
: this(null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProcessParameters(ISecuredObject securedObject)
|
|
||||||
: this(null, securedObject)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private ProcessParameters(ProcessParameters toClone, ISecuredObject securedObject)
|
|
||||||
: base(securedObject)
|
|
||||||
{
|
|
||||||
if (toClone != null)
|
|
||||||
{
|
|
||||||
if (toClone.Inputs.Count > 0)
|
|
||||||
{
|
|
||||||
Inputs.AddRange(toClone.Inputs.Select(i => i.Clone(securedObject)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toClone.SourceDefinitions.Count > 0)
|
|
||||||
{
|
|
||||||
SourceDefinitions.AddRange(toClone.SourceDefinitions.Select(sd => sd.Clone(securedObject)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toClone.DataSourceBindings.Count > 0)
|
|
||||||
{
|
|
||||||
DataSourceBindings.AddRange(toClone.DataSourceBindings.Select(dsb => dsb.Clone(securedObject)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IList<TaskInputDefinitionBase> Inputs
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_inputs == null)
|
|
||||||
{
|
|
||||||
m_inputs = new List<TaskInputDefinitionBase>();
|
|
||||||
}
|
|
||||||
return m_inputs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IList<TaskSourceDefinitionBase> SourceDefinitions
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_sourceDefinitions == null)
|
|
||||||
{
|
|
||||||
m_sourceDefinitions = new List<TaskSourceDefinitionBase>();
|
|
||||||
}
|
|
||||||
return m_sourceDefinitions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IList<DataSourceBindingBase> DataSourceBindings
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_dataSourceBindings == null)
|
|
||||||
{
|
|
||||||
m_dataSourceBindings = new List<DataSourceBindingBase>();
|
|
||||||
}
|
|
||||||
return m_dataSourceBindings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return base.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
var processParameters2 = obj as ProcessParameters;
|
|
||||||
if (processParameters2 == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.Inputs == null && processParameters2.Inputs == null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((this.Inputs != null && processParameters2.Inputs == null)
|
|
||||||
|| (this.Inputs == null && processParameters2.Inputs != null))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.Inputs.Count != processParameters2.Inputs.Count)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var orderedProcessParameters1 = this.Inputs.Where(i => i != null).OrderBy(i => i.Name);
|
|
||||||
var orderedProcessParameters2 = processParameters2.Inputs.Where(i => i != null).OrderBy(i => i.Name);
|
|
||||||
|
|
||||||
if (!orderedProcessParameters1.OrderBy(i => i.Name).SequenceEqual(orderedProcessParameters2))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProcessParameters Clone(ISecuredObject securedObject = null)
|
|
||||||
{
|
|
||||||
return new ProcessParameters(this, securedObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
[OnDeserialized]
|
|
||||||
private void OnDeserialized(StreamingContext context)
|
|
||||||
{
|
|
||||||
SerializationHelper.Copy(ref m_serializedInputs, ref m_inputs, true);
|
|
||||||
SerializationHelper.Copy(ref m_serializedSourceDefinitions, ref m_sourceDefinitions, true);
|
|
||||||
SerializationHelper.Copy(ref m_serializedDataSourceBindings, ref m_dataSourceBindings, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
[OnSerializing]
|
|
||||||
private void OnSerializing(StreamingContext context)
|
|
||||||
{
|
|
||||||
SerializationHelper.Copy(ref m_inputs, ref m_serializedInputs);
|
|
||||||
SerializationHelper.Copy(ref m_sourceDefinitions, ref m_serializedSourceDefinitions);
|
|
||||||
SerializationHelper.Copy(ref m_dataSourceBindings, ref m_serializedDataSourceBindings);
|
|
||||||
}
|
|
||||||
|
|
||||||
[OnSerialized]
|
|
||||||
private void OnSerialized(StreamingContext context)
|
|
||||||
{
|
|
||||||
m_serializedInputs = null;
|
|
||||||
m_serializedSourceDefinitions = null;
|
|
||||||
m_serializedDataSourceBindings = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(Name = "Inputs", EmitDefaultValue = false)]
|
|
||||||
private List<TaskInputDefinitionBase> m_serializedInputs;
|
|
||||||
|
|
||||||
[DataMember(Name = "SourceDefinitions", EmitDefaultValue = false)]
|
|
||||||
private List<TaskSourceDefinitionBase> m_serializedSourceDefinitions;
|
|
||||||
|
|
||||||
[DataMember(Name = "DataSourceBindings", EmitDefaultValue = false)]
|
|
||||||
private List<DataSourceBindingBase> m_serializedDataSourceBindings;
|
|
||||||
|
|
||||||
private List<TaskInputDefinitionBase> m_inputs;
|
|
||||||
private List<TaskSourceDefinitionBase> m_sourceDefinitions;
|
|
||||||
private List<DataSourceBindingBase> m_dataSourceBindings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Common.Contracts
|
|
||||||
{
|
|
||||||
[DataContract]
|
|
||||||
public class TaskInputDefinitionBase : BaseSecuredObject
|
|
||||||
{
|
|
||||||
public TaskInputDefinitionBase()
|
|
||||||
{
|
|
||||||
InputType = TaskInputType.String;
|
|
||||||
DefaultValue = String.Empty;
|
|
||||||
Required = false;
|
|
||||||
HelpMarkDown = String.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected TaskInputDefinitionBase(TaskInputDefinitionBase inputDefinitionToClone)
|
|
||||||
: this(inputDefinitionToClone, null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected TaskInputDefinitionBase(TaskInputDefinitionBase inputDefinitionToClone, ISecuredObject securedObject)
|
|
||||||
: base(securedObject)
|
|
||||||
{
|
|
||||||
this.DefaultValue = inputDefinitionToClone.DefaultValue;
|
|
||||||
this.InputType = inputDefinitionToClone.InputType;
|
|
||||||
this.Label = inputDefinitionToClone.Label;
|
|
||||||
this.Name = inputDefinitionToClone.Name;
|
|
||||||
this.Required = inputDefinitionToClone.Required;
|
|
||||||
this.HelpMarkDown = inputDefinitionToClone.HelpMarkDown;
|
|
||||||
this.VisibleRule = inputDefinitionToClone.VisibleRule;
|
|
||||||
this.GroupName = inputDefinitionToClone.GroupName;
|
|
||||||
|
|
||||||
if (inputDefinitionToClone.Validation != null)
|
|
||||||
{
|
|
||||||
this.Validation = inputDefinitionToClone.Validation.Clone(securedObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputDefinitionToClone.m_aliases != null)
|
|
||||||
{
|
|
||||||
this.m_aliases = new List<String>(inputDefinitionToClone.m_aliases);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputDefinitionToClone.m_options != null)
|
|
||||||
{
|
|
||||||
this.m_options = new Dictionary<String, String>(inputDefinitionToClone.m_options);
|
|
||||||
}
|
|
||||||
if (inputDefinitionToClone.m_properties != null)
|
|
||||||
{
|
|
||||||
this.m_properties = new Dictionary<String, String>(inputDefinitionToClone.m_properties);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IList<String> Aliases
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_aliases == null)
|
|
||||||
{
|
|
||||||
m_aliases = new List<String>();
|
|
||||||
}
|
|
||||||
return m_aliases;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String Name
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String Label
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String DefaultValue
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public Boolean Required
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(Name = "Type")]
|
|
||||||
public String InputType
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String HelpMarkDown
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisibleRule should specify the condition at which this input is to be shown/displayed
|
|
||||||
// Typical format is "NAME OF THE DEPENDENT INPUT = VALUE TOBE BOUND"
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public string VisibleRule
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public string GroupName
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public TaskInputValidation Validation
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<String, String> Options
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_options == null)
|
|
||||||
{
|
|
||||||
m_options = new Dictionary<String, String>();
|
|
||||||
}
|
|
||||||
return m_options;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<String, String> Properties
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_properties == null)
|
|
||||||
{
|
|
||||||
m_properties = new Dictionary<String, String>();
|
|
||||||
}
|
|
||||||
return m_properties;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual TaskInputDefinitionBase Clone(
|
|
||||||
ISecuredObject securedObject)
|
|
||||||
{
|
|
||||||
return new TaskInputDefinitionBase(this, securedObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return this.Name.GetHashCode() ^ this.DefaultValue.GetHashCode() ^ this.Label.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
var taskInput2 = obj as TaskInputDefinitionBase;
|
|
||||||
if (taskInput2 == null
|
|
||||||
|| !string.Equals(InputType, taskInput2.InputType, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| !string.Equals(Label, taskInput2.Label, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| !string.Equals(Name, taskInput2.Name, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| !string.Equals(GroupName, taskInput2.GroupName, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| !string.Equals(DefaultValue, taskInput2.DefaultValue, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| !string.Equals(HelpMarkDown, taskInput2.HelpMarkDown, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| !string.Equals(VisibleRule, taskInput2.VisibleRule, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| !this.Required.Equals(taskInput2.Required))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!AreListsEqual(Aliases, taskInput2.Aliases)
|
|
||||||
|| !AreDictionariesEqual(Properties, taskInput2.Properties)
|
|
||||||
|| !AreDictionariesEqual(Options, taskInput2.Options))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((Validation != null && taskInput2.Validation == null)
|
|
||||||
|| (Validation == null && taskInput2.Validation != null)
|
|
||||||
|| ((Validation != null && taskInput2.Validation != null)
|
|
||||||
&& !Validation.Equals(taskInput2.Validation)))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool AreDictionariesEqual(Dictionary<String, String> input1, Dictionary<String, String> input2)
|
|
||||||
{
|
|
||||||
if (input1 == null && input2 == null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((input1 == null && input2 != null)
|
|
||||||
|| (input1 != null && input2 == null)
|
|
||||||
|| (input1.Count != input2.Count))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var key in input1.Keys)
|
|
||||||
{
|
|
||||||
if (!(input2.ContainsKey(key) && String.Equals(input1[key], input2[key], StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Boolean AreListsEqual(IList<String> list1, IList<String> list2)
|
|
||||||
{
|
|
||||||
if (list1.Count != list2.Count)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Int32 i = 0; i < list1.Count; i++)
|
|
||||||
{
|
|
||||||
if (!String.Equals(list1[i], list2[i], StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(Name = "Aliases", EmitDefaultValue = false)]
|
|
||||||
private List<String> m_aliases;
|
|
||||||
|
|
||||||
[DataMember(Name = "Options", EmitDefaultValue = false)]
|
|
||||||
private Dictionary<String, String> m_options;
|
|
||||||
|
|
||||||
[DataMember(Name = "Properties", EmitDefaultValue = false)]
|
|
||||||
private Dictionary<String, String> m_properties;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System.Runtime.Serialization;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Common.Contracts
|
|
||||||
{
|
|
||||||
public static class TaskInputType
|
|
||||||
{
|
|
||||||
public const String String = "string";
|
|
||||||
public const String Repository = "repository";
|
|
||||||
public const String Boolean = "boolean";
|
|
||||||
public const String KeyValue = "keyvalue";
|
|
||||||
public const String FilePath = "filepath";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Common.Contracts
|
|
||||||
{
|
|
||||||
[DataContract]
|
|
||||||
public class TaskInputValidation : BaseSecuredObject
|
|
||||||
{
|
|
||||||
public TaskInputValidation()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private TaskInputValidation(TaskInputValidation toClone, ISecuredObject securedObject)
|
|
||||||
: base(securedObject)
|
|
||||||
{
|
|
||||||
if (toClone != null)
|
|
||||||
{
|
|
||||||
this.Expression = toClone.Expression;
|
|
||||||
this.Message = toClone.Message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Conditional expression
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String Expression
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Message explaining how user can correct if validation fails
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String Message
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return Expression.GetHashCode() ^ Message.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public TaskInputValidation Clone()
|
|
||||||
{
|
|
||||||
return this.Clone(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TaskInputValidation Clone(ISecuredObject securedObject)
|
|
||||||
{
|
|
||||||
return new TaskInputValidation(this, securedObject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Common.Contracts
|
|
||||||
{
|
|
||||||
[DataContract]
|
|
||||||
public class TaskSourceDefinitionBase : BaseSecuredObject
|
|
||||||
{
|
|
||||||
public TaskSourceDefinitionBase()
|
|
||||||
{
|
|
||||||
AuthKey = String.Empty;
|
|
||||||
Endpoint = String.Empty;
|
|
||||||
Selector = String.Empty;
|
|
||||||
Target = String.Empty;
|
|
||||||
KeySelector = String.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected TaskSourceDefinitionBase(TaskSourceDefinitionBase inputDefinitionToClone)
|
|
||||||
: this(inputDefinitionToClone, null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected TaskSourceDefinitionBase(TaskSourceDefinitionBase inputDefinitionToClone, ISecuredObject securedObject)
|
|
||||||
: base(securedObject)
|
|
||||||
{
|
|
||||||
this.Endpoint = inputDefinitionToClone.Endpoint;
|
|
||||||
this.Target = inputDefinitionToClone.Target;
|
|
||||||
this.AuthKey = inputDefinitionToClone.AuthKey;
|
|
||||||
this.Selector = inputDefinitionToClone.Selector;
|
|
||||||
this.KeySelector = inputDefinitionToClone.KeySelector;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual TaskSourceDefinitionBase Clone(ISecuredObject securedObject)
|
|
||||||
{
|
|
||||||
return new TaskSourceDefinitionBase(this, securedObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String Endpoint
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String Target
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String AuthKey
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String Selector
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public String KeySelector
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
internal sealed class AndNode : FunctionNode
|
|
||||||
{
|
|
||||||
protected sealed override Boolean TraceFullyRealized => false;
|
|
||||||
|
|
||||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
|
||||||
{
|
|
||||||
foreach (ExpressionNode parameter in Parameters)
|
|
||||||
{
|
|
||||||
if (!parameter.EvaluateBoolean(context))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
internal sealed class CoalesceNode : FunctionNode
|
|
||||||
{
|
|
||||||
protected sealed override Boolean TraceFullyRealized => false;
|
|
||||||
|
|
||||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
|
||||||
{
|
|
||||||
EvaluationResult result = null;
|
|
||||||
foreach (ExpressionNode parameter in Parameters)
|
|
||||||
{
|
|
||||||
result = parameter.Evaluate(context);
|
|
||||||
if (result.Kind == ValueKind.Null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.Kind == ValueKind.String && String.IsNullOrEmpty(result.Value as String))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result?.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions.CollectionAccessors
|
|
||||||
{
|
|
||||||
internal sealed class JArrayAccessor : IReadOnlyArray
|
|
||||||
{
|
|
||||||
public JArrayAccessor(JArray jarray)
|
|
||||||
{
|
|
||||||
m_jarray = jarray;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Int32 Count => m_jarray.Count;
|
|
||||||
|
|
||||||
public Object this[Int32 index] => m_jarray[index];
|
|
||||||
|
|
||||||
public IEnumerator<Object> GetEnumerator()
|
|
||||||
{
|
|
||||||
return m_jarray.GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return m_jarray.GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly JArray m_jarray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions.CollectionAccessors
|
|
||||||
{
|
|
||||||
internal sealed class JObjectAccessor : IReadOnlyObject
|
|
||||||
{
|
|
||||||
public JObjectAccessor(JObject jobject)
|
|
||||||
{
|
|
||||||
m_jobject = jobject;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Int32 Count => m_jobject.Count;
|
|
||||||
|
|
||||||
public IEnumerable<String> Keys => (m_jobject as IDictionary<String, JToken>).Keys;
|
|
||||||
|
|
||||||
// This uses Select. Calling .Values directly throws an exception.
|
|
||||||
public IEnumerable<Object> Values => (m_jobject as IDictionary<String, JToken>).Select(x => x.Value);
|
|
||||||
|
|
||||||
public Object this[String key] => m_jobject[key];
|
|
||||||
|
|
||||||
public Boolean ContainsKey(String key)
|
|
||||||
{
|
|
||||||
return (m_jobject as IDictionary<String, JToken>).ContainsKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerator<KeyValuePair<String, Object>> GetEnumerator()
|
|
||||||
{
|
|
||||||
return (m_jobject as IDictionary<String, JToken>).Select(x => new KeyValuePair<String, Object>(x.Key, x.Value)).GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return (m_jobject as IDictionary<String, JToken>).Select(x => new KeyValuePair<String, Object>(x.Key, x.Value)).GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean TryGetValue(
|
|
||||||
String key,
|
|
||||||
out Object value)
|
|
||||||
{
|
|
||||||
if ((m_jobject as IDictionary<String, JToken>).TryGetValue(key, out JToken val))
|
|
||||||
{
|
|
||||||
value = val;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly JObject m_jobject;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using Newtonsoft.Json.Serialization;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions.CollectionAccessors
|
|
||||||
{
|
|
||||||
internal sealed class JsonDictionaryContractAccessor : IReadOnlyObject
|
|
||||||
{
|
|
||||||
public JsonDictionaryContractAccessor(
|
|
||||||
JsonDictionaryContract contract,
|
|
||||||
Object obj)
|
|
||||||
{
|
|
||||||
m_contract = contract;
|
|
||||||
m_obj = obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Int32 Count
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var genericMethod = s_getCountTemplate.Value.MakeGenericMethod(m_contract.DictionaryValueType);
|
|
||||||
return (Int32)genericMethod.Invoke(null, new[] { m_obj });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<String> Keys
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var genericMethod = s_getKeysTemplate.Value.MakeGenericMethod(m_contract.DictionaryValueType);
|
|
||||||
return genericMethod.Invoke(null, new[] { m_obj }) as IEnumerable<String>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<Object> Values => Keys.Select(x => this[x]);
|
|
||||||
|
|
||||||
public Object this[String key]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (TryGetValue(key, out Object value))
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new KeyNotFoundException(ExpressionResources.KeyNotFound(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean ContainsKey(String key)
|
|
||||||
{
|
|
||||||
return TryGetValue(key, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerator<KeyValuePair<String, Object>> GetEnumerator()
|
|
||||||
{
|
|
||||||
return Keys.Select(x => new KeyValuePair<String, Object>(x, this[x])).GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return Keys.Select(x => new KeyValuePair<String, Object>(x, this[x])).GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean TryGetValue(
|
|
||||||
String key,
|
|
||||||
out Object value)
|
|
||||||
{
|
|
||||||
var genericMethod = s_tryGetValueTemplate.Value.MakeGenericMethod(m_contract.DictionaryValueType);
|
|
||||||
var tuple = genericMethod.Invoke(null, new[] { m_obj, key }) as Tuple<Boolean, Object>;
|
|
||||||
value = tuple.Item2;
|
|
||||||
return tuple.Item1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Int32 GetCount<TValue>(IDictionary<String, TValue> dictionary)
|
|
||||||
{
|
|
||||||
return dictionary.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<String> GetKeys<TValue>(IDictionary<String, TValue> dictionary)
|
|
||||||
{
|
|
||||||
return dictionary.Keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Tuple<Boolean, Object> TryGetValue<TValue>(
|
|
||||||
IDictionary<String, TValue> dictionary,
|
|
||||||
String key)
|
|
||||||
{
|
|
||||||
if (dictionary.TryGetValue(key, out TValue value))
|
|
||||||
{
|
|
||||||
return new Tuple<Boolean, Object>(true, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Tuple<Boolean, Object>(false, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Lazy<MethodInfo> s_getCountTemplate = new Lazy<MethodInfo>(() => typeof(JsonDictionaryContractAccessor).GetTypeInfo().GetMethod(nameof(GetCount), BindingFlags.NonPublic | BindingFlags.Static));
|
|
||||||
private static Lazy<MethodInfo> s_getKeysTemplate = new Lazy<MethodInfo>(() => typeof(JsonDictionaryContractAccessor).GetTypeInfo().GetMethod(nameof(GetKeys), BindingFlags.NonPublic | BindingFlags.Static));
|
|
||||||
private static Lazy<MethodInfo> s_tryGetValueTemplate = new Lazy<MethodInfo>(() => typeof(JsonDictionaryContractAccessor).GetTypeInfo().GetMethod(nameof(TryGetValue), BindingFlags.NonPublic | BindingFlags.Static));
|
|
||||||
private readonly JsonDictionaryContract m_contract;
|
|
||||||
private readonly Object m_obj;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Newtonsoft.Json.Serialization;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions.CollectionAccessors
|
|
||||||
{
|
|
||||||
internal sealed class JsonObjectContractAccessor : IReadOnlyObject
|
|
||||||
{
|
|
||||||
public JsonObjectContractAccessor(
|
|
||||||
JsonObjectContract contract,
|
|
||||||
Object obj)
|
|
||||||
{
|
|
||||||
m_contract = contract;
|
|
||||||
m_obj = obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Int32 Count => GetProperties().Count();
|
|
||||||
|
|
||||||
public IEnumerable<String> Keys => GetProperties().Select(x => x.PropertyName);
|
|
||||||
|
|
||||||
public IEnumerable<Object> Values => GetProperties().Select(x => x.ValueProvider.GetValue(m_obj));
|
|
||||||
|
|
||||||
public Object this[String key]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (TryGetValue(key, out Object value))
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new KeyNotFoundException(ExpressionResources.KeyNotFound(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean ContainsKey(String key)
|
|
||||||
{
|
|
||||||
return TryGetProperty(key, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerator<KeyValuePair<String, Object>> GetEnumerator()
|
|
||||||
{
|
|
||||||
return Keys.Select(x => new KeyValuePair<String, Object>(x, this[x])).GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return Keys.Select(x => new KeyValuePair<String, Object>(x, this[x])).GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean TryGetValue(
|
|
||||||
String key,
|
|
||||||
out Object value)
|
|
||||||
{
|
|
||||||
if (TryGetProperty(key, out JsonProperty property))
|
|
||||||
{
|
|
||||||
value = property.ValueProvider.GetValue(m_obj);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<JsonProperty> GetProperties()
|
|
||||||
{
|
|
||||||
return m_contract.Properties.Where(x => !x.Ignored);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Boolean TryGetProperty(
|
|
||||||
String key,
|
|
||||||
out JsonProperty property)
|
|
||||||
{
|
|
||||||
property = m_contract.Properties.GetClosestMatchProperty(key);
|
|
||||||
if (property != null && !property.Ignored)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
property = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly JsonObjectContract m_contract;
|
|
||||||
private readonly Object m_obj;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions.CollectionAccessors
|
|
||||||
{
|
|
||||||
internal sealed class ListOfObjectAccessor : IReadOnlyArray
|
|
||||||
{
|
|
||||||
public ListOfObjectAccessor(IList<Object> list)
|
|
||||||
{
|
|
||||||
m_list = list;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Int32 Count => m_list.Count;
|
|
||||||
|
|
||||||
public Object this[Int32 index] => m_list[index];
|
|
||||||
|
|
||||||
public IEnumerator<Object> GetEnumerator()
|
|
||||||
{
|
|
||||||
return m_list.GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return m_list.GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly IList<Object> m_list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions.CollectionAccessors
|
|
||||||
{
|
|
||||||
internal sealed class ReadOnlyDictionaryOfStringObjectAccessor : IReadOnlyObject
|
|
||||||
{
|
|
||||||
public ReadOnlyDictionaryOfStringObjectAccessor(IReadOnlyDictionary<String, Object> dictionary)
|
|
||||||
{
|
|
||||||
m_dictionary = dictionary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Int32 Count => m_dictionary.Count;
|
|
||||||
|
|
||||||
public IEnumerable<String> Keys => m_dictionary.Keys;
|
|
||||||
|
|
||||||
public IEnumerable<Object> Values => m_dictionary.Values;
|
|
||||||
|
|
||||||
public Object this[String key] => m_dictionary[key];
|
|
||||||
|
|
||||||
public Boolean ContainsKey(String key) => m_dictionary.ContainsKey(key);
|
|
||||||
|
|
||||||
public IEnumerator<KeyValuePair<String, Object>> GetEnumerator() => m_dictionary.GetEnumerator();
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator() => m_dictionary.GetEnumerator();
|
|
||||||
|
|
||||||
public Boolean TryGetValue(
|
|
||||||
String key,
|
|
||||||
out Object value)
|
|
||||||
{
|
|
||||||
return m_dictionary.TryGetValue(key, out value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly IReadOnlyDictionary<String, Object> m_dictionary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions.CollectionAccessors
|
|
||||||
{
|
|
||||||
internal sealed class ReadOnlyDictionaryOfStringStringAccessor : IReadOnlyObject
|
|
||||||
{
|
|
||||||
public ReadOnlyDictionaryOfStringStringAccessor(IReadOnlyDictionary<String, String> dictionary)
|
|
||||||
{
|
|
||||||
m_dictionary = dictionary;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Int32 Count => m_dictionary.Count;
|
|
||||||
|
|
||||||
public IEnumerable<String> Keys => m_dictionary.Keys;
|
|
||||||
|
|
||||||
public IEnumerable<Object> Values => m_dictionary.Values.OfType<Object>();
|
|
||||||
|
|
||||||
public Object this[String key] => m_dictionary[key];
|
|
||||||
|
|
||||||
public Boolean ContainsKey(String key)
|
|
||||||
{
|
|
||||||
return m_dictionary.ContainsKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerator<KeyValuePair<String, Object>> GetEnumerator()
|
|
||||||
{
|
|
||||||
return m_dictionary.Select(x => new KeyValuePair<String, Object>(x.Key, x.Value)).GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return m_dictionary.Select(x => new KeyValuePair<String, Object>(x.Key, x.Value)).GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean TryGetValue(
|
|
||||||
String key,
|
|
||||||
out Object value)
|
|
||||||
{
|
|
||||||
if (m_dictionary.TryGetValue(key, out String val))
|
|
||||||
{
|
|
||||||
value = val;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly IReadOnlyDictionary<String, String> m_dictionary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions.CollectionAccessors
|
|
||||||
{
|
|
||||||
internal sealed class ReadOnlyListOfObjectAccessor : IReadOnlyArray
|
|
||||||
{
|
|
||||||
public ReadOnlyListOfObjectAccessor(IReadOnlyList<Object> list)
|
|
||||||
{
|
|
||||||
m_list = list;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Int32 Count => m_list.Count;
|
|
||||||
|
|
||||||
public Object this[Int32 index] => m_list[index];
|
|
||||||
|
|
||||||
public IEnumerator<Object> GetEnumerator() => m_list.GetEnumerator();
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator() => m_list.GetEnumerator();
|
|
||||||
|
|
||||||
private readonly IReadOnlyList<Object> m_list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public abstract class ContainerNode : ExpressionNode
|
|
||||||
{
|
|
||||||
public IReadOnlyList<ExpressionNode> Parameters => m_parameters.AsReadOnly();
|
|
||||||
|
|
||||||
public void AddParameter(ExpressionNode node)
|
|
||||||
{
|
|
||||||
m_parameters.Add(node);
|
|
||||||
node.Container = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ReplaceParameter(Int32 index, ExpressionNode node)
|
|
||||||
{
|
|
||||||
m_parameters[index] = node;
|
|
||||||
node.Container = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IEnumerable<T> GetParameters<T>()
|
|
||||||
{
|
|
||||||
List<T> matched = new List<T>();
|
|
||||||
Queue<IExpressionNode> parameters = new Queue<IExpressionNode>(this.Parameters);
|
|
||||||
|
|
||||||
while (parameters.Count > 0)
|
|
||||||
{
|
|
||||||
var parameter = parameters.Dequeue();
|
|
||||||
if (typeof(T).GetTypeInfo().IsAssignableFrom(parameter.GetType().GetTypeInfo()))
|
|
||||||
{
|
|
||||||
matched.Add((T)parameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var childParameter in parameter.GetParameters<T>())
|
|
||||||
{
|
|
||||||
parameters.Enqueue(childParameter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matched;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly List<ExpressionNode> m_parameters = new List<ExpressionNode>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
internal sealed class ContainsNode : FunctionNode
|
|
||||||
{
|
|
||||||
protected sealed override Boolean TraceFullyRealized => false;
|
|
||||||
|
|
||||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
|
||||||
{
|
|
||||||
String left = Parameters[0].EvaluateString(context) as String ?? String.Empty;
|
|
||||||
String right = Parameters[1].EvaluateString(context) as String ?? String.Empty;
|
|
||||||
return left.IndexOf(right, StringComparison.OrdinalIgnoreCase) >= 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
internal sealed class ContainsValueNode : FunctionNode
|
|
||||||
{
|
|
||||||
protected sealed override Boolean TraceFullyRealized => false;
|
|
||||||
|
|
||||||
protected sealed override object EvaluateCore(EvaluationContext context)
|
|
||||||
{
|
|
||||||
EvaluationResult left = Parameters[0].Evaluate(context);
|
|
||||||
|
|
||||||
if (left.TryGetCollectionInterface(out Object collection))
|
|
||||||
{
|
|
||||||
EvaluationResult right = Parameters[1].Evaluate(context);
|
|
||||||
|
|
||||||
if (collection is IReadOnlyArray array)
|
|
||||||
{
|
|
||||||
foreach (var item in array)
|
|
||||||
{
|
|
||||||
var itemResult = EvaluationResult.CreateIntermediateResult(context, item, out _);
|
|
||||||
|
|
||||||
if (right.Equals(context, itemResult))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (collection is IReadOnlyObject obj)
|
|
||||||
{
|
|
||||||
foreach (var value in obj.Values)
|
|
||||||
{
|
|
||||||
var valueResult = EvaluationResult.CreateIntermediateResult(context, value, out _);
|
|
||||||
|
|
||||||
if (right.Equals(context, valueResult))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public struct ConversionResult
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Result object after the conversion
|
|
||||||
/// </summary>
|
|
||||||
public Object Result;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Memory overhead for the result object
|
|
||||||
/// </summary>
|
|
||||||
public ResultMemory ResultMemory;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
internal sealed class EndsWithNode : FunctionNode
|
|
||||||
{
|
|
||||||
protected sealed override Boolean TraceFullyRealized => false;
|
|
||||||
|
|
||||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
|
||||||
{
|
|
||||||
String left = Parameters[0].EvaluateString(context) ?? String.Empty;
|
|
||||||
String right = Parameters[1].EvaluateString(context) ?? String.Empty;
|
|
||||||
return left.EndsWith(right, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
internal sealed class EqualNode : FunctionNode
|
|
||||||
{
|
|
||||||
protected sealed override Boolean TraceFullyRealized => false;
|
|
||||||
|
|
||||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
|
||||||
{
|
|
||||||
return Parameters[0].Evaluate(context).Equals(context, Parameters[1].Evaluate(context));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using GitHub.DistributedTask.Logging;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public sealed class EvaluationContext
|
|
||||||
{
|
|
||||||
internal EvaluationContext(
|
|
||||||
ITraceWriter trace,
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
Object state,
|
|
||||||
EvaluationOptions options,
|
|
||||||
ExpressionNode node)
|
|
||||||
{
|
|
||||||
ArgumentUtility.CheckForNull(trace, nameof(trace));
|
|
||||||
ArgumentUtility.CheckForNull(secretMasker, nameof(secretMasker));
|
|
||||||
Trace = trace;
|
|
||||||
SecretMasker = secretMasker;
|
|
||||||
State = state;
|
|
||||||
|
|
||||||
// Copy the options
|
|
||||||
options = new EvaluationOptions(copy: options);
|
|
||||||
if (options.MaxMemory == 0)
|
|
||||||
{
|
|
||||||
// Set a reasonable default max memory
|
|
||||||
options.MaxMemory = 1048576; // 1 mb
|
|
||||||
}
|
|
||||||
Options = options;
|
|
||||||
Memory = new EvaluationMemory(options.MaxMemory, node);
|
|
||||||
|
|
||||||
m_traceResults = new Dictionary<ExpressionNode, String>();
|
|
||||||
m_traceMemory = new MemoryCounter(null, options.MaxMemory);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ITraceWriter Trace { get; }
|
|
||||||
|
|
||||||
public ISecretMasker SecretMasker { get; }
|
|
||||||
|
|
||||||
public Object State { get; }
|
|
||||||
|
|
||||||
internal EvaluationMemory Memory { get; }
|
|
||||||
|
|
||||||
internal EvaluationOptions Options { get; }
|
|
||||||
|
|
||||||
internal void SetTraceResult(
|
|
||||||
ExpressionNode node,
|
|
||||||
EvaluationResult result)
|
|
||||||
{
|
|
||||||
// Remove if previously added. This typically should not happen. This could happen
|
|
||||||
// due to a badly authored function. So we'll handle it and track memory correctly.
|
|
||||||
if (m_traceResults.TryGetValue(node, out String oldValue))
|
|
||||||
{
|
|
||||||
m_traceMemory.Remove(oldValue);
|
|
||||||
m_traceResults.Remove(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check max memory
|
|
||||||
String value = ExpressionUtil.FormatValue(SecretMasker, result);
|
|
||||||
if (m_traceMemory.TryAdd(value))
|
|
||||||
{
|
|
||||||
// Store the result
|
|
||||||
m_traceResults[node] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal Boolean TryGetTraceResult(ExpressionNode node, out String value)
|
|
||||||
{
|
|
||||||
return m_traceResults.TryGetValue(node, out value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Dictionary<ExpressionNode, String> m_traceResults = new Dictionary<ExpressionNode, String>();
|
|
||||||
private readonly MemoryCounter m_traceMemory;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// This is an internal class only.
|
|
||||||
///
|
|
||||||
/// This class is used to track current memory consumption
|
|
||||||
/// across the entire expression evaluation.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class EvaluationMemory
|
|
||||||
{
|
|
||||||
internal EvaluationMemory(
|
|
||||||
Int32 maxBytes,
|
|
||||||
ExpressionNode node)
|
|
||||||
{
|
|
||||||
m_maxAmount = maxBytes;
|
|
||||||
m_node = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void AddAmount(
|
|
||||||
Int32 depth,
|
|
||||||
Int32 bytes,
|
|
||||||
Boolean trimDepth = false)
|
|
||||||
{
|
|
||||||
// Trim deeper depths
|
|
||||||
if (trimDepth)
|
|
||||||
{
|
|
||||||
while (m_maxActiveDepth > depth)
|
|
||||||
{
|
|
||||||
var amount = m_depths[m_maxActiveDepth];
|
|
||||||
|
|
||||||
if (amount > 0)
|
|
||||||
{
|
|
||||||
// Sanity check
|
|
||||||
if (amount > m_totalAmount)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Bytes to subtract exceeds total bytes");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtract from the total
|
|
||||||
checked
|
|
||||||
{
|
|
||||||
m_totalAmount -= amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset the amount
|
|
||||||
m_depths[m_maxActiveDepth] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_maxActiveDepth--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grow the depths
|
|
||||||
if (depth > m_maxActiveDepth)
|
|
||||||
{
|
|
||||||
// Grow the list
|
|
||||||
while (m_depths.Count <= depth)
|
|
||||||
{
|
|
||||||
m_depths.Add(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust the max active depth
|
|
||||||
m_maxActiveDepth = depth;
|
|
||||||
}
|
|
||||||
|
|
||||||
checked
|
|
||||||
{
|
|
||||||
// Add to the depth
|
|
||||||
m_depths[depth] += bytes;
|
|
||||||
|
|
||||||
// Add to the total
|
|
||||||
m_totalAmount += bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check max
|
|
||||||
if (m_totalAmount > m_maxAmount)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(ExpressionResources.ExceededAllowedMemory(m_node?.ConvertToExpression()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Int32 CalculateBytes(Object obj)
|
|
||||||
{
|
|
||||||
if (obj is String str)
|
|
||||||
{
|
|
||||||
// This measurement doesn't have to be perfect
|
|
||||||
// https://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/
|
|
||||||
|
|
||||||
checked
|
|
||||||
{
|
|
||||||
return c_stringBaseOverhead + ((str?.Length ?? 0) * sizeof(Char));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return c_minObjectSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const Int32 c_minObjectSize = 24;
|
|
||||||
private const Int32 c_stringBaseOverhead = 26;
|
|
||||||
private readonly List<Int32> m_depths = new List<Int32>();
|
|
||||||
private readonly Int32 m_maxAmount;
|
|
||||||
private readonly ExpressionNode m_node;
|
|
||||||
private Int32 m_maxActiveDepth = -1;
|
|
||||||
private Int32 m_totalAmount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public sealed class EvaluationOptions
|
|
||||||
{
|
|
||||||
public EvaluationOptions()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public EvaluationOptions(EvaluationOptions copy)
|
|
||||||
{
|
|
||||||
if (copy != null)
|
|
||||||
{
|
|
||||||
Converters = copy.Converters;
|
|
||||||
MaxMemory = copy.MaxMemory;
|
|
||||||
TimeZone = copy.TimeZone;
|
|
||||||
UseCollectionInterfaces = copy.UseCollectionInterfaces;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converters allow types to be coerced into data that is friendly
|
|
||||||
/// for expression functions to operate on it.
|
|
||||||
///
|
|
||||||
/// As each node in the expression tree is evaluated, converters are applied.
|
|
||||||
/// When a node's result matches a converter type, the result is intercepted
|
|
||||||
/// by the converter, and converter result is used instead.
|
|
||||||
/// </summary>
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public IDictionary<Type, Converter<Object, ConversionResult>> Converters { get; set; }
|
|
||||||
|
|
||||||
public Int32 MaxMemory { get; set; }
|
|
||||||
|
|
||||||
public TimeZoneInfo TimeZone { get; set; }
|
|
||||||
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public Boolean UseCollectionInterfaces { get; set; } // Feature flag for now behavior
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,828 +0,0 @@
|
|||||||
using GitHub.DistributedTask.Expressions.CollectionAccessors;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using Newtonsoft.Json.Serialization;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public sealed class EvaluationResult
|
|
||||||
{
|
|
||||||
internal EvaluationResult(
|
|
||||||
EvaluationContext context,
|
|
||||||
Int32 level,
|
|
||||||
Object val,
|
|
||||||
ValueKind kind,
|
|
||||||
Object raw)
|
|
||||||
: this(context, level, val, kind, raw, false)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
internal EvaluationResult(
|
|
||||||
EvaluationContext context,
|
|
||||||
Int32 level,
|
|
||||||
Object val,
|
|
||||||
ValueKind kind,
|
|
||||||
Object raw,
|
|
||||||
Boolean omitTracing)
|
|
||||||
{
|
|
||||||
m_level = level;
|
|
||||||
Value = val;
|
|
||||||
Kind = kind;
|
|
||||||
Raw = raw;
|
|
||||||
m_omitTracing = omitTracing;
|
|
||||||
|
|
||||||
if (!omitTracing)
|
|
||||||
{
|
|
||||||
TraceValue(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueKind Kind { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When a custom converter is applied to the node result, raw contains the original value
|
|
||||||
/// </summary>
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public Object Raw { get; }
|
|
||||||
|
|
||||||
public Object Value { get; }
|
|
||||||
|
|
||||||
public int CompareTo(
|
|
||||||
EvaluationContext context,
|
|
||||||
EvaluationResult right)
|
|
||||||
{
|
|
||||||
Object leftValue;
|
|
||||||
ValueKind leftKind;
|
|
||||||
switch (Kind)
|
|
||||||
{
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
case ValueKind.DateTime:
|
|
||||||
case ValueKind.Number:
|
|
||||||
case ValueKind.String:
|
|
||||||
case ValueKind.Version:
|
|
||||||
leftValue = Value;
|
|
||||||
leftKind = Kind;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
leftValue = ConvertToNumber(context); // Will throw or succeed
|
|
||||||
leftKind = ValueKind.Number;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (leftKind == ValueKind.Boolean)
|
|
||||||
{
|
|
||||||
Boolean b = right.ConvertToBoolean(context);
|
|
||||||
return ((Boolean)leftValue).CompareTo(b);
|
|
||||||
}
|
|
||||||
else if (leftKind == ValueKind.DateTime)
|
|
||||||
{
|
|
||||||
DateTimeOffset d = right.ConvertToDateTime(context);
|
|
||||||
return ((DateTimeOffset)leftValue).CompareTo(d);
|
|
||||||
}
|
|
||||||
else if (leftKind == ValueKind.Number)
|
|
||||||
{
|
|
||||||
Decimal d = right.ConvertToNumber(context);
|
|
||||||
return ((Decimal)leftValue).CompareTo(d);
|
|
||||||
}
|
|
||||||
else if (leftKind == ValueKind.String)
|
|
||||||
{
|
|
||||||
String s = right.ConvertToString(context);
|
|
||||||
return String.Compare(leftValue as String ?? String.Empty, s ?? String.Empty, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
else //if (leftKind == ValueKind.Version)
|
|
||||||
{
|
|
||||||
Version v = right.ConvertToVersion(context);
|
|
||||||
return (leftValue as Version).CompareTo(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean ConvertToBoolean(EvaluationContext context)
|
|
||||||
{
|
|
||||||
Boolean result;
|
|
||||||
switch (Kind)
|
|
||||||
{
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
return (Boolean)Value; // Not converted. Don't trace.
|
|
||||||
|
|
||||||
case ValueKind.Number:
|
|
||||||
result = (Decimal)Value != 0m; // 0 converts to false, otherwise true.
|
|
||||||
TraceValue(context, result, ValueKind.Boolean);
|
|
||||||
return result;
|
|
||||||
|
|
||||||
case ValueKind.String:
|
|
||||||
result = !String.IsNullOrEmpty(Value as String);
|
|
||||||
TraceValue(context, result, ValueKind.Boolean);
|
|
||||||
return result;
|
|
||||||
|
|
||||||
case ValueKind.Array:
|
|
||||||
case ValueKind.DateTime:
|
|
||||||
case ValueKind.Object:
|
|
||||||
case ValueKind.Version:
|
|
||||||
result = true;
|
|
||||||
TraceValue(context, result, ValueKind.Boolean);
|
|
||||||
return result;
|
|
||||||
|
|
||||||
case ValueKind.Null:
|
|
||||||
result = false;
|
|
||||||
TraceValue(context, result, ValueKind.Boolean);
|
|
||||||
return result;
|
|
||||||
|
|
||||||
default: // Should never reach here.
|
|
||||||
throw new NotSupportedException($"Unable to convert value to Boolean. Unexpected value kind '{Kind}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public DateTimeOffset ConvertToDateTime(EvaluationContext context)
|
|
||||||
{
|
|
||||||
DateTimeOffset result;
|
|
||||||
if (TryConvertToDateTime(context, out result))
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new TypeCastException(context?.SecretMasker, Value, fromKind: Kind, toKind: ValueKind.DateTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Object ConvertToNull(EvaluationContext context)
|
|
||||||
{
|
|
||||||
Object result;
|
|
||||||
if (TryConvertToNull(context, out result))
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new TypeCastException(context?.SecretMasker, Value, fromKind: Kind, toKind: ValueKind.Null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Decimal ConvertToNumber(EvaluationContext context)
|
|
||||||
{
|
|
||||||
Decimal result;
|
|
||||||
if (TryConvertToNumber(context, out result))
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new TypeCastException(context?.SecretMasker, Value, fromKind: Kind, toKind: ValueKind.Number);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String ConvertToString(EvaluationContext context)
|
|
||||||
{
|
|
||||||
String result;
|
|
||||||
if (TryConvertToString(context, out result))
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new TypeCastException(context?.SecretMasker, Value, fromKind: Kind, toKind: ValueKind.String);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Version ConvertToVersion(EvaluationContext context)
|
|
||||||
{
|
|
||||||
Version result;
|
|
||||||
if (TryConvertToVersion(context, out result))
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new TypeCastException(context?.SecretMasker, Value, fromKind: Kind, toKind: ValueKind.Version);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean Equals(
|
|
||||||
EvaluationContext context,
|
|
||||||
EvaluationResult right)
|
|
||||||
{
|
|
||||||
if (Kind == ValueKind.Boolean)
|
|
||||||
{
|
|
||||||
Boolean b = right.ConvertToBoolean(context);
|
|
||||||
return (Boolean)Value == b;
|
|
||||||
}
|
|
||||||
else if (Kind == ValueKind.DateTime)
|
|
||||||
{
|
|
||||||
DateTimeOffset d;
|
|
||||||
if (right.TryConvertToDateTime(context, out d))
|
|
||||||
{
|
|
||||||
return (DateTimeOffset)Value == d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (Kind == ValueKind.Number)
|
|
||||||
{
|
|
||||||
Decimal d;
|
|
||||||
if (right.TryConvertToNumber(context, out d))
|
|
||||||
{
|
|
||||||
return (Decimal)Value == d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (Kind == ValueKind.Version)
|
|
||||||
{
|
|
||||||
Version v;
|
|
||||||
if (right.TryConvertToVersion(context, out v))
|
|
||||||
{
|
|
||||||
return (Version)Value == v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (Kind == ValueKind.String)
|
|
||||||
{
|
|
||||||
String s;
|
|
||||||
if (right.TryConvertToString(context, out s))
|
|
||||||
{
|
|
||||||
return String.Equals(
|
|
||||||
Value as String ?? String.Empty,
|
|
||||||
s ?? String.Empty,
|
|
||||||
StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (Kind == ValueKind.Array || Kind == ValueKind.Object)
|
|
||||||
{
|
|
||||||
return Kind == right.Kind && Object.ReferenceEquals(Value, right.Value);
|
|
||||||
}
|
|
||||||
else if (Kind == ValueKind.Null)
|
|
||||||
{
|
|
||||||
Object n;
|
|
||||||
if (right.TryConvertToNull(context, out n))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean TryConvertToDateTime(
|
|
||||||
EvaluationContext context,
|
|
||||||
out DateTimeOffset result)
|
|
||||||
{
|
|
||||||
switch (Kind)
|
|
||||||
{
|
|
||||||
case ValueKind.DateTime:
|
|
||||||
result = (DateTimeOffset)Value; // Not converted. Don't trace again.
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case ValueKind.String:
|
|
||||||
if (TryParseDateTime(context?.Options, Value as String, out result))
|
|
||||||
{
|
|
||||||
TraceValue(context, result, ValueKind.DateTime);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceCoercionFailed(context, toKind: ValueKind.DateTime);
|
|
||||||
return false;
|
|
||||||
|
|
||||||
case ValueKind.Array:
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
case ValueKind.Null:
|
|
||||||
case ValueKind.Number:
|
|
||||||
case ValueKind.Object:
|
|
||||||
case ValueKind.Version:
|
|
||||||
result = default;
|
|
||||||
TraceCoercionFailed(context, toKind: ValueKind.DateTime);
|
|
||||||
return false;
|
|
||||||
|
|
||||||
default: // Should never reach here.
|
|
||||||
throw new NotSupportedException($"Unable to determine whether value can be converted to Number. Unexpected value kind '{Kind}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean TryConvertToNull(
|
|
||||||
EvaluationContext context,
|
|
||||||
out Object result)
|
|
||||||
{
|
|
||||||
switch (Kind)
|
|
||||||
{
|
|
||||||
case ValueKind.Null:
|
|
||||||
result = null; // Not converted. Don't trace again.
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case ValueKind.String:
|
|
||||||
if (String.IsNullOrEmpty(Value as String))
|
|
||||||
{
|
|
||||||
result = null;
|
|
||||||
TraceValue(context, result, ValueKind.Null);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = null;
|
|
||||||
TraceCoercionFailed(context, toKind: ValueKind.Null);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean TryConvertToNumber(
|
|
||||||
EvaluationContext context,
|
|
||||||
out Decimal result)
|
|
||||||
{
|
|
||||||
switch (Kind)
|
|
||||||
{
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
result = (Boolean)Value ? 1m : 0m;
|
|
||||||
TraceValue(context, result, ValueKind.Number);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case ValueKind.Number:
|
|
||||||
result = (Decimal)Value; // Not converted. Don't trace again.
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case ValueKind.String:
|
|
||||||
String s = Value as String ?? String.Empty;
|
|
||||||
if (String.IsNullOrEmpty(s))
|
|
||||||
{
|
|
||||||
result = 0m;
|
|
||||||
TraceValue(context, result, ValueKind.Number);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Decimal.TryParse(s, s_numberStyles, CultureInfo.InvariantCulture, out result))
|
|
||||||
{
|
|
||||||
TraceValue(context, result, ValueKind.Number);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceCoercionFailed(context, toKind: ValueKind.Number);
|
|
||||||
return false;
|
|
||||||
|
|
||||||
case ValueKind.Array:
|
|
||||||
case ValueKind.DateTime:
|
|
||||||
case ValueKind.Object:
|
|
||||||
case ValueKind.Version:
|
|
||||||
result = default(Decimal);
|
|
||||||
TraceCoercionFailed(context, toKind: ValueKind.Number);
|
|
||||||
return false;
|
|
||||||
|
|
||||||
case ValueKind.Null:
|
|
||||||
result = 0m;
|
|
||||||
TraceValue(context, result, ValueKind.Number);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default: // Should never reach here.
|
|
||||||
throw new NotSupportedException($"Unable to determine whether value can be converted to Number. Unexpected value kind '{Kind}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean TryConvertToString(
|
|
||||||
EvaluationContext context,
|
|
||||||
out String result)
|
|
||||||
{
|
|
||||||
switch (Kind)
|
|
||||||
{
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
result = String.Format(CultureInfo.InvariantCulture, "{0}", Value);
|
|
||||||
TraceValue(context, result, ValueKind.String);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case ValueKind.DateTime:
|
|
||||||
result = ((DateTimeOffset)Value).ToString(ExpressionConstants.DateTimeFormat, CultureInfo.InvariantCulture);
|
|
||||||
TraceValue(context, result, ValueKind.String);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case ValueKind.Number:
|
|
||||||
result = ((Decimal)Value).ToString(ExpressionConstants.NumberFormat, CultureInfo.InvariantCulture);
|
|
||||||
TraceValue(context, result, ValueKind.String);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case ValueKind.String:
|
|
||||||
result = Value as String; // Not converted. Don't trace.
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case ValueKind.Version:
|
|
||||||
result = (Value as Version).ToString();
|
|
||||||
TraceValue(context, result, ValueKind.String);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case ValueKind.Null:
|
|
||||||
result = String.Empty;
|
|
||||||
TraceValue(context, result, ValueKind.Null);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case ValueKind.Array:
|
|
||||||
case ValueKind.Object:
|
|
||||||
result = null;
|
|
||||||
TraceCoercionFailed(context, toKind: ValueKind.String);
|
|
||||||
return false;
|
|
||||||
|
|
||||||
default: // Should never reach here.
|
|
||||||
throw new NotSupportedException($"Unable to convert to String. Unexpected value kind '{Kind}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean TryConvertToVersion(
|
|
||||||
EvaluationContext context,
|
|
||||||
out Version result)
|
|
||||||
{
|
|
||||||
switch (Kind)
|
|
||||||
{
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
result = null;
|
|
||||||
TraceCoercionFailed(context, toKind: ValueKind.Version);
|
|
||||||
return false;
|
|
||||||
|
|
||||||
case ValueKind.Number:
|
|
||||||
if (Version.TryParse(ConvertToString(context), out result))
|
|
||||||
{
|
|
||||||
TraceValue(context, result, ValueKind.Version);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceCoercionFailed(context, toKind: ValueKind.Version);
|
|
||||||
return false;
|
|
||||||
|
|
||||||
case ValueKind.String:
|
|
||||||
String s = Value as String ?? String.Empty;
|
|
||||||
if (Version.TryParse(s, out result))
|
|
||||||
{
|
|
||||||
TraceValue(context, result, ValueKind.Version);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceCoercionFailed(context, toKind: ValueKind.Version);
|
|
||||||
return false;
|
|
||||||
|
|
||||||
case ValueKind.Version:
|
|
||||||
result = Value as Version; // Not converted. Don't trace again.
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case ValueKind.Array:
|
|
||||||
case ValueKind.DateTime:
|
|
||||||
case ValueKind.Object:
|
|
||||||
case ValueKind.Null:
|
|
||||||
result = null;
|
|
||||||
TraceCoercionFailed(context, toKind: ValueKind.Version);
|
|
||||||
return false;
|
|
||||||
|
|
||||||
default: // Should never reach here.
|
|
||||||
throw new NotSupportedException($"Unable to convert to Version. Unexpected value kind '{Kind}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public Boolean TryGetCollectionInterface(out Object collection)
|
|
||||||
{
|
|
||||||
if ((Kind == ValueKind.Object || Kind == ValueKind.Array))
|
|
||||||
{
|
|
||||||
var obj = Value;
|
|
||||||
if (obj is IReadOnlyObject)
|
|
||||||
{
|
|
||||||
collection = obj;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (obj is IDictionary<String, String> dictionary1)
|
|
||||||
{
|
|
||||||
collection = new ReadOnlyDictionaryOfStringStringAccessor(new ReadOnlyDictionary<String, String>(dictionary1));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (obj is IDictionary<String, Object> dictionary2)
|
|
||||||
{
|
|
||||||
collection = new ReadOnlyDictionaryOfStringObjectAccessor(new ReadOnlyDictionary<String, Object>(dictionary2));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (obj is IReadOnlyDictionary<String, String> dictionary3)
|
|
||||||
{
|
|
||||||
collection = new ReadOnlyDictionaryOfStringStringAccessor(dictionary3);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (obj is IReadOnlyDictionary<String, Object> dictionary4)
|
|
||||||
{
|
|
||||||
collection = new ReadOnlyDictionaryOfStringObjectAccessor(dictionary4);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (obj is JObject jobject)
|
|
||||||
{
|
|
||||||
collection = new JObjectAccessor(jobject);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (obj is IReadOnlyArray)
|
|
||||||
{
|
|
||||||
collection = obj;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (obj is IList<Object> list1)
|
|
||||||
{
|
|
||||||
collection = new ListOfObjectAccessor(list1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (obj is IReadOnlyList<Object> list2)
|
|
||||||
{
|
|
||||||
collection = new ReadOnlyListOfObjectAccessor(list2);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (obj is JArray jarray)
|
|
||||||
{
|
|
||||||
collection = new JArrayAccessor(jarray);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var contract = s_serializer.Value.ContractResolver.ResolveContract(obj.GetType());
|
|
||||||
if (contract is JsonObjectContract objectContract)
|
|
||||||
{
|
|
||||||
collection = new JsonObjectContractAccessor(objectContract, obj);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (contract is JsonDictionaryContract dictionaryContract && dictionaryContract.DictionaryKeyType == typeof(String))
|
|
||||||
{
|
|
||||||
collection = new JsonDictionaryContractAccessor(dictionaryContract, obj);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
collection = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Useful for working with values that are not the direct evaluation result of a parameter.
|
|
||||||
/// This allows ExpressionNode authors to leverage the coercion and comparision functions
|
|
||||||
/// for any values.
|
|
||||||
///
|
|
||||||
/// Also note, the value will be canonicalized (for example numeric types converted to decimal) and any
|
|
||||||
/// matching converters applied.
|
|
||||||
/// </summary>
|
|
||||||
public static EvaluationResult CreateIntermediateResult(
|
|
||||||
EvaluationContext context,
|
|
||||||
Object obj,
|
|
||||||
out ResultMemory conversionResultMemory)
|
|
||||||
{
|
|
||||||
var val = ExpressionUtil.ConvertToCanonicalValue(context?.Options, obj, out ValueKind kind, out Object raw, out conversionResultMemory);
|
|
||||||
return new EvaluationResult(context, 0, val, kind, raw, omitTracing: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TraceCoercionFailed(
|
|
||||||
EvaluationContext context,
|
|
||||||
ValueKind toKind)
|
|
||||||
{
|
|
||||||
if (!m_omitTracing)
|
|
||||||
{
|
|
||||||
TraceVerbose(context, String.Format(CultureInfo.InvariantCulture, "=> Unable to coerce {0} to {1}.", Kind, toKind));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TraceValue(EvaluationContext context)
|
|
||||||
{
|
|
||||||
if (!m_omitTracing)
|
|
||||||
{
|
|
||||||
TraceValue(context, Value, Kind);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TraceValue(
|
|
||||||
EvaluationContext context,
|
|
||||||
Object val,
|
|
||||||
ValueKind kind)
|
|
||||||
{
|
|
||||||
if (!m_omitTracing)
|
|
||||||
{
|
|
||||||
TraceVerbose(context, String.Concat("=> ", ExpressionUtil.FormatValue(context?.SecretMasker, val, kind)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TraceVerbose(
|
|
||||||
EvaluationContext context,
|
|
||||||
String message)
|
|
||||||
{
|
|
||||||
if (!m_omitTracing)
|
|
||||||
{
|
|
||||||
context?.Trace.Verbose(String.Empty.PadLeft(m_level * 2, '.') + (message ?? String.Empty));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean TryParseDateTime(
|
|
||||||
EvaluationOptions options,
|
|
||||||
String s,
|
|
||||||
out DateTimeOffset result)
|
|
||||||
{
|
|
||||||
if (String.IsNullOrEmpty(s))
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
s = s.Trim();
|
|
||||||
var i = 0;
|
|
||||||
|
|
||||||
// Year, month, day, hour, min, sec
|
|
||||||
if (!ReadInt32(s, 4, 4, ref i, out Int32 year) ||
|
|
||||||
!ReadSeparator(s, ref i, new[] { '-', '/' }, out Char dateSeparator) ||
|
|
||||||
!ReadInt32(s, 1, 2, ref i, out Int32 month) ||
|
|
||||||
!ReadSeparator(s, ref i, dateSeparator) ||
|
|
||||||
!ReadInt32(s, 1, 2, ref i, out Int32 day) ||
|
|
||||||
!ReadSeparator(s, ref i, ' ', 'T') ||
|
|
||||||
!ReadInt32(s, 1, 2, ref i, out Int32 hour) ||
|
|
||||||
!ReadSeparator(s, ref i, ':') ||
|
|
||||||
!ReadInt32(s, 1, 2, ref i, out Int32 minute) ||
|
|
||||||
!ReadSeparator(s, ref i, ':') ||
|
|
||||||
!ReadInt32(s, 1, 2, ref i, out Int32 second))
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fraction of second
|
|
||||||
Int32 ticks;
|
|
||||||
if (ExpressionUtil.SafeCharAt(s, i) == '.')
|
|
||||||
{
|
|
||||||
i++;
|
|
||||||
if (!ReadDigits(s, 1, 7, ref i, out String digits))
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (digits.Length < 7)
|
|
||||||
{
|
|
||||||
digits = digits.PadRight(7, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
ticks = Int32.Parse(digits, NumberStyles.None, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ticks = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeSpan offset;
|
|
||||||
|
|
||||||
// End of string indicates local time zone
|
|
||||||
if (i >= s.Length)
|
|
||||||
{
|
|
||||||
// Determine the offset
|
|
||||||
var timeZone = options?.TimeZone ?? TimeZoneInfo.Local;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var dateTime = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified);
|
|
||||||
offset = timeZone.GetUtcOffset(dateTime);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Offset, then end of string
|
|
||||||
else if (!ReadOffset(s, ref i, out offset) ||
|
|
||||||
i < s.Length)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the DateTimeOffset
|
|
||||||
try
|
|
||||||
{
|
|
||||||
result = new DateTimeOffset(year, month, day, hour, minute, second, offset);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add fraction of second
|
|
||||||
if (ticks > 0)
|
|
||||||
{
|
|
||||||
result = result.AddTicks(ticks);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean ReadDigits(
|
|
||||||
String str,
|
|
||||||
Int32 minLength,
|
|
||||||
Int32 maxLength,
|
|
||||||
ref Int32 index,
|
|
||||||
out String result)
|
|
||||||
{
|
|
||||||
var startIndex = index;
|
|
||||||
while (Char.IsDigit(ExpressionUtil.SafeCharAt(str, index)))
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var length = index - startIndex;
|
|
||||||
if (length < minLength || length > maxLength)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = str.Substring(startIndex, length);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean ReadInt32(
|
|
||||||
String str,
|
|
||||||
Int32 minLength,
|
|
||||||
Int32 maxLength,
|
|
||||||
ref Int32 index,
|
|
||||||
out Int32 result)
|
|
||||||
{
|
|
||||||
if (!ReadDigits(str, minLength, maxLength, ref index, out String digits))
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = Int32.Parse(digits, NumberStyles.None, CultureInfo.InvariantCulture);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean ReadSeparator(
|
|
||||||
String str,
|
|
||||||
ref Int32 index,
|
|
||||||
params Char[] allowed)
|
|
||||||
{
|
|
||||||
return ReadSeparator(str, ref index, allowed, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean ReadSeparator(
|
|
||||||
String str,
|
|
||||||
ref Int32 index,
|
|
||||||
Char[] allowed,
|
|
||||||
out Char separator)
|
|
||||||
{
|
|
||||||
separator = ExpressionUtil.SafeCharAt(str, index++);
|
|
||||||
foreach (var a in allowed)
|
|
||||||
{
|
|
||||||
if (separator == a)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
separator = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean ReadOffset(
|
|
||||||
String str,
|
|
||||||
ref Int32 index,
|
|
||||||
out TimeSpan offset)
|
|
||||||
{
|
|
||||||
// Z indicates UTC
|
|
||||||
if (ExpressionUtil.SafeCharAt(str, index) == 'Z')
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
offset = TimeSpan.Zero;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Boolean subtract;
|
|
||||||
|
|
||||||
// Negative
|
|
||||||
if (ExpressionUtil.SafeCharAt(str, index) == '-')
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
subtract = true;
|
|
||||||
}
|
|
||||||
// Positive
|
|
||||||
else if (ExpressionUtil.SafeCharAt(str, index) == '+')
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
subtract = false;
|
|
||||||
}
|
|
||||||
// Invalid
|
|
||||||
else
|
|
||||||
{
|
|
||||||
offset = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hour and minute
|
|
||||||
if (!ReadInt32(str, 1, 2, ref index, out Int32 hour) ||
|
|
||||||
!ReadSeparator(str, ref index, ':') ||
|
|
||||||
!ReadInt32(str, 1, 2, ref index, out Int32 minute))
|
|
||||||
{
|
|
||||||
offset = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the offset
|
|
||||||
if (subtract)
|
|
||||||
{
|
|
||||||
offset = TimeSpan.Zero.Subtract(new TimeSpan(hour, minute, 0));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
offset = new TimeSpan(hour, minute, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly NumberStyles s_numberStyles =
|
|
||||||
NumberStyles.AllowDecimalPoint |
|
|
||||||
NumberStyles.AllowLeadingSign |
|
|
||||||
NumberStyles.AllowLeadingWhite |
|
|
||||||
NumberStyles.AllowThousands |
|
|
||||||
NumberStyles.AllowTrailingWhite;
|
|
||||||
private static readonly Lazy<JsonSerializer> s_serializer = new Lazy<JsonSerializer>(() => JsonUtility.CreateJsonSerializer());
|
|
||||||
private readonly Int32 m_level;
|
|
||||||
private readonly Boolean m_omitTracing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
using System;
|
|
||||||
using GitHub.DistributedTask.Logging;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
internal sealed class EvaluationTraceWriter : ITraceWriter
|
|
||||||
{
|
|
||||||
public EvaluationTraceWriter(ITraceWriter trace, ISecretMasker secretMasker)
|
|
||||||
{
|
|
||||||
ArgumentUtility.CheckForNull(secretMasker, nameof(secretMasker));
|
|
||||||
m_trace = trace;
|
|
||||||
m_secretMasker = secretMasker;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Info(String message)
|
|
||||||
{
|
|
||||||
if (m_trace != null)
|
|
||||||
{
|
|
||||||
message = m_secretMasker.MaskSecrets(message);
|
|
||||||
m_trace.Info(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Verbose(String message)
|
|
||||||
{
|
|
||||||
if (m_trace != null)
|
|
||||||
{
|
|
||||||
message = m_secretMasker.MaskSecrets(message);
|
|
||||||
m_trace.Verbose(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly ISecretMasker m_secretMasker;
|
|
||||||
private readonly ITraceWriter m_trace;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
internal static class ExpressionConstants
|
|
||||||
{
|
|
||||||
static ExpressionConstants()
|
|
||||||
{
|
|
||||||
AddFunction<AndNode>("and", 2, Int32.MaxValue);
|
|
||||||
AddFunction<CoalesceNode>("coalesce", 2, Int32.MaxValue);
|
|
||||||
AddFunction<ContainsNode>("contains", 2, 2);
|
|
||||||
AddFunction<ContainsValueNode>("containsValue", 2, 2);
|
|
||||||
AddFunction<EndsWithNode>("endsWith", 2, 2);
|
|
||||||
AddFunction<EqualNode>("eq", 2, 2);
|
|
||||||
AddFunction<FormatNode>("format", 1, Byte.MaxValue);
|
|
||||||
AddFunction<GreaterThanNode>("gt", 2, 2);
|
|
||||||
AddFunction<GreaterThanOrEqualNode>("ge", 2, 2);
|
|
||||||
AddFunction<LessThanNode>("lt", 2, 2);
|
|
||||||
AddFunction<JoinNode>("join", 2, 2);
|
|
||||||
AddFunction<LessThanOrEqualNode>("le", 2, 2);
|
|
||||||
AddFunction<InNode>("in", 2, Int32.MaxValue);
|
|
||||||
AddFunction<NotNode>("not", 1, 1);
|
|
||||||
AddFunction<NotEqualNode>("ne", 2, 2);
|
|
||||||
AddFunction<NotInNode>("notIn", 2, Int32.MaxValue);
|
|
||||||
AddFunction<OrNode>("or", 2, Int32.MaxValue);
|
|
||||||
AddFunction<StartsWithNode>("startsWith", 2, 2);
|
|
||||||
AddFunction<XorNode>("xor", 2, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddFunction<T>(String name, Int32 minParameters, Int32 maxParameters)
|
|
||||||
where T : FunctionNode, new()
|
|
||||||
{
|
|
||||||
WellKnownFunctions.Add(name, new FunctionInfo<T>(name, minParameters, maxParameters));
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static readonly String DateTimeFormat = @"yyyy\-MM\-dd\ HH\:mm\:sszzz";
|
|
||||||
internal static readonly Int32 MaxDepth = 50;
|
|
||||||
internal static readonly Int32 MaxLength = 21000; // Under 85,000 large object heap threshold, even if .NET switches to UTF-32
|
|
||||||
internal static readonly String NumberFormat = "0.#######";
|
|
||||||
internal static readonly Dictionary<String, IFunctionInfo> WellKnownFunctions = new Dictionary<String, IFunctionInfo>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
// Punctuation
|
|
||||||
internal const Char StartIndex = '[';
|
|
||||||
internal const Char StartParameter = '(';
|
|
||||||
internal const Char EndIndex = ']';
|
|
||||||
internal const Char EndParameter = ')';
|
|
||||||
internal const Char Separator = ',';
|
|
||||||
internal const Char Dereference = '.';
|
|
||||||
internal const Char Wildcard = '*';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using System;
|
|
||||||
using GitHub.DistributedTask.Logging;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
public class ExpressionException : Exception
|
|
||||||
{
|
|
||||||
internal ExpressionException(ISecretMasker secretMasker, String message)
|
|
||||||
{
|
|
||||||
if (secretMasker != null)
|
|
||||||
{
|
|
||||||
message = secretMasker.MaskSecrets(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override String Message => m_message;
|
|
||||||
|
|
||||||
private readonly String m_message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,494 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using GitHub.DistributedTask.Logging;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public abstract class ExpressionNode : IExpressionNode
|
|
||||||
{
|
|
||||||
internal ContainerNode Container { get; set; }
|
|
||||||
|
|
||||||
internal Int32 Level { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name is used for tracing. Normally the parser will set the name. However if a node
|
|
||||||
/// is added manually, then the name may not be set and will fallback to the type name.
|
|
||||||
/// </summary>
|
|
||||||
protected internal String Name
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return !String.IsNullOrEmpty(m_name) ? m_name : this.GetType().Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
m_name = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates whether the evalation result should be stored on the context and used
|
|
||||||
/// when the realized result is traced.
|
|
||||||
/// </summary>
|
|
||||||
protected abstract Boolean TraceFullyRealized { get; }
|
|
||||||
|
|
||||||
internal abstract String ConvertToExpression();
|
|
||||||
|
|
||||||
internal abstract String ConvertToRealizedExpression(EvaluationContext context);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluates the node
|
|
||||||
/// </summary>
|
|
||||||
protected virtual Object EvaluateCore(EvaluationContext context)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Method {nameof(EvaluateCore)} not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluates the node
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">The current expression context</param>
|
|
||||||
/// <param name="resultMemory">
|
|
||||||
/// Helps determine how much memory is being consumed across the evaluation of the expression.
|
|
||||||
/// </param>
|
|
||||||
protected virtual Object EvaluateCore(
|
|
||||||
EvaluationContext context,
|
|
||||||
out ResultMemory resultMemory)
|
|
||||||
{
|
|
||||||
resultMemory = null;
|
|
||||||
return EvaluateCore(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// INode entry point.
|
|
||||||
/// </summary>
|
|
||||||
public T Evaluate<T>(
|
|
||||||
ITraceWriter trace,
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
Object state,
|
|
||||||
EvaluationOptions options = null)
|
|
||||||
{
|
|
||||||
if (Container != null)
|
|
||||||
{
|
|
||||||
// Do not localize. This is an SDK consumer error.
|
|
||||||
throw new NotSupportedException($"Expected {nameof(IExpressionNode)}.{nameof(Evaluate)} to be called on root node only.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ISecretMasker originalSecretMasker = secretMasker;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
secretMasker = secretMasker?.Clone() ?? new SecretMasker();
|
|
||||||
trace = new EvaluationTraceWriter(trace, secretMasker);
|
|
||||||
var context = new EvaluationContext(trace, secretMasker, state, options, this);
|
|
||||||
trace.Info($"Evaluating: {ConvertToExpression()}");
|
|
||||||
|
|
||||||
// String
|
|
||||||
if (typeof(T).Equals(typeof(String)))
|
|
||||||
{
|
|
||||||
String stringResult = EvaluateString(context);
|
|
||||||
TraceTreeResult(context, stringResult, ValueKind.String);
|
|
||||||
return (T)(Object)stringResult;
|
|
||||||
}
|
|
||||||
// Boolean
|
|
||||||
else if (typeof(T).Equals(typeof(Boolean)))
|
|
||||||
{
|
|
||||||
Boolean booleanResult = EvaluateBoolean(context);
|
|
||||||
TraceTreeResult(context, booleanResult, ValueKind.Boolean);
|
|
||||||
return (T)(Object)booleanResult;
|
|
||||||
}
|
|
||||||
// Version
|
|
||||||
else if (typeof(T).Equals(typeof(Version)))
|
|
||||||
{
|
|
||||||
Version versionResult = EvaluateVersion(context);
|
|
||||||
TraceTreeResult(context, versionResult, ValueKind.Version);
|
|
||||||
return (T)(Object)versionResult;
|
|
||||||
}
|
|
||||||
// DateTime types
|
|
||||||
else if (typeof(T).Equals(typeof(DateTimeOffset)))
|
|
||||||
{
|
|
||||||
DateTimeOffset dateTimeResult = EvaluateDateTime(context);
|
|
||||||
TraceTreeResult(context, dateTimeResult, ValueKind.DateTime);
|
|
||||||
return (T)(Object)dateTimeResult;
|
|
||||||
}
|
|
||||||
else if (typeof(T).Equals(typeof(DateTime)))
|
|
||||||
{
|
|
||||||
DateTimeOffset dateTimeResult = EvaluateDateTime(context);
|
|
||||||
TraceTreeResult(context, dateTimeResult, ValueKind.DateTime);
|
|
||||||
return (T)(Object)dateTimeResult.UtcDateTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
TypeInfo typeInfo = typeof(T).GetTypeInfo();
|
|
||||||
if (typeInfo.IsPrimitive)
|
|
||||||
{
|
|
||||||
// Decimal
|
|
||||||
if (typeof(T).Equals(typeof(Decimal)))
|
|
||||||
{
|
|
||||||
Decimal decimalResult = EvaluateNumber(context);
|
|
||||||
TraceTreeResult(context, decimalResult, ValueKind.Number);
|
|
||||||
return (T)(Object)decimalResult;
|
|
||||||
}
|
|
||||||
// Other numeric types
|
|
||||||
else if (typeof(T).Equals(typeof(Byte)) ||
|
|
||||||
typeof(T).Equals(typeof(SByte)) ||
|
|
||||||
typeof(T).Equals(typeof(Int16)) ||
|
|
||||||
typeof(T).Equals(typeof(UInt16)) ||
|
|
||||||
typeof(T).Equals(typeof(Int32)) ||
|
|
||||||
typeof(T).Equals(typeof(UInt32)) ||
|
|
||||||
typeof(T).Equals(typeof(Int64)) ||
|
|
||||||
typeof(T).Equals(typeof(UInt64)) ||
|
|
||||||
typeof(T).Equals(typeof(Single)) ||
|
|
||||||
typeof(T).Equals(typeof(Double)))
|
|
||||||
{
|
|
||||||
Decimal decimalResult = EvaluateNumber(context);
|
|
||||||
trace.Verbose($"Converting expression result to type {typeof(T).Name}");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
T numericResult = (T)Convert.ChangeType(decimalResult, typeof(T));
|
|
||||||
|
|
||||||
// Note, the value is converted back to decimal before tracing, in order to leverage the same
|
|
||||||
// util-formatting method used in other places.
|
|
||||||
TraceTreeResult(context, Convert.ToDecimal((Object)numericResult), ValueKind.Number);
|
|
||||||
|
|
||||||
return numericResult;
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
context.Trace.Verbose($"Failed to convert the result number into the type {typeof(T).Name}. {exception.Message}");
|
|
||||||
throw new TypeCastException(
|
|
||||||
secretMasker,
|
|
||||||
value: decimalResult,
|
|
||||||
fromKind: ValueKind.Number,
|
|
||||||
toType: typeof(T),
|
|
||||||
error: exception.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic evaluate
|
|
||||||
EvaluationResult result = Evaluate(context);
|
|
||||||
TraceTreeResult(context, result.Value, result.Kind);
|
|
||||||
|
|
||||||
// JToken
|
|
||||||
if (typeof(T).Equals(typeof(JToken)))
|
|
||||||
{
|
|
||||||
if (result.Value is null)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
else if (result.Value is JToken)
|
|
||||||
{
|
|
||||||
return (T)result.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return (T)(Object)JToken.FromObject(result.Value, JsonUtility.CreateJsonSerializer());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Object or Array
|
|
||||||
else if (result.Kind == ValueKind.Object || result.Kind == ValueKind.Array)
|
|
||||||
{
|
|
||||||
Type resultType = result.Value.GetType();
|
|
||||||
context.Trace.Verbose($"Result type: {resultType.Name}");
|
|
||||||
if (typeInfo.IsAssignableFrom(resultType.GetTypeInfo()))
|
|
||||||
{
|
|
||||||
return (T)result.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Trace.Verbose($"Unable to assign result to the type {typeof(T).Name}");
|
|
||||||
throw new TypeCastException(fromType: resultType, toType: typeof(T));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Null
|
|
||||||
else if (result.Kind == ValueKind.Null)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
// String
|
|
||||||
else if (result.Kind == ValueKind.String)
|
|
||||||
{
|
|
||||||
// Treat empty string as null
|
|
||||||
String stringResult = result.Value as String;
|
|
||||||
if (String.IsNullOrEmpty(stringResult))
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise deserialize
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return JsonUtility.FromString<T>(stringResult);
|
|
||||||
}
|
|
||||||
catch (Exception exception) when (exception is JsonReaderException || exception is JsonSerializationException)
|
|
||||||
{
|
|
||||||
context.Trace.Verbose($"Failed to json-deserialize the result string into the type {typeof(T).Name}. {exception.Message}");
|
|
||||||
throw new TypeCastException(
|
|
||||||
context.SecretMasker,
|
|
||||||
value: stringResult,
|
|
||||||
fromKind: ValueKind.String,
|
|
||||||
toType: typeof(T),
|
|
||||||
error: exception.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Trace.Verbose($"Unable to convert from kind {result.Kind} to the type {typeof(T).Name}");
|
|
||||||
throw new TypeCastException(
|
|
||||||
context.SecretMasker,
|
|
||||||
value: result.Value,
|
|
||||||
fromKind: result.Kind,
|
|
||||||
toType: typeof(T));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (secretMasker != null && secretMasker != originalSecretMasker)
|
|
||||||
{
|
|
||||||
(secretMasker as IDisposable)?.Dispose();
|
|
||||||
secretMasker = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// INode entry point.
|
|
||||||
/// </summary>
|
|
||||||
public Object Evaluate(
|
|
||||||
ITraceWriter trace,
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
Object state,
|
|
||||||
EvaluationOptions options = null)
|
|
||||||
{
|
|
||||||
return Evaluate(trace, secretMasker, state, options, out _, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// INode entry point.
|
|
||||||
/// </summary>
|
|
||||||
public Boolean EvaluateBoolean(
|
|
||||||
ITraceWriter trace,
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
Object state)
|
|
||||||
{
|
|
||||||
return Evaluate<Boolean>(trace, secretMasker, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// INode entry point.
|
|
||||||
/// </summary>
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public EvaluationResult EvaluateResult(
|
|
||||||
ITraceWriter trace,
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
Object state,
|
|
||||||
EvaluationOptions options)
|
|
||||||
{
|
|
||||||
var val = Evaluate(trace, secretMasker, state, options, out ValueKind kind, out Object raw);
|
|
||||||
return new EvaluationResult(null, 0, val, kind, raw, omitTracing: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This function is intended only for ExpressionNode authors to call. The EvaluationContext
|
|
||||||
/// caches result-state specific to the evaluation instance.
|
|
||||||
/// </summary>
|
|
||||||
public EvaluationResult Evaluate(EvaluationContext context)
|
|
||||||
{
|
|
||||||
// Evaluate
|
|
||||||
Level = Container == null ? 0 : Container.Level + 1;
|
|
||||||
TraceVerbose(context, Level, $"Evaluating {Name}:");
|
|
||||||
var coreResult = EvaluateCore(context, out ResultMemory coreMemory);
|
|
||||||
|
|
||||||
if (coreMemory == null)
|
|
||||||
{
|
|
||||||
coreMemory = new ResultMemory();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to canonical value
|
|
||||||
var val = ExpressionUtil.ConvertToCanonicalValue(context.Options, coreResult, out ValueKind kind, out Object raw, out ResultMemory conversionMemory);
|
|
||||||
|
|
||||||
// The depth can be safely trimmed when the total size of the core result is known,
|
|
||||||
// or when the total size of the core result can easily be determined.
|
|
||||||
var trimDepth = coreMemory.IsTotal || (Object.ReferenceEquals(raw, null) && s_simpleKinds.Contains(kind));
|
|
||||||
|
|
||||||
// Account for the memory overhead of the core result
|
|
||||||
var coreBytes = coreMemory.Bytes ?? EvaluationMemory.CalculateBytes(raw ?? val);
|
|
||||||
context.Memory.AddAmount(Level, coreBytes, trimDepth);
|
|
||||||
|
|
||||||
// Account for the memory overhead of the conversion result
|
|
||||||
if (!Object.ReferenceEquals(raw, null))
|
|
||||||
{
|
|
||||||
if (conversionMemory == null)
|
|
||||||
{
|
|
||||||
conversionMemory = new ResultMemory();
|
|
||||||
}
|
|
||||||
|
|
||||||
var conversionBytes = conversionMemory.Bytes ?? EvaluationMemory.CalculateBytes(val);
|
|
||||||
context.Memory.AddAmount(Level, conversionBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new EvaluationResult(context, Level, val, kind, raw);
|
|
||||||
|
|
||||||
// Store the trace result
|
|
||||||
if (this.TraceFullyRealized)
|
|
||||||
{
|
|
||||||
context.SetTraceResult(this, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This function is intended only for ExpressionNode authors to call during evaluation.
|
|
||||||
/// The EvaluationContext caches result-state specific to the evaluation instance.
|
|
||||||
/// </summary>
|
|
||||||
public Boolean EvaluateBoolean(EvaluationContext context)
|
|
||||||
{
|
|
||||||
return Evaluate(context).ConvertToBoolean(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This function is intended only for ExpressionNode authors to call during evaluation.
|
|
||||||
/// The EvaluationContext caches result-state specific to the evaluation instance.
|
|
||||||
/// </summary>
|
|
||||||
public DateTimeOffset EvaluateDateTime(EvaluationContext context)
|
|
||||||
{
|
|
||||||
return Evaluate(context).ConvertToDateTime(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This function is intended only for ExpressionNode authors to call during evaluation.
|
|
||||||
/// The EvaluationContext caches result-state specific to the evaluation instance.
|
|
||||||
/// </summary>
|
|
||||||
public Decimal EvaluateNumber(EvaluationContext context)
|
|
||||||
{
|
|
||||||
return Evaluate(context).ConvertToNumber(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This function is intended only for ExpressionNode authors to call during evaluation.
|
|
||||||
/// The EvaluationContext caches result-state specific to the evaluation instance.
|
|
||||||
/// </summary>
|
|
||||||
public String EvaluateString(EvaluationContext context)
|
|
||||||
{
|
|
||||||
return Evaluate(context).ConvertToString(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This function is intended only for ExpressionNode authors to call during evaluation.
|
|
||||||
/// The EvaluationContext caches result-state specific to the evaluation instance.
|
|
||||||
/// </summary>
|
|
||||||
public Version EvaluateVersion(EvaluationContext context)
|
|
||||||
{
|
|
||||||
return Evaluate(context).ConvertToVersion(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual IEnumerable<T> GetParameters<T>() where T : IExpressionNode
|
|
||||||
{
|
|
||||||
return new T[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected MemoryCounter CreateMemoryCounter(EvaluationContext context)
|
|
||||||
{
|
|
||||||
return new MemoryCounter(this, context.Options.MaxMemory);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object Evaluate(
|
|
||||||
ITraceWriter trace,
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
Object state,
|
|
||||||
EvaluationOptions options,
|
|
||||||
out ValueKind kind,
|
|
||||||
out Object raw)
|
|
||||||
{
|
|
||||||
if (Container != null)
|
|
||||||
{
|
|
||||||
// Do not localize. This is an SDK consumer error.
|
|
||||||
throw new NotSupportedException($"Expected {nameof(IExpressionNode)}.{nameof(Evaluate)} to be called on root node only.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ISecretMasker originalSecretMasker = secretMasker;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Evaluate
|
|
||||||
secretMasker = secretMasker?.Clone() ?? new SecretMasker();
|
|
||||||
trace = new EvaluationTraceWriter(trace, secretMasker);
|
|
||||||
var context = new EvaluationContext(trace, secretMasker, state, options, this);
|
|
||||||
trace.Info($"Evaluating: {ConvertToExpression()}");
|
|
||||||
EvaluationResult result = Evaluate(context);
|
|
||||||
|
|
||||||
// Trace the result
|
|
||||||
TraceTreeResult(context, result.Value, result.Kind);
|
|
||||||
|
|
||||||
kind = result.Kind;
|
|
||||||
raw = result.Raw;
|
|
||||||
return result.Value;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (secretMasker != null && secretMasker != originalSecretMasker)
|
|
||||||
{
|
|
||||||
(secretMasker as IDisposable)?.Dispose();
|
|
||||||
secretMasker = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TraceTreeResult(
|
|
||||||
EvaluationContext context,
|
|
||||||
Object result,
|
|
||||||
ValueKind kind)
|
|
||||||
{
|
|
||||||
// Get the realized expression
|
|
||||||
String realizedExpression = ConvertToRealizedExpression(context);
|
|
||||||
|
|
||||||
// Format the result
|
|
||||||
String traceValue = ExpressionUtil.FormatValue(context.SecretMasker, result, kind);
|
|
||||||
|
|
||||||
// Only trace the realized expression if it is meaningfully different
|
|
||||||
if (!String.Equals(realizedExpression, traceValue, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
if (kind == ValueKind.Number &&
|
|
||||||
String.Equals(realizedExpression, $"'{traceValue}'", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
// Don't bother tracing the realized expression when the result is a number and the
|
|
||||||
// realized expresion is a precisely matching string.
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Trace.Info($"Expanded: {realizedExpression}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always trace the result
|
|
||||||
context.Trace.Info($"Result: {traceValue}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TraceVerbose(
|
|
||||||
EvaluationContext context,
|
|
||||||
Int32 level,
|
|
||||||
String message)
|
|
||||||
{
|
|
||||||
context.Trace.Verbose(String.Empty.PadLeft(level * 2, '.') + (message ?? String.Empty));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly ValueKind[] s_simpleKinds = new[]
|
|
||||||
{
|
|
||||||
ValueKind.Boolean,
|
|
||||||
ValueKind.DateTime,
|
|
||||||
ValueKind.Null,
|
|
||||||
ValueKind.Number,
|
|
||||||
ValueKind.String,
|
|
||||||
ValueKind.Version,
|
|
||||||
};
|
|
||||||
|
|
||||||
private String m_name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,547 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public sealed class ExpressionParser
|
|
||||||
{
|
|
||||||
public ExpressionParser(): this(null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public ExpressionParser(ExpressionParserOptions options)
|
|
||||||
{
|
|
||||||
m_parserOptions = options ?? new ExpressionParserOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IExpressionNode CreateTree(
|
|
||||||
String expression,
|
|
||||||
ITraceWriter trace,
|
|
||||||
IEnumerable<INamedValueInfo> namedValues,
|
|
||||||
IEnumerable<IFunctionInfo> functions)
|
|
||||||
{
|
|
||||||
var context = new ParseContext(expression, trace, namedValues, functions, allowUnknownKeywords: false, allowKeywordHyphens: m_parserOptions.AllowHyphens);
|
|
||||||
context.Trace.Info($"Parsing expression: <{expression}>");
|
|
||||||
return CreateTree(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ValidateSyntax(
|
|
||||||
String expression,
|
|
||||||
ITraceWriter trace)
|
|
||||||
{
|
|
||||||
var context = new ParseContext(expression, trace, namedValues: null, functions: null, allowUnknownKeywords: true, allowKeywordHyphens: m_parserOptions.AllowHyphens);
|
|
||||||
context.Trace.Info($"Validating expression syntax: <{expression}>");
|
|
||||||
CreateTree(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IExpressionNode CreateTree(ParseContext context)
|
|
||||||
{
|
|
||||||
while (TryGetNextToken(context))
|
|
||||||
{
|
|
||||||
switch (context.Token.Kind)
|
|
||||||
{
|
|
||||||
// Punctuation
|
|
||||||
case TokenKind.StartIndex:
|
|
||||||
HandleStartIndex(context);
|
|
||||||
break;
|
|
||||||
case TokenKind.EndIndex:
|
|
||||||
HandleEndIndex(context);
|
|
||||||
break;
|
|
||||||
case TokenKind.EndParameter:
|
|
||||||
HandleEndParameter(context);
|
|
||||||
break;
|
|
||||||
case TokenKind.Separator:
|
|
||||||
HandleSeparator(context);
|
|
||||||
break;
|
|
||||||
case TokenKind.Dereference:
|
|
||||||
HandleDereference(context);
|
|
||||||
break;
|
|
||||||
case TokenKind.Wildcard:
|
|
||||||
HandleWildcard(context);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Functions
|
|
||||||
case TokenKind.WellKnownFunction:
|
|
||||||
case TokenKind.ExtensionFunction:
|
|
||||||
HandleFunction(context);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Leaf values
|
|
||||||
case TokenKind.Boolean:
|
|
||||||
case TokenKind.Number:
|
|
||||||
case TokenKind.Version:
|
|
||||||
case TokenKind.String:
|
|
||||||
case TokenKind.ExtensionNamedValue:
|
|
||||||
HandleValue(context);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Unknown keyword
|
|
||||||
case TokenKind.UnknownKeyword:
|
|
||||||
HandleUnknownKeyword(context);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Malformed
|
|
||||||
case TokenKind.Unrecognized:
|
|
||||||
throw new ParseException(ParseExceptionKind.UnrecognizedValue, context.Token, context.Expression);
|
|
||||||
|
|
||||||
// Unexpected
|
|
||||||
case TokenKind.PropertyName: // PropertyName should never reach here (HandleDereference reads next token).
|
|
||||||
case TokenKind.StartParameter: // StartParameter is only expected by HandleFunction.
|
|
||||||
default:
|
|
||||||
throw new ParseException(ParseExceptionKind.UnexpectedSymbol, context.Token, context.Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate depth.
|
|
||||||
if (context.Containers.Count >= ExpressionConstants.MaxDepth)
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.ExceededMaxDepth, token: null, expression: context.Expression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate all containers were closed.
|
|
||||||
if (context.Containers.Count > 0)
|
|
||||||
{
|
|
||||||
ContainerInfo container = context.Containers.Peek();
|
|
||||||
if (container.Node is FunctionNode)
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnclosedFunction, container.Token, context.Expression);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnclosedIndexer, container.Token, context.Expression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.Root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryGetNextToken(ParseContext context)
|
|
||||||
{
|
|
||||||
context.LastToken = context.Token;
|
|
||||||
if (context.Lexer.TryGetNextToken(ref context.Token))
|
|
||||||
{
|
|
||||||
// Adjust indent level.
|
|
||||||
int indentLevel = context.Containers.Count;
|
|
||||||
if (indentLevel > 0)
|
|
||||||
{
|
|
||||||
switch (context.Token.Kind)
|
|
||||||
{
|
|
||||||
case TokenKind.StartParameter:
|
|
||||||
case TokenKind.EndParameter:
|
|
||||||
case TokenKind.EndIndex:
|
|
||||||
indentLevel--;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String indent = String.Empty.PadRight(indentLevel * 2, '.');
|
|
||||||
switch (context.Token.Kind)
|
|
||||||
{
|
|
||||||
// Literal values
|
|
||||||
case TokenKind.Boolean:
|
|
||||||
context.Trace.Verbose($"{indent}{ExpressionUtil.FormatValue(null, context.Token.ParsedValue, ValueKind.Boolean)}");
|
|
||||||
break;
|
|
||||||
case TokenKind.Number:
|
|
||||||
context.Trace.Verbose($"{indent}{ExpressionUtil.FormatValue(null, context.Token.ParsedValue, ValueKind.Number)}");
|
|
||||||
break;
|
|
||||||
case TokenKind.Version:
|
|
||||||
context.Trace.Verbose($"{indent}{ExpressionUtil.FormatValue(null, context.Token.ParsedValue, ValueKind.Version)}");
|
|
||||||
break;
|
|
||||||
case TokenKind.String:
|
|
||||||
context.Trace.Verbose($"{indent}{ExpressionUtil.FormatValue(null, context.Token.ParsedValue, ValueKind.String)}");
|
|
||||||
break;
|
|
||||||
// Property or unrecognized
|
|
||||||
case TokenKind.PropertyName:
|
|
||||||
case TokenKind.Unrecognized:
|
|
||||||
context.Trace.Verbose($"{indent}{context.Token.Kind} {ExpressionUtil.FormatValue(null, context.Token.RawValue, ValueKind.String)}");
|
|
||||||
break;
|
|
||||||
// Function or punctuation
|
|
||||||
case TokenKind.WellKnownFunction:
|
|
||||||
case TokenKind.ExtensionFunction:
|
|
||||||
case TokenKind.ExtensionNamedValue:
|
|
||||||
case TokenKind.Wildcard:
|
|
||||||
case TokenKind.UnknownKeyword:
|
|
||||||
case TokenKind.StartIndex:
|
|
||||||
case TokenKind.StartParameter:
|
|
||||||
case TokenKind.EndIndex:
|
|
||||||
case TokenKind.EndParameter:
|
|
||||||
case TokenKind.Separator:
|
|
||||||
case TokenKind.Dereference:
|
|
||||||
context.Trace.Verbose($"{indent}{context.Token.RawValue}");
|
|
||||||
break;
|
|
||||||
default: // Should never reach here.
|
|
||||||
throw new NotSupportedException($"Unexpected token kind: {context.Token.Kind}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void HandleStartIndex(ParseContext context)
|
|
||||||
{
|
|
||||||
// Validate follows ")", "]", "*", or a property name.
|
|
||||||
if (context.LastToken == null ||
|
|
||||||
(context.LastToken.Kind != TokenKind.EndParameter && context.LastToken.Kind != TokenKind.EndIndex && context.LastToken.Kind != TokenKind.PropertyName && context.LastToken.Kind != TokenKind.ExtensionNamedValue && context.LastToken.Kind != TokenKind.UnknownKeyword && context.LastToken.Kind != TokenKind.Wildcard))
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnexpectedSymbol, context.Token, context.Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap the object being indexed into.
|
|
||||||
var indexer = new IndexerNode();
|
|
||||||
ExpressionNode obj = null;
|
|
||||||
if (context.Containers.Count > 0)
|
|
||||||
{
|
|
||||||
ContainerNode container = context.Containers.Peek().Node;
|
|
||||||
Int32 objIndex = container.Parameters.Count - 1;
|
|
||||||
obj = container.Parameters[objIndex];
|
|
||||||
container.ReplaceParameter(objIndex, indexer);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
obj = context.Root;
|
|
||||||
context.Root = indexer;
|
|
||||||
}
|
|
||||||
|
|
||||||
indexer.AddParameter(obj);
|
|
||||||
|
|
||||||
// Update the container stack.
|
|
||||||
context.Containers.Push(new ContainerInfo() { Node = indexer, Token = context.Token });
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void HandleDereference(ParseContext context)
|
|
||||||
{
|
|
||||||
// Validate follows ")", "]", "*", or a property name.
|
|
||||||
if (context.LastToken == null ||
|
|
||||||
(context.LastToken.Kind != TokenKind.EndParameter && context.LastToken.Kind != TokenKind.EndIndex && context.LastToken.Kind != TokenKind.PropertyName && context.LastToken.Kind != TokenKind.ExtensionNamedValue && context.LastToken.Kind != TokenKind.UnknownKeyword && context.LastToken.Kind != TokenKind.Wildcard))
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnexpectedSymbol, context.Token, context.Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap the object being indexed into.
|
|
||||||
var indexer = new IndexerNode();
|
|
||||||
ExpressionNode obj = null;
|
|
||||||
if (context.Containers.Count > 0)
|
|
||||||
{
|
|
||||||
ContainerNode container = context.Containers.Peek().Node;
|
|
||||||
Int32 objIndex = container.Parameters.Count - 1;
|
|
||||||
obj = container.Parameters[objIndex];
|
|
||||||
container.ReplaceParameter(objIndex, indexer);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
obj = context.Root;
|
|
||||||
context.Root = indexer;
|
|
||||||
}
|
|
||||||
|
|
||||||
indexer.AddParameter(obj);
|
|
||||||
|
|
||||||
// Validate a token follows.
|
|
||||||
if (!TryGetNextToken(context))
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.ExpectedPropertyName, context.LastToken, context.Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.Token.Kind == TokenKind.PropertyName)
|
|
||||||
{
|
|
||||||
indexer.AddParameter(new LiteralValueNode(context.Token.RawValue));
|
|
||||||
}
|
|
||||||
else if (context.Token.Kind == TokenKind.Wildcard)
|
|
||||||
{
|
|
||||||
// For a wildcard we add a third parameter, a boolean set to true, so that we know it's a wildcard.
|
|
||||||
indexer.AddParameter(new LiteralValueNode(context.Token.RawValue));
|
|
||||||
indexer.AddParameter(new LiteralValueNode(true));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnexpectedSymbol, context.Token, context.Expression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void HandleWildcard(ParseContext context)
|
|
||||||
{
|
|
||||||
// Validate follows "[".
|
|
||||||
if (context.LastToken == null ||
|
|
||||||
context.LastToken.Kind != TokenKind.StartIndex)
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnexpectedSymbol, context.Token, context.Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
// When we have a wildcard, we add the wildcard and also third boolean parameter set to true.
|
|
||||||
// This lets us differentiate downstream from '*'.
|
|
||||||
context.Containers.Peek().Node.AddParameter(new LiteralValueNode(context.Token.RawValue));
|
|
||||||
context.Containers.Peek().Node.AddParameter(new LiteralValueNode(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void HandleEndParameter(ParseContext context)
|
|
||||||
{
|
|
||||||
ContainerInfo container = context.Containers.Count > 0 ? context.Containers.Peek() : null; // Validate:
|
|
||||||
if (container == null || // 1) Container is not null
|
|
||||||
!(container.Node is FunctionNode) || // 2) Container is a function
|
|
||||||
container.Node.Parameters.Count < GetMinParamCount(context, container.Token) || // 3) Not below min param threshold
|
|
||||||
container.Node.Parameters.Count > GetMaxParamCount(context, container.Token) || // 4) Not above max param threshold
|
|
||||||
context.LastToken.Kind == TokenKind.Separator) // 5) Last token is not a separator
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnexpectedSymbol, context.Token, context.Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Containers.Pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void HandleEndIndex(ParseContext context)
|
|
||||||
{
|
|
||||||
IndexerNode indexer = context.Containers.Count > 0 ? context.Containers.Peek().Node as IndexerNode : null;
|
|
||||||
// // Validate:
|
|
||||||
if (indexer == null || // 1) Container is an indexer
|
|
||||||
!(indexer.Parameters.Count == 2 || indexer.Parameters.Count == 3)) // 2) Can be 2 or 3 parameters. It's 3 parameters when we are using a filtered array since we
|
|
||||||
// set a boolean along with the wildcard.
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnexpectedSymbol, context.Token, context.Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Containers.Pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void HandleUnknownKeyword(ParseContext context)
|
|
||||||
{
|
|
||||||
// Validate.
|
|
||||||
if (!context.AllowUnknownKeywords)
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnrecognizedValue, context.Token, context.Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try handle function.
|
|
||||||
if (HandleFunction(context, bestEffort: true))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle named value.
|
|
||||||
HandleValue(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void HandleValue(ParseContext context)
|
|
||||||
{
|
|
||||||
// Validate either A) is the first token OR B) follows "[" "(" or ",".
|
|
||||||
if (context.LastToken != null &&
|
|
||||||
context.LastToken.Kind != TokenKind.StartIndex &&
|
|
||||||
context.LastToken.Kind != TokenKind.StartParameter &&
|
|
||||||
context.LastToken.Kind != TokenKind.Separator)
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnexpectedSymbol, context.Token, context.Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the node.
|
|
||||||
ExpressionNode node;
|
|
||||||
switch (context.Token.Kind)
|
|
||||||
{
|
|
||||||
case TokenKind.ExtensionNamedValue:
|
|
||||||
String name = context.Token.RawValue;
|
|
||||||
node = context.ExtensionNamedValues[name].CreateNode();
|
|
||||||
node.Name = name;
|
|
||||||
break;
|
|
||||||
case TokenKind.UnknownKeyword:
|
|
||||||
node = new UnknownNamedValueNode();
|
|
||||||
node.Name = context.Token.RawValue;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
node = new LiteralValueNode(context.Token.ParsedValue);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the tree.
|
|
||||||
if (context.Root == null)
|
|
||||||
{
|
|
||||||
context.Root = node;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Containers.Peek().Node.AddParameter(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void HandleSeparator(ParseContext context)
|
|
||||||
{
|
|
||||||
ContainerInfo container = context.Containers.Count > 0 ? context.Containers.Peek() : null; // Validate:
|
|
||||||
if (container == null || // 1) Container is not null
|
|
||||||
!(container.Node is FunctionNode) || // 2) Container is a function
|
|
||||||
container.Node.Parameters.Count < 1 || // 3) At least one parameter
|
|
||||||
container.Node.Parameters.Count >= GetMaxParamCount(context, container.Token) ||// 4) Under max parameters threshold
|
|
||||||
context.LastToken.Kind == TokenKind.Separator) // 5) Last token is not a separator
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnexpectedSymbol, context.Token, context.Expression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean HandleFunction(
|
|
||||||
ParseContext context,
|
|
||||||
Boolean bestEffort = false)
|
|
||||||
{
|
|
||||||
// Validate either A) is first token OR B) follows "," or "[" or "(".
|
|
||||||
if (context.LastToken != null &&
|
|
||||||
(context.LastToken.Kind != TokenKind.Separator &&
|
|
||||||
context.LastToken.Kind != TokenKind.StartIndex &&
|
|
||||||
context.LastToken.Kind != TokenKind.StartParameter))
|
|
||||||
{
|
|
||||||
if (bestEffort)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ParseException(ParseExceptionKind.UnexpectedSymbol, context.Token, context.Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate '(' follows.
|
|
||||||
if (bestEffort)
|
|
||||||
{
|
|
||||||
Token nextToken = null;
|
|
||||||
if (!context.Lexer.TryPeekNextToken(ref nextToken) || nextToken.Kind != TokenKind.StartParameter)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
TryGetNextToken(context);
|
|
||||||
}
|
|
||||||
else if (!TryGetNextToken(context) || context.Token.Kind != TokenKind.StartParameter)
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.ExpectedStartParameter, context.LastToken, context.Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the node.
|
|
||||||
FunctionNode node;
|
|
||||||
String name = context.LastToken.RawValue;
|
|
||||||
switch (context.LastToken.Kind)
|
|
||||||
{
|
|
||||||
case TokenKind.WellKnownFunction:
|
|
||||||
node = ExpressionConstants.WellKnownFunctions[name].CreateNode();
|
|
||||||
node.Name = name;
|
|
||||||
break;
|
|
||||||
case TokenKind.ExtensionFunction:
|
|
||||||
node = context.ExtensionFunctions[name].CreateNode();
|
|
||||||
node.Name = name;
|
|
||||||
break;
|
|
||||||
case TokenKind.UnknownKeyword:
|
|
||||||
node = new UnknownFunctionNode();
|
|
||||||
node.Name = name;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Should never reach here.
|
|
||||||
throw new NotSupportedException($"Unexpected function token name: '{context.LastToken.Kind}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the tree.
|
|
||||||
if (context.Root == null)
|
|
||||||
{
|
|
||||||
context.Root = node;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Containers.Peek().Node.AddParameter(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the container stack.
|
|
||||||
context.Containers.Push(new ContainerInfo() { Node = node, Token = context.LastToken });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int GetMinParamCount(
|
|
||||||
ParseContext context,
|
|
||||||
Token token)
|
|
||||||
{
|
|
||||||
switch (token.Kind)
|
|
||||||
{
|
|
||||||
case TokenKind.WellKnownFunction:
|
|
||||||
return ExpressionConstants.WellKnownFunctions[token.RawValue].MinParameters;
|
|
||||||
case TokenKind.ExtensionFunction:
|
|
||||||
return context.ExtensionFunctions[token.RawValue].MinParameters;
|
|
||||||
case TokenKind.UnknownKeyword:
|
|
||||||
return 0;
|
|
||||||
default: // Should never reach here.
|
|
||||||
throw new NotSupportedException($"Unexpected token kind '{token.Kind}'. Unable to determine min param count.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Int32 GetMaxParamCount(
|
|
||||||
ParseContext context,
|
|
||||||
Token token)
|
|
||||||
{
|
|
||||||
switch (token.Kind)
|
|
||||||
{
|
|
||||||
case TokenKind.WellKnownFunction:
|
|
||||||
return ExpressionConstants.WellKnownFunctions[token.RawValue].MaxParameters;
|
|
||||||
case TokenKind.ExtensionFunction:
|
|
||||||
return context.ExtensionFunctions[token.RawValue].MaxParameters;
|
|
||||||
case TokenKind.UnknownKeyword:
|
|
||||||
return Int32.MaxValue;
|
|
||||||
default: // Should never reach here.
|
|
||||||
throw new NotSupportedException($"Unexpected token kind '{token.Kind}'. Unable to determine max param count.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ExpressionParserOptions m_parserOptions;
|
|
||||||
|
|
||||||
private sealed class ContainerInfo
|
|
||||||
{
|
|
||||||
public ContainerNode Node { get; set; }
|
|
||||||
|
|
||||||
public Token Token { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ParseContext
|
|
||||||
{
|
|
||||||
public readonly Boolean AllowUnknownKeywords;
|
|
||||||
public readonly Stack<ContainerInfo> Containers = new Stack<ContainerInfo>();
|
|
||||||
public readonly String Expression;
|
|
||||||
public readonly Dictionary<String, IFunctionInfo> ExtensionFunctions = new Dictionary<String, IFunctionInfo>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
public readonly Dictionary<String, INamedValueInfo> ExtensionNamedValues = new Dictionary<String, INamedValueInfo>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
public readonly LexicalAnalyzer Lexer;
|
|
||||||
public readonly ITraceWriter Trace;
|
|
||||||
public Token Token;
|
|
||||||
public Token LastToken;
|
|
||||||
public ExpressionNode Root;
|
|
||||||
|
|
||||||
public ParseContext(
|
|
||||||
String expression,
|
|
||||||
ITraceWriter trace,
|
|
||||||
IEnumerable<INamedValueInfo> namedValues,
|
|
||||||
IEnumerable<IFunctionInfo> functions,
|
|
||||||
Boolean allowUnknownKeywords = false,
|
|
||||||
Boolean allowKeywordHyphens = false)
|
|
||||||
{
|
|
||||||
Expression = expression ?? String.Empty;
|
|
||||||
if (Expression.Length > ExpressionConstants.MaxLength)
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.ExceededMaxLength, token: null, expression: Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
Trace = trace ?? new NoOperationTraceWriter();
|
|
||||||
foreach (INamedValueInfo namedValueInfo in (namedValues ?? new INamedValueInfo[0]))
|
|
||||||
{
|
|
||||||
ExtensionNamedValues.Add(namedValueInfo.Name, namedValueInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (IFunctionInfo functionInfo in (functions ?? new IFunctionInfo[0]))
|
|
||||||
{
|
|
||||||
ExtensionFunctions.Add(functionInfo.Name, functionInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
AllowUnknownKeywords = allowUnknownKeywords;
|
|
||||||
Lexer = new LexicalAnalyzer(Expression, namedValues: ExtensionNamedValues.Keys, functions: ExtensionFunctions.Keys, allowKeywordHyphens);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class NoOperationTraceWriter : ITraceWriter
|
|
||||||
{
|
|
||||||
public void Info(String message)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Verbose(String message)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
public sealed class ExpressionParserOptions
|
|
||||||
{
|
|
||||||
public Boolean AllowHyphens
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Reflection;
|
|
||||||
using GitHub.DistributedTask.Logging;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
internal static class ExpressionUtil
|
|
||||||
{
|
|
||||||
internal static Object ConvertToCanonicalValue(
|
|
||||||
EvaluationOptions options,
|
|
||||||
Object val,
|
|
||||||
out ValueKind kind,
|
|
||||||
out Object raw,
|
|
||||||
out ResultMemory conversionResultMemory)
|
|
||||||
{
|
|
||||||
// Apply converter
|
|
||||||
if (options?.Converters?.Count > 0 &&
|
|
||||||
!Object.ReferenceEquals(val, null) &&
|
|
||||||
options.Converters.TryGetValue(val.GetType(), out Converter<Object, ConversionResult> convert))
|
|
||||||
{
|
|
||||||
raw = val;
|
|
||||||
var conversionResult = convert(val);
|
|
||||||
val = conversionResult.Result;
|
|
||||||
conversionResultMemory = conversionResult.ResultMemory;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
raw = null;
|
|
||||||
conversionResultMemory = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.ReferenceEquals(val, null))
|
|
||||||
{
|
|
||||||
kind = ValueKind.Null;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
else if (val is IString str)
|
|
||||||
{
|
|
||||||
kind = ValueKind.String;
|
|
||||||
return str.GetString();
|
|
||||||
}
|
|
||||||
else if (val is IBoolean booleanValue)
|
|
||||||
{
|
|
||||||
kind = ValueKind.Boolean;
|
|
||||||
return booleanValue.GetBoolean();
|
|
||||||
}
|
|
||||||
else if (val is INumber num)
|
|
||||||
{
|
|
||||||
kind = ValueKind.Number;
|
|
||||||
return num.GetNumber();
|
|
||||||
}
|
|
||||||
else if (val is JToken)
|
|
||||||
{
|
|
||||||
var jtoken = val as JToken;
|
|
||||||
switch (jtoken.Type)
|
|
||||||
{
|
|
||||||
case JTokenType.Array:
|
|
||||||
kind = ValueKind.Array;
|
|
||||||
return jtoken;
|
|
||||||
case JTokenType.Boolean:
|
|
||||||
kind = ValueKind.Boolean;
|
|
||||||
return jtoken.ToObject<Boolean>();
|
|
||||||
case JTokenType.Float:
|
|
||||||
case JTokenType.Integer:
|
|
||||||
kind = ValueKind.Number;
|
|
||||||
// todo: test the extents of the conversion
|
|
||||||
return jtoken.ToObject<Decimal>();
|
|
||||||
case JTokenType.Null:
|
|
||||||
kind = ValueKind.Null;
|
|
||||||
return null;
|
|
||||||
case JTokenType.Object:
|
|
||||||
kind = ValueKind.Object;
|
|
||||||
return jtoken;
|
|
||||||
case JTokenType.String:
|
|
||||||
kind = ValueKind.String;
|
|
||||||
return jtoken.ToObject<String>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (val is String)
|
|
||||||
{
|
|
||||||
kind = ValueKind.String;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
else if (val is Version)
|
|
||||||
{
|
|
||||||
kind = ValueKind.Version;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
else if (!val.GetType().GetTypeInfo().IsClass)
|
|
||||||
{
|
|
||||||
if (val is Boolean)
|
|
||||||
{
|
|
||||||
kind = ValueKind.Boolean;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
else if (val is DateTimeOffset)
|
|
||||||
{
|
|
||||||
kind = ValueKind.DateTime;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
else if (val is DateTime dateTime)
|
|
||||||
{
|
|
||||||
kind = ValueKind.DateTime;
|
|
||||||
switch (dateTime.Kind)
|
|
||||||
{
|
|
||||||
// When Local: convert to preferred time zone
|
|
||||||
case DateTimeKind.Local:
|
|
||||||
var targetTimeZone = options?.TimeZone ?? TimeZoneInfo.Local;
|
|
||||||
var localDateTimeOffset = new DateTimeOffset(dateTime);
|
|
||||||
return TimeZoneInfo.ConvertTime(localDateTimeOffset, targetTimeZone);
|
|
||||||
// When Unspecified: assume preferred time zone
|
|
||||||
case DateTimeKind.Unspecified:
|
|
||||||
var timeZone = options?.TimeZone ?? TimeZoneInfo.Local;
|
|
||||||
var offset = timeZone.GetUtcOffset(dateTime);
|
|
||||||
return new DateTimeOffset(dateTime, offset);
|
|
||||||
// When UTC: keep UTC
|
|
||||||
case DateTimeKind.Utc:
|
|
||||||
return new DateTimeOffset(dateTime);
|
|
||||||
default:
|
|
||||||
throw new NotSupportedException($"Unexpected DateTimeKind '{dateTime.Kind}'"); // Should never happen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (val is Decimal || val is Byte || val is SByte || val is Int16 || val is UInt16 || val is Int32 || val is UInt32 || val is Int64 || val is UInt64 || val is Single || val is Double)
|
|
||||||
{
|
|
||||||
kind = ValueKind.Number;
|
|
||||||
return Convert.ToDecimal(val);
|
|
||||||
}
|
|
||||||
else if (val is Enum)
|
|
||||||
{
|
|
||||||
var strVal = String.Format(CultureInfo.InvariantCulture, "{0:G}", val);
|
|
||||||
if (Decimal.TryParse(strVal, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out Decimal decVal))
|
|
||||||
{
|
|
||||||
kind = ValueKind.Number;
|
|
||||||
return decVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
kind = ValueKind.String;
|
|
||||||
return strVal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kind = ValueKind.Object;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static String FormatValue(
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
EvaluationResult evaluationResult)
|
|
||||||
{
|
|
||||||
return FormatValue(secretMasker, evaluationResult.Value, evaluationResult.Kind);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static String FormatValue(
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
Object value,
|
|
||||||
ValueKind kind)
|
|
||||||
{
|
|
||||||
switch (kind)
|
|
||||||
{
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
return ((Boolean)value).ToString();
|
|
||||||
|
|
||||||
case ValueKind.DateTime:
|
|
||||||
var strDateTime = "(DateTime)" + ((DateTimeOffset)value).ToString(ExpressionConstants.DateTimeFormat, CultureInfo.InvariantCulture);
|
|
||||||
return secretMasker != null ? secretMasker.MaskSecrets(strDateTime) : strDateTime;
|
|
||||||
|
|
||||||
case ValueKind.Number:
|
|
||||||
var strNumber = ((Decimal)value).ToString(ExpressionConstants.NumberFormat, CultureInfo.InvariantCulture);
|
|
||||||
return secretMasker != null ? secretMasker.MaskSecrets(strNumber) : strNumber;
|
|
||||||
|
|
||||||
case ValueKind.String:
|
|
||||||
// Mask secrets before string-escaping.
|
|
||||||
var strValue = secretMasker != null ? secretMasker.MaskSecrets(value as String) : value as String;
|
|
||||||
return $"'{StringEscape(strValue)}'";
|
|
||||||
|
|
||||||
case ValueKind.Version:
|
|
||||||
String strVersion = secretMasker != null ? secretMasker.MaskSecrets(value.ToString()) : value.ToString();
|
|
||||||
return $"v{strVersion}";
|
|
||||||
|
|
||||||
case ValueKind.Array:
|
|
||||||
case ValueKind.Null:
|
|
||||||
case ValueKind.Object:
|
|
||||||
return kind.ToString();
|
|
||||||
|
|
||||||
default: // Should never reach here.
|
|
||||||
throw new NotSupportedException($"Unable to convert to realized expression. Unexpected value kind: {kind}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Char SafeCharAt(
|
|
||||||
String str,
|
|
||||||
Int32 index)
|
|
||||||
{
|
|
||||||
if (str.Length > index)
|
|
||||||
{
|
|
||||||
return str[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
return '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static String StringEscape(String value)
|
|
||||||
{
|
|
||||||
return String.IsNullOrEmpty(value) ? String.Empty : value.Replace("'", "''");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,394 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
internal sealed class FormatNode : FunctionNode
|
|
||||||
{
|
|
||||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
|
||||||
{
|
|
||||||
var format = Parameters[0].EvaluateString(context);
|
|
||||||
var index = 0;
|
|
||||||
var result = new FormatResultBuilder(this, context, CreateMemoryCounter(context));
|
|
||||||
while (index < format.Length)
|
|
||||||
{
|
|
||||||
var lbrace = format.IndexOf('{', index);
|
|
||||||
var rbrace = format.IndexOf('}', index);
|
|
||||||
|
|
||||||
// Left brace
|
|
||||||
if (lbrace >= 0 && (rbrace < 0 || rbrace > lbrace))
|
|
||||||
{
|
|
||||||
// Escaped left brace
|
|
||||||
if (ExpressionUtil.SafeCharAt(format, lbrace + 1) == '{')
|
|
||||||
{
|
|
||||||
result.Append(format.Substring(index, lbrace - index + 1));
|
|
||||||
index = lbrace + 2;
|
|
||||||
}
|
|
||||||
// Left brace, number, optional format specifiers, right brace
|
|
||||||
else if (rbrace > lbrace + 1 &&
|
|
||||||
ReadArgIndex(format, lbrace + 1, out Byte argIndex, out Int32 endArgIndex) &&
|
|
||||||
ReadFormatSpecifiers(format, endArgIndex + 1, out String formatSpecifiers, out rbrace))
|
|
||||||
{
|
|
||||||
// Check parameter count
|
|
||||||
if (argIndex > Parameters.Count - 2)
|
|
||||||
{
|
|
||||||
throw new FormatException(ExpressionResources.InvalidFormatArgIndex(format));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the portion before the left brace
|
|
||||||
if (lbrace > index)
|
|
||||||
{
|
|
||||||
result.Append(format.Substring(index, lbrace - index));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the arg
|
|
||||||
result.Append(argIndex, formatSpecifiers);
|
|
||||||
index = rbrace + 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new FormatException(ExpressionResources.InvalidFormatString(format));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Right brace
|
|
||||||
else if (rbrace >= 0)
|
|
||||||
{
|
|
||||||
// Escaped right brace
|
|
||||||
if (ExpressionUtil.SafeCharAt(format, rbrace + 1) == '}')
|
|
||||||
{
|
|
||||||
result.Append(format.Substring(index, rbrace - index + 1));
|
|
||||||
index = rbrace + 2;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new FormatException(ExpressionResources.InvalidFormatString(format));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Last segment
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.Append(format.Substring(index));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Boolean ReadArgIndex(
|
|
||||||
String str,
|
|
||||||
Int32 startIndex,
|
|
||||||
out Byte result,
|
|
||||||
out Int32 endIndex)
|
|
||||||
{
|
|
||||||
// Count the number of digits
|
|
||||||
var length = 0;
|
|
||||||
while (Char.IsDigit(ExpressionUtil.SafeCharAt(str, startIndex + length)))
|
|
||||||
{
|
|
||||||
length++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate at least one digit
|
|
||||||
if (length < 1)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
endIndex = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the number
|
|
||||||
endIndex = startIndex + length - 1;
|
|
||||||
return Byte.TryParse(str.Substring(startIndex, length), NumberStyles.None, CultureInfo.InvariantCulture, out result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Boolean ReadFormatSpecifiers(
|
|
||||||
String str,
|
|
||||||
Int32 startIndex,
|
|
||||||
out String result,
|
|
||||||
out Int32 rbrace)
|
|
||||||
{
|
|
||||||
// No format specifiers
|
|
||||||
var c = ExpressionUtil.SafeCharAt(str, startIndex);
|
|
||||||
if (c == '}')
|
|
||||||
{
|
|
||||||
result = String.Empty;
|
|
||||||
rbrace = startIndex;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate starts with ":"
|
|
||||||
if (c != ':')
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
rbrace = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the specifiers
|
|
||||||
var specifiers = new StringBuilder();
|
|
||||||
var index = startIndex + 1;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
// Validate not the end of the string
|
|
||||||
if (index >= str.Length)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
rbrace = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
c = str[index];
|
|
||||||
|
|
||||||
// Not right-brace
|
|
||||||
if (c != '}')
|
|
||||||
{
|
|
||||||
specifiers.Append(c);
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
// Escaped right-brace
|
|
||||||
else if (ExpressionUtil.SafeCharAt(str, index + 1) == '}')
|
|
||||||
{
|
|
||||||
specifiers.Append('}');
|
|
||||||
index += 2;
|
|
||||||
}
|
|
||||||
// Closing right-brace
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result = specifiers.ToString();
|
|
||||||
rbrace = index;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class FormatResultBuilder
|
|
||||||
{
|
|
||||||
internal FormatResultBuilder(
|
|
||||||
FormatNode node,
|
|
||||||
EvaluationContext context,
|
|
||||||
MemoryCounter counter)
|
|
||||||
{
|
|
||||||
m_node = node;
|
|
||||||
m_context = context;
|
|
||||||
m_counter = counter;
|
|
||||||
m_cache = new ArgValue[node.Parameters.Count - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the final string. This is when lazy segments are evaluated.
|
|
||||||
public override String ToString()
|
|
||||||
{
|
|
||||||
return String.Join(
|
|
||||||
String.Empty,
|
|
||||||
m_segments.Select(obj =>
|
|
||||||
{
|
|
||||||
if (obj is Lazy<String> lazy)
|
|
||||||
{
|
|
||||||
return lazy.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return obj as String;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append a static value
|
|
||||||
internal void Append(String value)
|
|
||||||
{
|
|
||||||
if (value?.Length > 0)
|
|
||||||
{
|
|
||||||
// Track memory
|
|
||||||
m_counter.Add(value);
|
|
||||||
|
|
||||||
// Append the segment
|
|
||||||
m_segments.Add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append an argument
|
|
||||||
internal void Append(
|
|
||||||
Int32 argIndex,
|
|
||||||
String formatSpecifiers)
|
|
||||||
{
|
|
||||||
// Delay execution until the final ToString
|
|
||||||
m_segments.Add(new Lazy<String>(() =>
|
|
||||||
{
|
|
||||||
String result;
|
|
||||||
|
|
||||||
// Get the arg from the cache
|
|
||||||
var argValue = m_cache[argIndex];
|
|
||||||
|
|
||||||
// Evaluate the arg and cache the result
|
|
||||||
if (argValue == null)
|
|
||||||
{
|
|
||||||
// The evaluation result is required when format specifiers are used. Otherwise the string
|
|
||||||
// result is required. Go ahead and store both values. Since ConvertToString produces tracing,
|
|
||||||
// we need to run that now so the tracing appears in order in the log.
|
|
||||||
var evaluationResult = m_node.Parameters[argIndex + 1].Evaluate(m_context);
|
|
||||||
var stringResult = evaluationResult.ConvertToString(m_context);
|
|
||||||
argValue = new ArgValue(evaluationResult, stringResult);
|
|
||||||
m_cache[argIndex] = argValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No format specifiers
|
|
||||||
if (String.IsNullOrEmpty(formatSpecifiers))
|
|
||||||
{
|
|
||||||
result = argValue.StringResult;
|
|
||||||
}
|
|
||||||
// DateTime
|
|
||||||
else if (argValue.EvaluationResult.Kind == ValueKind.DateTime)
|
|
||||||
{
|
|
||||||
result = FormatDateTime((DateTimeOffset)argValue.EvaluationResult.Value, formatSpecifiers);
|
|
||||||
}
|
|
||||||
// Invalid
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new FormatException(ExpressionResources.InvalidFormatSpecifiers(formatSpecifiers, argValue.EvaluationResult.Kind));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track memory
|
|
||||||
if (!String.IsNullOrEmpty(result))
|
|
||||||
{
|
|
||||||
m_counter.Add(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String FormatDateTime(
|
|
||||||
DateTimeOffset dateTime,
|
|
||||||
String specifiers)
|
|
||||||
{
|
|
||||||
var result = new StringBuilder();
|
|
||||||
var i = 0;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
// Get the next specifier
|
|
||||||
var specifier = GetNextSpecifier(specifiers, ref i);
|
|
||||||
|
|
||||||
// Check end of string
|
|
||||||
if (String.IsNullOrEmpty(specifier))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the value
|
|
||||||
switch (specifier)
|
|
||||||
{
|
|
||||||
case "yyyy":
|
|
||||||
case "yy":
|
|
||||||
case "MM":
|
|
||||||
case "dd":
|
|
||||||
case "HH":
|
|
||||||
case "mm":
|
|
||||||
case "ss":
|
|
||||||
case "ff":
|
|
||||||
case "fff":
|
|
||||||
case "ffff":
|
|
||||||
case "fffff":
|
|
||||||
case "ffffff":
|
|
||||||
case "fffffff":
|
|
||||||
case "zzz":
|
|
||||||
result.Append(dateTime.ToString(specifier));
|
|
||||||
break;
|
|
||||||
|
|
||||||
// .Net requires a leading % for some specifiers
|
|
||||||
case "M":
|
|
||||||
case "d":
|
|
||||||
case "H":
|
|
||||||
case "m":
|
|
||||||
case "s":
|
|
||||||
case "f":
|
|
||||||
case "K":
|
|
||||||
result.Append(dateTime.ToString("%" + specifier));
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Escaped character
|
|
||||||
if (specifier[0] == '\\')
|
|
||||||
{
|
|
||||||
result.Append(specifier[1]);
|
|
||||||
}
|
|
||||||
else if (specifier[0] == ' ')
|
|
||||||
{
|
|
||||||
result.Append(specifier);
|
|
||||||
}
|
|
||||||
// Unexpected
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new FormatException(ExpressionResources.InvalidFormatSpecifiers(specifiers, ValueKind.DateTime));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String GetNextSpecifier(
|
|
||||||
String specifiers,
|
|
||||||
ref Int32 index)
|
|
||||||
{
|
|
||||||
// End of string
|
|
||||||
if (index >= specifiers.Length)
|
|
||||||
{
|
|
||||||
return String.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the first char
|
|
||||||
var startIndex = index;
|
|
||||||
var c = specifiers[index++];
|
|
||||||
|
|
||||||
// Escaped
|
|
||||||
if (c == '\\')
|
|
||||||
{
|
|
||||||
// End of string
|
|
||||||
if (index >= specifiers.Length)
|
|
||||||
{
|
|
||||||
throw new FormatException(ExpressionResources.InvalidFormatSpecifiers(specifiers, ValueKind.DateTime));
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
// Find consecutive matches
|
|
||||||
else
|
|
||||||
{
|
|
||||||
while (index < specifiers.Length && specifiers[index] == c)
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return specifiers.Substring(startIndex, index - startIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly ArgValue[] m_cache;
|
|
||||||
private readonly EvaluationContext m_context;
|
|
||||||
private readonly MemoryCounter m_counter;
|
|
||||||
private readonly FormatNode m_node;
|
|
||||||
private readonly List<Object> m_segments = new List<Object>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stores an EvaluateResult and the value converted to a String.
|
|
||||||
/// </summary>
|
|
||||||
private sealed class ArgValue
|
|
||||||
{
|
|
||||||
public ArgValue(
|
|
||||||
EvaluationResult evaluationResult,
|
|
||||||
String stringResult)
|
|
||||||
{
|
|
||||||
EvaluationResult = evaluationResult;
|
|
||||||
StringResult = stringResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EvaluationResult EvaluationResult { get; }
|
|
||||||
|
|
||||||
public String StringResult { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
public class FunctionInfo<T> : IFunctionInfo
|
|
||||||
where T : FunctionNode, new()
|
|
||||||
{
|
|
||||||
public FunctionInfo(String name, Int32 minParameters, Int32 maxParameters)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
MinParameters = minParameters;
|
|
||||||
MaxParameters = maxParameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String Name { get; }
|
|
||||||
|
|
||||||
public Int32 MinParameters { get; }
|
|
||||||
|
|
||||||
public Int32 MaxParameters { get; }
|
|
||||||
|
|
||||||
public FunctionNode CreateNode()
|
|
||||||
{
|
|
||||||
return new T();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public abstract class FunctionNode : ContainerNode
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Generally this should not be overridden. True indicates the result of the node is traced as part of the "expanded"
|
|
||||||
/// (i.e. "realized") trace information. Otherwise the node expression is printed, and parameters to the node may or
|
|
||||||
/// may not be fully realized - depending on each respective parameter's trace-fully-realized setting.
|
|
||||||
///
|
|
||||||
/// The purpose is so the end user can understand how their expression expanded at run time. For example, consider
|
|
||||||
/// the expression: eq(variables.publish, 'true'). The runtime-expanded expression may be: eq('true', 'true')
|
|
||||||
/// </summary>
|
|
||||||
protected override Boolean TraceFullyRealized => true;
|
|
||||||
|
|
||||||
internal sealed override String ConvertToExpression()
|
|
||||||
{
|
|
||||||
return String.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"{0}({1})",
|
|
||||||
Name,
|
|
||||||
String.Join(", ", Parameters.Select(x => x.ConvertToExpression())));
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed override String ConvertToRealizedExpression(EvaluationContext context)
|
|
||||||
{
|
|
||||||
// Check if the result was stored
|
|
||||||
if (context.TryGetTraceResult(this, out String result))
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return String.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"{0}({1})",
|
|
||||||
Name,
|
|
||||||
String.Join(", ", Parameters.Select(x => x.ConvertToRealizedExpression(context))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
internal sealed class GreaterThanNode : FunctionNode
|
|
||||||
{
|
|
||||||
protected sealed override Boolean TraceFullyRealized => false;
|
|
||||||
|
|
||||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
|
||||||
{
|
|
||||||
return Parameters[0].Evaluate(context).CompareTo(context, Parameters[1].Evaluate(context)) > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
internal sealed class GreaterThanOrEqualNode : FunctionNode
|
|
||||||
{
|
|
||||||
protected sealed override Boolean TraceFullyRealized => false;
|
|
||||||
|
|
||||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
|
||||||
{
|
|
||||||
return Parameters[0].Evaluate(context).CompareTo(context, Parameters[1].Evaluate(context)) >= 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public interface IBoolean
|
|
||||||
{
|
|
||||||
Boolean GetBoolean();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using GitHub.DistributedTask.Logging;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public interface IExpressionNode
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluates the expression and attempts to cast or deserialize the result to the specified
|
|
||||||
/// type. The specified type can either be simple type or a JSON-serializable class. Allowed
|
|
||||||
/// simple types are: Boolean, String, Version, Byte, SByte, Int16, UInt16, Int32, UInt32,
|
|
||||||
/// Int64, UInt64, Single, Double, or Decimal. When a JSON-serializable class is specified, the
|
|
||||||
/// following rules are applied: If the type of the evaluation result object, is assignable to
|
|
||||||
/// the specified type, then the result will be cast and returned. If the evaluation result
|
|
||||||
/// object is a String, it will be deserialized as the specified type. If the evaluation result
|
|
||||||
/// object is null, null will be returned.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="trace">Optional trace writer</param>
|
|
||||||
/// <param name="secretMasker">Optional secret masker</param>
|
|
||||||
/// <param name="state">State object for custom evaluation function nodes and custom named-value nodes</param>
|
|
||||||
T Evaluate<T>(
|
|
||||||
ITraceWriter trace,
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
Object state,
|
|
||||||
EvaluationOptions options = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluates the expression and returns the result.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="trace">Optional trace writer</param>
|
|
||||||
/// <param name="secretMasker">Optional secret masker</param>
|
|
||||||
/// <param name="state">State object for custom evaluation function nodes and custom named-value nodes</param>
|
|
||||||
Object Evaluate(
|
|
||||||
ITraceWriter trace,
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
Object state,
|
|
||||||
EvaluationOptions options = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluates the expression and casts the result to a Boolean.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="trace">Optional trace writer</param>
|
|
||||||
/// <param name="secretMasker">Optional secret masker</param>
|
|
||||||
/// <param name="state">State object for custom evaluation function nodes and custom named-value nodes</param>
|
|
||||||
Boolean EvaluateBoolean(
|
|
||||||
ITraceWriter trace,
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
Object state);
|
|
||||||
|
|
||||||
IEnumerable<T> GetParameters<T>() where T : IExpressionNode;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluates the expression and returns the result, wrapped in the SDK helper
|
|
||||||
/// for converting, comparing, and traversing objects.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="trace">Optional trace writer</param>
|
|
||||||
/// <param name="secretMasker">Optional secret masker</param>
|
|
||||||
/// <param name="state">State object for custom evaluation function nodes and custom named-value nodes</param>
|
|
||||||
/// <param name="options">Evaluation options</param>
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
EvaluationResult EvaluateResult(
|
|
||||||
ITraceWriter trace,
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
Object state,
|
|
||||||
EvaluationOptions options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions
|
|
||||||
{
|
|
||||||
public interface IFunctionInfo
|
|
||||||
{
|
|
||||||
String Name { get; }
|
|
||||||
Int32 MinParameters { get; }
|
|
||||||
Int32 MaxParameters { get; }
|
|
||||||
FunctionNode CreateNode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user