Skip to content

Commit

Permalink
Healthcheck checks all hosts if multiple added with AddHost (#1713)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelmairegger authored Mar 14, 2023
1 parent 6a44aac commit 60d25ab
Show file tree
Hide file tree
Showing 19 changed files with 261 additions and 122 deletions.
12 changes: 12 additions & 0 deletions src/HealthCheckResultTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,15 @@ internal static class HealthCheckResultTask
/// </summary>
public static readonly Task<HealthCheckResult> Healthy = Task.FromResult(HealthCheckResult.Healthy());
}

internal static class StringListExtensions
{
public static HealthCheckResult GetHealthState(this List<string>? instance, HealthCheckContext context)
{
if (instance is null || instance.Count == 0)
{
return HealthCheckResult.Healthy();
}
return new HealthCheckResult(context.Registration.FailureStatus, description: string.Join("; ", instance));
}
}
9 changes: 7 additions & 2 deletions src/HealthChecks.Network/DnsResolveHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
{
try
{
List<string>? errorList = null;
foreach (var item in _options.ConfigureHosts.Values)
{
#if NET6_0_OR_GREATER
Expand All @@ -32,12 +33,16 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
{
if (item.Resolutions == null || !item.Resolutions.Contains(ipAddress.ToString()))
{
return new HealthCheckResult(context.Registration.FailureStatus, description: $"Ip Address {ipAddress} was not resolved from host {item.Host}");
(errorList ??= new()).Add($"Ip Address {ipAddress} was not resolved from host {item.Host}");
if (!_options.CheckAllHosts)
{
break;
}
}
}
}

return HealthCheckResult.Healthy();
return errorList.GetHealthState(context);
}
catch (Exception ex)
{
Expand Down
8 changes: 8 additions & 0 deletions src/HealthChecks.Network/DnsResolveOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,12 @@ internal void AddHost(string host, DnsRegistration registration)
{
ConfigureHosts.Add(host, registration);
}

public DnsResolveOptions WithCheckAllHosts()
{
CheckAllHosts = true;
return this;
}

public bool CheckAllHosts { get; set; }
}
11 changes: 9 additions & 2 deletions src/HealthChecks.Network/FtpHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,24 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
{
try
{
List<string>? errorList = null;
foreach (var (host, createFile, credentials) in _options.Hosts.Values)
{
var ftpRequest = CreateFtpWebRequest(host, createFile, credentials);

using var ftpResponse = (FtpWebResponse)await ftpRequest.GetResponseAsync().WithCancellationTokenAsync(cancellationToken).ConfigureAwait(false);

if (ftpResponse.StatusCode != FtpStatusCode.PathnameCreated && ftpResponse.StatusCode != FtpStatusCode.ClosingData)
return new HealthCheckResult(context.Registration.FailureStatus, description: $"Error connecting to ftp host {host} with exit code {ftpResponse.StatusCode}");
{
(errorList ??= new()).Add($"Error connecting to ftp host {host} with exit code {ftpResponse.StatusCode}");
if (!_options.CheckAllHosts)
{
break;
}
}
}

return HealthCheckResult.Healthy();
return errorList.GetHealthState(context);
}
catch (Exception ex)
{
Expand Down
8 changes: 8 additions & 0 deletions src/HealthChecks.Network/FtpHealthCheckOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,12 @@ public FtpHealthCheckOptions AddHost(string host, bool createFile = false, Netwo

return this;
}

public FtpHealthCheckOptions WithCheckAllHosts()
{
CheckAllHosts = true;
return this;
}

public bool CheckAllHosts { get; set; }
}
11 changes: 9 additions & 2 deletions src/HealthChecks.Network/PingHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,23 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context

try
{
List<string>? errorList = null;
foreach (var (host, timeout) in configuredHosts)
{
using var ping = new Ping();

var pingReply = await ping.SendPingAsync(host, timeout).ConfigureAwait(false);
if (pingReply.Status != IPStatus.Success)
return new HealthCheckResult(context.Registration.FailureStatus, description: $"Ping check for host {host} is failed with status reply:{pingReply.Status}");
{
(errorList ??= new()).Add($"Ping check for host {host} is failed with status reply:{pingReply.Status}");
if (!_options.CheckAllHosts)
{
break;
}
}
}

return HealthCheckResult.Healthy();
return errorList.GetHealthState(context);
}
catch (Exception ex)
{
Expand Down
8 changes: 8 additions & 0 deletions src/HealthChecks.Network/PingHealthCheckOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@ public class PingHealthCheckOptions
{
internal Dictionary<string, (string Host, int TimeOut)> ConfiguredHosts { get; } = new Dictionary<string, (string, int)>();

public bool CheckAllHosts { get; set; }

public PingHealthCheckOptions AddHost(string host, int timeout)
{
ConfiguredHosts.Add(host, (host, timeout));
return this;
}

public PingHealthCheckOptions WithCheckAllHosts()
{
CheckAllHosts = true;
return this;
}
}
9 changes: 7 additions & 2 deletions src/HealthChecks.Network/SftpHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, Canc
{
try
{
List<string>? errorList = null;
foreach (var item in _options.ConfiguredHosts.Values)
{
var connectionInfo = new ConnectionInfo(item.Host, item.Port, item.UserName, item.AuthenticationMethods.ToArray());
Expand All @@ -42,11 +43,15 @@ public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, Canc
}
else
{
return Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus, description: $"Connection with sftp host {item.Host}:{item.Port} failed."));
(errorList ??= new()).Add($"Connection with sftp host {item.Host}:{item.Port} failed.");
if (!_options.CheckAllHosts)
{
break;
}
}
}

return HealthCheckResultTask.Healthy;
return Task.FromResult(errorList.GetHealthState(context));
}
catch (Exception ex)
{
Expand Down
8 changes: 8 additions & 0 deletions src/HealthChecks.Network/SftpHealthCheckOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,12 @@ public SftpHealthCheckOptions AddHost(SftpConfiguration sftpConfiguration)
ConfiguredHosts.Add(sftpConfiguration.Host, sftpConfiguration);
return this;
}

public SftpHealthCheckOptions WithCheckAllHosts()
{
CheckAllHosts = true;
return this;
}

public bool CheckAllHosts { get; set; }
}
167 changes: 93 additions & 74 deletions src/HealthChecks.Network/SslHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -1,74 +1,93 @@
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
#if !NET5_0_OR_GREATER
using HealthChecks.Network.Extensions;
#endif
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace HealthChecks.Network;

