GitHub Actions Runner

This commit is contained in:
Tingluo Huang
2019-10-10 00:52:42 -04:00
commit c8afc84840
1255 changed files with 198670 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace GitHub.DistributedTask.Logging
{
internal interface ISecret
{
/// <summary>
/// Returns one item (start, length) for each match found in the input string.
/// </summary>
IEnumerable<ReplacementPosition> GetPositions(String input);
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.ComponentModel;
namespace GitHub.DistributedTask.Logging
{
[EditorBrowsable(EditorBrowsableState.Never)]
public interface ISecretMasker
{
void AddRegex(String pattern);
void AddValue(String value);
void AddValueEncoder(ValueEncoder encoder);
ISecretMasker Clone();
String MaskSecrets(String input);
}
}

View File

@@ -0,0 +1,50 @@
using GitHub.Services.Common;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace GitHub.DistributedTask.Logging
{
internal sealed class RegexSecret : ISecret
{
public RegexSecret(String pattern)
{
ArgumentUtility.CheckStringForNullOrEmpty(pattern, nameof(pattern));
m_pattern = pattern;
m_regex = new Regex(pattern);
}
public override Boolean Equals(Object obj)
{
var item = obj as RegexSecret;
if (item == null)
{
return false;
}
return String.Equals(m_pattern, item.m_pattern, StringComparison.Ordinal);
}
public override int GetHashCode() => m_pattern.GetHashCode();
public IEnumerable<ReplacementPosition> GetPositions(String input)
{
Int32 startIndex = 0;
while (startIndex < input.Length)
{
var match = m_regex.Match(input, startIndex);
if (match.Success)
{
startIndex = match.Index + 1;
yield return new ReplacementPosition(match.Index, match.Length);
}
else
{
yield break;
}
}
}
private readonly String m_pattern;
private readonly Regex m_regex;
}
}

View File

@@ -0,0 +1,29 @@
using System;
namespace GitHub.DistributedTask.Logging
{
internal sealed class ReplacementPosition
{
public ReplacementPosition(Int32 start, Int32 length)
{
Start = start;
Length = length;
}
public ReplacementPosition(ReplacementPosition copy)
{
Start = copy.Start;
Length = copy.Length;
}
public Int32 Start { get; set; }
public Int32 Length { get; set; }
public Int32 End
{
get
{
return Start + Length;
}
}
}
}

View File

@@ -0,0 +1,298 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
namespace GitHub.DistributedTask.Logging
{
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class SecretMasker : ISecretMasker, IDisposable
{
public SecretMasker()
{
m_originalValueSecrets = new HashSet<ValueSecret>();
m_regexSecrets = new HashSet<RegexSecret>();
m_valueEncoders = new HashSet<ValueEncoder>();
m_valueSecrets = new HashSet<ValueSecret>();
}
private SecretMasker(SecretMasker copy)
{
// Read section.
try
{
copy.m_lock.EnterReadLock();
// Copy the hash sets.
m_originalValueSecrets = new HashSet<ValueSecret>(copy.m_originalValueSecrets);
m_regexSecrets = new HashSet<RegexSecret>(copy.m_regexSecrets);
m_valueEncoders = new HashSet<ValueEncoder>(copy.m_valueEncoders);
m_valueSecrets = new HashSet<ValueSecret>(copy.m_valueSecrets);
}
finally
{
if (copy.m_lock.IsReadLockHeld)
{
copy.m_lock.ExitReadLock();
}
}
}
/// <summary>
/// This implementation assumes no more than one thread is adding regexes, values, or encoders at any given time.
/// </summary>
public void AddRegex(String pattern)
{
// Test for empty.
if (String.IsNullOrEmpty(pattern))
{
return;
}
// Write section.
try
{
m_lock.EnterWriteLock();
// Add the value.
m_regexSecrets.Add(new RegexSecret(pattern));
}
finally
{
if (m_lock.IsWriteLockHeld)
{
m_lock.ExitWriteLock();
}
}
}
/// <summary>
/// This implementation assumes no more than one thread is adding regexes, values, or encoders at any given time.
/// </summary>
public void AddValue(String value)
{
// Test for empty.
if (String.IsNullOrEmpty(value))
{
return;
}
var valueSecrets = new List<ValueSecret>(new[] { new ValueSecret(value) });
// Read section.
ValueEncoder[] valueEncoders;
try
{
m_lock.EnterReadLock();
// Test whether already added.
if (m_originalValueSecrets.Contains(valueSecrets[0]))
{
return;
}
// Read the value encoders.
valueEncoders = m_valueEncoders.ToArray();
}
finally
{
if (m_lock.IsReadLockHeld)
{
m_lock.ExitReadLock();
}
}
// Compute the encoded values.
foreach (ValueEncoder valueEncoder in valueEncoders)
{
String encodedValue = valueEncoder(value);
if (!String.IsNullOrEmpty(encodedValue))
{
valueSecrets.Add(new ValueSecret(encodedValue));
}
}
// Write section.
try
{
m_lock.EnterWriteLock();
// Add the values.
m_originalValueSecrets.Add(valueSecrets[0]);
foreach (ValueSecret valueSecret in valueSecrets)
{
m_valueSecrets.Add(valueSecret);
}
}
finally
{
if (m_lock.IsWriteLockHeld)
{
m_lock.ExitWriteLock();
}
}
}
/// <summary>
/// This implementation assumes no more than one thread is adding regexes, values, or encoders at any given time.
/// </summary>
public void AddValueEncoder(ValueEncoder encoder)
{
ValueSecret[] originalSecrets;
// Read section.
try
{
m_lock.EnterReadLock();
// Test whether already added.
if (m_valueEncoders.Contains(encoder))
{
return;
}
// Read the original value secrets.
originalSecrets = m_originalValueSecrets.ToArray();
}
finally
{
if (m_lock.IsReadLockHeld)
{
m_lock.ExitReadLock();
}
}
// Compute the encoded values.
var encodedSecrets = new List<ValueSecret>();
foreach (ValueSecret originalSecret in originalSecrets)
{
String encodedValue = encoder(originalSecret.m_value);
if (!String.IsNullOrEmpty(encodedValue))
{
encodedSecrets.Add(new ValueSecret(encodedValue));
}
}
// Write section.
try
{
m_lock.EnterWriteLock();
// Add the encoder.
m_valueEncoders.Add(encoder);
// Add the values.
foreach (ValueSecret encodedSecret in encodedSecrets)
{
m_valueSecrets.Add(encodedSecret);
}
}
finally
{
if (m_lock.IsWriteLockHeld)
{
m_lock.ExitWriteLock();
}
}
}
public ISecretMasker Clone() => new SecretMasker(this);
public void Dispose()
{
m_lock?.Dispose();
m_lock = null;
}
public String MaskSecrets(String input)
{
if (String.IsNullOrEmpty(input))
{
return String.Empty;
}
var secretPositions = new List<ReplacementPosition>();
// Read section.
try
{
m_lock.EnterReadLock();
// Get indexes and lengths of all substrings that will be replaced.
foreach (RegexSecret regexSecret in m_regexSecrets)
{
secretPositions.AddRange(regexSecret.GetPositions(input));
}
foreach (ValueSecret valueSecret in m_valueSecrets)
{
secretPositions.AddRange(valueSecret.GetPositions(input));
}
}
finally
{
if (m_lock.IsReadLockHeld)
{
m_lock.ExitReadLock();
}
}
// Short-circuit if nothing to replace.
if (secretPositions.Count == 0)
{
return input;
}
// Merge positions into ranges of characters to replace.
List<ReplacementPosition> replacementPositions = new List<ReplacementPosition>();
ReplacementPosition currentReplacement = null;
foreach (ReplacementPosition secretPosition in secretPositions.OrderBy(x => x.Start))
{
if (currentReplacement == null)
{
currentReplacement = new ReplacementPosition(copy: secretPosition);
replacementPositions.Add(currentReplacement);
}
else
{
if (secretPosition.Start <= currentReplacement.End)
{
// Overlap
currentReplacement.Length = Math.Max(currentReplacement.End, secretPosition.End) - currentReplacement.Start;
}
else
{
// No overlap
currentReplacement = new ReplacementPosition(copy: secretPosition);
replacementPositions.Add(currentReplacement);
}
}
}
// Replace
var stringBuilder = new StringBuilder();
Int32 startIndex = 0;
foreach (var replacement in replacementPositions)
{
stringBuilder.Append(input.Substring(startIndex, replacement.Start - startIndex));
stringBuilder.Append("***");
startIndex = replacement.Start + replacement.Length;
}
if (startIndex < input.Length)
{
stringBuilder.Append(input.Substring(startIndex));
}
return stringBuilder.ToString();
}
private readonly HashSet<ValueSecret> m_originalValueSecrets;
private readonly HashSet<RegexSecret> m_regexSecrets;
private readonly HashSet<ValueEncoder> m_valueEncoders;
private readonly HashSet<ValueSecret> m_valueSecrets;
private ReaderWriterLockSlim m_lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
}
}

View File

@@ -0,0 +1,116 @@
using System;
using System.ComponentModel;
using System.Security;
using System.Text;
using Newtonsoft.Json;
namespace GitHub.DistributedTask.Logging
{
[EditorBrowsable(EditorBrowsableState.Never)]
public delegate String ValueEncoder(String value);
[EditorBrowsable(EditorBrowsableState.Never)]
public static class ValueEncoders
{
public static String Base64StringEscape(String value)
{
return Convert.ToBase64String(Encoding.UTF8.GetBytes(value));
}
// Base64 is 6 bytes -> char
// When end user doing somthing like base64(user:password)
// The length of the leading content will cause different base64 encoding result on the password
// So we add base64(value - 1/2/3/4/5 bytes) as secret as well.
public static String Base64StringEscapeShift1(String value)
{
return Base64StringEscapeShift(value, 1);
}
public static String Base64StringEscapeShift2(String value)
{
return Base64StringEscapeShift(value, 2);
}
public static String Base64StringEscapeShift3(String value)
{
return Base64StringEscapeShift(value, 3);
}
public static String Base64StringEscapeShift4(String value)
{
return Base64StringEscapeShift(value, 4);
}
public static String Base64StringEscapeShift5(String value)
{
return Base64StringEscapeShift(value, 5);
}
public static String ExpressionStringEscape(String value)
{
return Expressions.ExpressionUtil.StringEscape(value);
}
public static String JsonStringEscape(String value)
{
// Convert to a JSON string and then remove the leading/trailing double-quote.
String jsonString = JsonConvert.ToString(value);
String jsonEscapedValue = jsonString.Substring(startIndex: 1, length: jsonString.Length - 2);
return jsonEscapedValue;
}
public static String UriDataEscape(String value)
{
return UriDataEscape(value, 65519);
}
public static String XmlDataEscape(String value)
{
return SecurityElement.Escape(value);
}
private static string Base64StringEscapeShift(String value, int shift)
{
var bytes = Encoding.UTF8.GetBytes(value);
if (bytes.Length > shift)
{
var shiftArray = new byte[bytes.Length - shift];
Array.Copy(bytes, shift, shiftArray, 0, bytes.Length - shift);
return Convert.ToBase64String(shiftArray);
}
else
{
return Convert.ToBase64String(bytes);
}
}
private static String UriDataEscape(
String value,
Int32 maxSegmentSize)
{
if (value.Length <= maxSegmentSize)
{
return Uri.EscapeDataString(value);
}
// Workaround size limitation in Uri.EscapeDataString
var result = new StringBuilder();
var i = 0;
do
{
var length = Math.Min(value.Length - i, maxSegmentSize);
if (Char.IsHighSurrogate(value[i + length - 1]) && length > 1)
{
length--;
}
result.Append(Uri.EscapeDataString(value.Substring(i, length)));
i += length;
}
while (i < value.Length);
return result.ToString();
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using GitHub.Services.Common;
namespace GitHub.DistributedTask.Logging
{
internal sealed class ValueSecret : ISecret
{
public ValueSecret(String value)
{
ArgumentUtility.CheckStringForNullOrEmpty(value, nameof(value));
m_value = value;
}
public override Boolean Equals(Object obj)
{
var item = obj as ValueSecret;
if (item == null)
{
return false;
}
return String.Equals(m_value, item.m_value, StringComparison.Ordinal);
}
public override Int32 GetHashCode() => m_value.GetHashCode();
public IEnumerable<ReplacementPosition> GetPositions(String input)
{
if (!String.IsNullOrEmpty(input) && !String.IsNullOrEmpty(m_value))
{
Int32 startIndex = 0;
while (startIndex > -1 &&
startIndex < input.Length &&
input.Length - startIndex >= m_value.Length) // remaining substring longer than secret value
{
startIndex = input.IndexOf(m_value, startIndex, StringComparison.Ordinal);
if (startIndex > -1)
{
yield return new ReplacementPosition(startIndex, m_value.Length);
++startIndex;
}
}
}
}
internal readonly String m_value;
}
}