diff --git a/src/Agent.Sdk/Util/SslUtil.cs b/src/Agent.Sdk/Util/SslUtil.cs
index 5d6f2b1011..a74bdb8332 100644
--- a/src/Agent.Sdk/Util/SslUtil.cs
+++ b/src/Agent.Sdk/Util/SslUtil.cs
@@ -1,22 +1,24 @@
-using System.Net.Security;
-using System.Security.Cryptography.X509Certificates;
-using System.Net.Http;
using Agent.Sdk;
+using System;
using System.Collections.Generic;
using System.Linq;
-using System;
+using System.Net.Http;
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
namespace Microsoft.VisualStudio.Services.Agent.Util
{
public sealed class SslUtil
{
- private ITraceWriter _trace { get; set; }
- private bool _IgnoreCertificateErrors { get; set; }
+ private readonly ITraceWriter _trace;
- public SslUtil(ITraceWriter trace, bool IgnoreCertificateErrors = false)
+ private readonly bool _ignoreCertificateErrors;
+
+ public SslUtil(ITraceWriter trace, bool ignoreCertificateErrors = false)
{
this._trace = trace;
- this._IgnoreCertificateErrors = IgnoreCertificateErrors;
+ this._ignoreCertificateErrors = ignoreCertificateErrors;
}
/// Implementation of custom callback function that logs SSL-related data from the web request to the agent's logs.
@@ -24,38 +26,42 @@ public SslUtil(ITraceWriter trace, bool IgnoreCertificateErrors = false)
public bool RequestStatusCustomValidation(HttpRequestMessage requestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslErrors)
{
bool isRequestSuccessful = (sslErrors == SslPolicyErrors.None);
-
+
if (!isRequestSuccessful)
{
LoggingRequestDiagnosticData(requestMessage, certificate, chain, sslErrors);
}
- if (this._IgnoreCertificateErrors) {
- this._trace?.Info("Ignoring certificate errors.");
+ if (this._ignoreCertificateErrors)
+ {
+ this._trace?.Info("Ignoring certificate errors.");
}
- return this._IgnoreCertificateErrors || isRequestSuccessful;
+ return this._ignoreCertificateErrors || isRequestSuccessful;
}
/// Logs SSL related data to agent's logs
private void LoggingRequestDiagnosticData(HttpRequestMessage requestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslErrors)
{
- string diagInfo = "Diagnostic data for request:\n";
-
if (this._trace != null)
{
- diagInfo += SslDiagnosticDataProvider.ResolveSslPolicyErrorsMessage(sslErrors);
- diagInfo += SslDiagnosticDataProvider.GetRequestMessageData(requestMessage);
- diagInfo += SslDiagnosticDataProvider.GetCertificateData(certificate);
- diagInfo += SslDiagnosticDataProvider.GetChainData(chain);
+ var logBuilder = new SslDiagnosticsLogBuilder();
+ logBuilder.AddSslPolicyErrorsMessages(sslErrors);
+ logBuilder.AddRequestMessageLog(requestMessage);
+ logBuilder.AddCertificateLog(certificate);
+ logBuilder.AddChainLog(chain);
+
+ var formattedLog = logBuilder.BuildLog();
- this._trace?.Info(diagInfo);
+ this._trace?.Info($"Diagnostic data for request:{Environment.NewLine}{formattedLog}");
}
}
}
- public static class SslDiagnosticDataProvider
+ internal sealed class SslDiagnosticsLogBuilder
{
+ private readonly StringBuilder _logBuilder = new StringBuilder();
+
/// A predefined list of headers to get from the request
private static readonly string[] _requiredRequestHeaders = new[]
{
@@ -74,122 +80,99 @@ public static class SslDiagnosticDataProvider
};
///
- /// Get diagnostic data about the HTTP request.
+ /// Add diagnostics data about the HTTP request.
/// This method extracts common information about the request itself and the request's headers.
/// To expand list of headers please update .
- /// Diagnostic data as a formatted string
- public static string GetRequestMessageData(HttpRequestMessage requestMessage)
+ public void AddRequestMessageLog(HttpRequestMessage requestMessage)
{
// Getting general information about request
- string requestDiagInfoHeader = "HttpRequest";
- string diagInfo = string.Empty;
-
if (requestMessage is null)
{
- return $"{requestDiagInfoHeader} data is empty";
+ _logBuilder.AppendLine($"HttpRequest data is empty");
+ return;
}
- var requestDiagInfo = new List>();
var requestedUri = requestMessage?.RequestUri.ToString();
var methodType = requestMessage?.Method.ToString();
- requestDiagInfo.Add(new KeyValuePair("Requested URI", requestedUri));
- requestDiagInfo.Add(new KeyValuePair("Request method", methodType));
-
- diagInfo = GetFormattedData(requestDiagInfoHeader, requestDiagInfo);
+ _logBuilder.AppendLine($"[HttpRequest]");
+ _logBuilder.AppendLine($"Requested URI: {requestedUri}");
+ _logBuilder.AppendLine($"Requested method: {methodType}");
// Getting informantion from headers
var requestHeaders = requestMessage?.Headers;
- if (requestHeaders is null)
+ if (requestHeaders is null || !requestHeaders.Any())
{
- return diagInfo;
+ return;
}
- string headersDiagInfoHeader = "HttpRequestHeaders";
-
- var headersDiagInfo = new List>();
+ _logBuilder.AppendLine($"[HttpRequestHeaders]");
foreach (var headerKey in _requiredRequestHeaders)
{
IEnumerable headerValues;
if (requestHeaders.TryGetValues(headerKey, out headerValues))
{
- var headerValue = string.Join(", ", headerValues.ToArray());
- if (headerValue != null)
- {
- headersDiagInfo.Add(new KeyValuePair(headerKey, headerValue.ToString()));
- }
+ _logBuilder.AppendLine($"{headerKey}: {string.Join(", ", headerValues.ToArray())}");
}
}
-
- diagInfo += GetFormattedData(headersDiagInfoHeader, headersDiagInfo);
-
- return diagInfo;
}
///
- /// Get diagnostic data about the certificate.
+ /// Add diagnostics data about the certificate.
/// This method extracts common information about the certificate.
///
- /// Diagnostic data as a formatted string
- public static string GetCertificateData(X509Certificate2 certificate)
+ public void AddCertificateLog(X509Certificate2 certificate)
{
- string diagInfoHeader = "Certificate";
var diagInfo = new List>();
if (certificate is null)
{
- return $"{diagInfoHeader} data is empty";
+ _logBuilder.AppendLine($"Certificate data is empty");
+ return;
}
- diagInfo.Add(new KeyValuePair("Effective date", certificate?.GetEffectiveDateString()));
- diagInfo.Add(new KeyValuePair("Expiration date", certificate?.GetExpirationDateString()));
- diagInfo.Add(new KeyValuePair("Issuer", certificate?.Issuer));
- diagInfo.Add(new KeyValuePair("Subject", certificate?.Subject));
-
- return GetFormattedData(diagInfoHeader, diagInfo);
+ _logBuilder.AppendLine($"[Certificate]");
+ AddCertificateData(certificate);
}
///
- /// Get diagnostic data about the chain.
+ /// Add diagnostics data about the chain.
/// This method extracts common information about the chain.
///
- /// Diagnostic data as a formatted string
- public static string GetChainData(X509Chain chain)
+ public void AddChainLog(X509Chain chain)
{
- string diagInfoHeader = "ChainStatus";
- var diagInfo = new List>();
-
- if (chain is null)
+ if (chain is null || chain.ChainElements is null)
{
- return $"{diagInfoHeader} data is empty";
+ _logBuilder.AppendLine($"ChainElements data is empty");
+ return;
}
- foreach (var status in chain.ChainStatus)
+ _logBuilder.AppendLine("[ChainElements]");
+ foreach (var chainElement in chain.ChainElements)
{
- diagInfo.Add(new KeyValuePair("Status", status.Status.ToString()));
- diagInfo.Add(new KeyValuePair("Status Information", status.StatusInformation));
+ AddCertificateData(chainElement.Certificate);
+ foreach (var status in chainElement.ChainElementStatus)
+ {
+ _logBuilder.AppendLine($"Status: {status.Status}");
+ _logBuilder.AppendLine($"Status Information: {status.StatusInformation}");
+ }
}
-
- return GetFormattedData(diagInfoHeader, diagInfo);
}
///
- /// Get list of SSL policy errors with descriptions.
+ /// Add list of SSL policy errors with descriptions.
/// This method checks SSL policy errors and mapping them to user-friendly descriptions.
/// To update SSL policy errors description please update .
///
- /// Diagnostic data as a formatted string
- public static string ResolveSslPolicyErrorsMessage(SslPolicyErrors sslErrors)
+ public void AddSslPolicyErrorsMessages(SslPolicyErrors sslErrors)
{
- string diagInfoHeader = $"SSL Policy Errors";
- var diagInfo = new List>();
+ _logBuilder.AppendLine($"[SSL Policy Errors]");
if (sslErrors == SslPolicyErrors.None)
{
- diagInfo.Add(new KeyValuePair(sslErrors.ToString(), _sslPolicyErrorsMapping[sslErrors]));
- return GetFormattedData(diagInfoHeader, diagInfo);
+ _logBuilder.AppendLine($"No SSL policy errors");
}
// Since we can get several SSL policy errors we should check all of them
@@ -197,33 +180,31 @@ public static string ResolveSslPolicyErrorsMessage(SslPolicyErrors sslErrors)
{
if ((sslErrors & errorCode) != 0)
{
- string errorValue = errorCode.ToString();
- string errorMessage = string.Empty;
-
- if (!_sslPolicyErrorsMapping.TryGetValue(errorCode, out errorMessage))
+ if (!_sslPolicyErrorsMapping.ContainsKey(errorCode))
{
- errorMessage = "Could not resolve related error message";
+ _logBuilder.AppendLine($"{errorCode.ToString()}: Could not resolve related error message");
+ }
+ else
+ {
+ _logBuilder.AppendLine($"{errorCode.ToString()}: {_sslPolicyErrorsMapping[errorCode]}");
}
-
- diagInfo.Add(new KeyValuePair(errorValue, errorMessage));
}
}
-
- return GetFormattedData(diagInfoHeader, diagInfo);
}
- /// Get diagnostic data as formatted text
- /// Formatted string
- private static string GetFormattedData(string diagInfoHeader, List> diagInfo)
+ public string BuildLog()
{
- string formattedData = $"[{diagInfoHeader}]\n";
+ return _logBuilder.ToString();
+ }
- foreach (var record in diagInfo)
- {
- formattedData += $"{record.Key}: {record.Value}\n";
- }
- return formattedData;
+ private void AddCertificateData(X509Certificate2 certificate)
+ {
+ _logBuilder.AppendLine($"Effective date: {certificate?.GetEffectiveDateString()}");
+ _logBuilder.AppendLine($"Expiration date: {certificate?.GetExpirationDateString()}");
+ _logBuilder.AppendLine($"Issuer: {certificate?.Issuer}");
+ _logBuilder.AppendLine($"Subject: {certificate?.Subject}");
+ _logBuilder.AppendLine($"Thumbprint: {certificate?.Thumbprint}");
}
}
}
diff --git a/src/Test/L0/Util/SslUtilL0.cs b/src/Test/L0/Util/SslUtilL0.cs
new file mode 100644
index 0000000000..2f57ea7e7c
--- /dev/null
+++ b/src/Test/L0/Util/SslUtilL0.cs
@@ -0,0 +1,193 @@
+using Microsoft.VisualStudio.Services.Agent.Util;
+using System;
+using System.Net.Http;
+using System.Net.Security;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using Xunit;
+
+namespace Microsoft.VisualStudio.Services.Agent.Tests.Util
+{
+ public sealed class SslUtilL0
+ {
+ [Fact]
+ public void AddSslPolicyErrorsMesssages_NoErrors_ShouldReturnCorrectLog()
+ {
+ // Arrange
+ var logBuilder = new SslDiagnosticsLogBuilder();
+
+ // Act
+ logBuilder.AddSslPolicyErrorsMessages(SslPolicyErrors.None);
+ var log = logBuilder.BuildLog();
+
+ // Assert
+ Assert.Contains("No SSL policy errors", log);
+ }
+
+ [Fact]
+ public void AddSslPolicyErrorsMesssages_HasErrors_ShouldReturnCorrectLog()
+ {
+ // Arrange
+ var logBuilder = new SslDiagnosticsLogBuilder();
+
+ // Act
+ logBuilder.AddSslPolicyErrorsMessages(SslPolicyErrors.RemoteCertificateNameMismatch);
+ logBuilder.AddSslPolicyErrorsMessages(SslPolicyErrors.RemoteCertificateChainErrors);
+ var log = logBuilder.BuildLog();
+
+ // Assert
+ Assert.Contains("ChainStatus has returned a non empty array", log);
+ Assert.Contains("Certificate name mismatch", log);
+ }
+
+ [Fact]
+ public void AddRequestMessageLog_RequestMessageIsNull_ShouldReturnCorrectLog()
+ {
+ // Arrange
+ HttpRequestMessage requestMessage = null;
+ var logBuilder = new SslDiagnosticsLogBuilder();
+
+ // Act
+ logBuilder.AddRequestMessageLog(requestMessage);
+ var log = logBuilder.BuildLog();
+
+ // Assert
+ Assert.Equal($"HttpRequest data is empty{Environment.NewLine}", log);
+ }
+
+ [Fact]
+ public void AddRequestMessageLog_RequestMessageIsNotNull_ShouldReturnCorrectLog()
+ {
+ // Arrange
+ var requestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost");
+ var logBuilder = new SslDiagnosticsLogBuilder();
+ var log = string.Empty;
+
+ // Act
+ using (requestMessage)
+ {
+ logBuilder.AddRequestMessageLog(requestMessage);
+ log = logBuilder.BuildLog();
+ }
+
+ // Assert
+ Assert.Contains("Requested URI: http://localhost/", log);
+ Assert.Contains("Requested method: GET", log);
+ }
+
+ [Fact]
+ public void AddRequestMessageLog_RequestMessageHasHeaders_ShouldReturnCorrectLog()
+ {
+ // Arrange
+ var requestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost");
+ requestMessage.Headers.Add("X-TFS-Session", "value1");
+ requestMessage.Headers.Add("X-VSS-E2EID", "value2_1");
+ requestMessage.Headers.Add("X-VSS-E2EID", "value2_2");
+ requestMessage.Headers.Add("User-Agent", "value3");
+ requestMessage.Headers.Add("CustomHeader", "CustomValue");
+ var logBuilder = new SslDiagnosticsLogBuilder();
+ var log = string.Empty;
+
+ // Act
+ using (requestMessage)
+ {
+ logBuilder.AddRequestMessageLog(requestMessage);
+ log = logBuilder.BuildLog();
+ }
+
+ // Assert
+ Assert.Contains("Requested URI: http://localhost/", log);
+ Assert.Contains("Requested method: GET", log);
+ Assert.Contains("X-TFS-Session: value1", log);
+ Assert.Contains("X-VSS-E2EID: value2_1, value2_2", log);
+ Assert.Contains("User-Agent: value3", log);
+ Assert.DoesNotContain("CustomHeader", log);
+ }
+
+ [Fact]
+ public void AddCertificateLog_CertificateIsNull_ShouldReturnCorrectLog()
+ {
+ // Arrange
+ var logBuilder = new SslDiagnosticsLogBuilder();
+
+ // Act
+ logBuilder.AddCertificateLog(null);
+ var log = logBuilder.BuildLog();
+
+ // Assert
+ Assert.Equal($"Certificate data is empty{Environment.NewLine}", log);
+ }
+
+ [Fact]
+ public void AddCertificateLog_CertificateIsNotNull_ShouldReturnCorrectLog()
+ {
+ // Arrange
+ var certificate = GenerateTestCertificate();
+ var logBuilder = new SslDiagnosticsLogBuilder();
+ var log = string.Empty;
+
+ // Act
+ using (certificate)
+ {
+ logBuilder.AddCertificateLog(certificate);
+ log = logBuilder.BuildLog();
+ }
+
+ // Assert
+ Assert.Contains("Effective date: ", log);
+ Assert.Contains("Expiration date: ", log);
+ Assert.Contains("Subject: ", log);
+ Assert.Contains("Issuer: ", log);
+ Assert.Contains("Thumbprint: ", log);
+ }
+
+ [Fact]
+ public void AddChainLog_ChainIsNull_ShouldReturnCorrectLog()
+ {
+ // Arrange
+ var logBuilder = new SslDiagnosticsLogBuilder();
+
+ // Act
+ logBuilder.AddChainLog(null);
+ var log = logBuilder.BuildLog();
+
+ // Assert
+ Assert.Equal($"ChainElements data is empty{Environment.NewLine}", log);
+ }
+
+ [Fact]
+ public void AddChainLog_ChainIsNotNull_ShouldReturnCorrectLog()
+ {
+ // Arrange
+ var certificate = GenerateTestCertificate();
+ using var chain = new X509Chain();
+ chain.Build(certificate);
+ var logBuilder = new SslDiagnosticsLogBuilder();
+ var log = string.Empty;
+
+ // Act
+ using (certificate)
+ {
+ logBuilder.AddChainLog(chain);
+ log = logBuilder.BuildLog();
+ }
+
+ // Assert
+ Assert.Contains("[ChainElements]", log);
+ Assert.Contains("Effective date: ", log);
+ Assert.Contains("Expiration date: ", log);
+ Assert.Contains("Subject: ", log);
+ Assert.Contains("Issuer: ", log);
+ Assert.Contains("Thumbprint: ", log);
+ }
+
+ private X509Certificate2 GenerateTestCertificate()
+ {
+ using var ecdsa = ECDsa.Create(); // generate asymmetric key pair
+ var req = new CertificateRequest("CN=TestSubject", ecdsa, HashAlgorithmName.SHA256);
+ var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5));
+
+ return cert;
+ }
+ }
+}