public class SslHealthCheck : IHealthCheck
{
private readonly SslHealthCheckOptions _options;

public SslHealthCheck(SslHealthCheckOptions options)
{
_options = Guard.ThrowIfNull(options);
}

/// <inheritdoc />
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
foreach (var (host, port, checkLeftDays) in _options.ConfiguredHosts)
{
using var tcpClient = new TcpClient(_options.AddressFamily);
#if NET5_0_OR_GREATER
await tcpClient.ConnectAsync(host, port, cancellationToken).ConfigureAwait(false);
#else
await tcpClient.ConnectAsync(host, port).WithCancellationTokenAsync(cancellationToken).ConfigureAwait(false);
#endif
if (!tcpClient.Connected)
return new HealthCheckResult(context.Registration.FailureStatus, description: $"Connection to host {host}:{port} failed");

var certificate = await GetSslCertificateAsync(tcpClient, host).ConfigureAwait(false);

if (certificate is null || !certificate.Verify())
return new HealthCheckResult(context.Registration.FailureStatus, description: $"Ssl certificate not present or not valid for {host}:{port}");

if (certificate.NotAfter.Subtract(DateTime.Now).TotalDays <= checkLeftDays)
return new HealthCheckResult(context.Registration.FailureStatus, description: $"Ssl certificate for {host}:{port} is about to expire in {checkLeftDays} days");

return HealthCheckResult.Healthy();
}

return HealthCheckResult.Healthy();
}
catch (Exception ex)
{
return new HealthCheckResult(context.Registration.FailureStatus, exception: ex);
}
}

private async Task<X509Certificate2?> GetSslCertificateAsync(TcpClient client, string host)
{
var ssl = new SslStream(client.GetStream(), false, new RemoteCertificateValidationCallback((sender, cert, ca, sslPolicyErrors) => sslPolicyErrors == SslPolicyErrors.None), null);

try
{
await ssl.AuthenticateAsClientAsync(host).ConfigureAwait(false);
var cert = ssl.RemoteCertificate;
return cert == null ? null : new X509Certificate2(cert);
}
catch (Exception)
{
return null;
}
finally
{
ssl.Close();
}
}
}
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
#if !NET5_0_OR_GREATER
using HealthChecks.Network.Extensions;
#endif
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace HealthChecks.Network;

public class SslHealthCheck : IHealthCheck
{
private readonly SslHealthCheckOptions _options;

public SslHealthCheck(SslHealthCheckOptions options)
{
_options = Guard.ThrowIfNull(options);
}

/// <inheritdoc />
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
List<string>? errorList = null;
foreach (var (host, port, checkLeftDays) in _options.ConfiguredHosts)
{
using var tcpClient = new TcpClient(_options.AddressFamily);
#if NET5_0_OR_GREATER
await tcpClient.ConnectAsync(host, port, cancellationToken).ConfigureAwait(false);
#else
await tcpClient.ConnectAsync(host, port).WithCancellationTokenAsync(cancellationToken).ConfigureAwait(false);
#endif
if (!tcpClient.Connected)
{
(errorList ??= new()).Add($"Connection to host {host}:{port} failed");
if (!_options.CheckAllHosts)
{
break;
}
continue;
}

var certificate = await GetSslCertificateAsync(tcpClient, host).ConfigureAwait(false);

if (certificate is null || !certificate.Verify())
{
(errorList ??= new()).Add($"Ssl certificate not present or not valid for {host}:{port}");
if (!_options.CheckAllHosts)
{
break;
}
continue;
}

if (certificate.NotAfter.Subtract(DateTime.Now).TotalDays <= checkLeftDays)
{
(errorList ??= new()).Add($"Ssl certificate for {host}:{port} is about to expire in {checkLeftDays} days");
if (!_options.CheckAllHosts)
{
break;
}
}
}

return errorList.GetHealthState(context);
}
catch (Exception ex)
{
return new HealthCheckResult(context.Registration.FailureStatus, exception: ex);
}
}

private async Task<X509Certificate2?> GetSslCertificateAsync(TcpClient client, string host)
{
var ssl = new SslStream(client.GetStream(), false, new RemoteCertificateValidationCallback((sender, cert, ca, sslPolicyErrors) => sslPolicyErrors == SslPolicyErrors.None), null);

try
{
await ssl.AuthenticateAsClientAsync(host).ConfigureAwait(false);
var cert = ssl.RemoteCertificate;
return cert == null ? null : new X509Certificate2(cert);
}
catch (Exception)
{
return null;
}
finally
{
ssl.Close();
}
}
}
Loading

0 comments on commit 60d25ab

Please sign in to comment.