Skip to content

Commit af35757

Browse files
authored
Merge pull request #11051 from umbraco/v9/bugfix/httpscheck-concurrency
Fix concurrency and certificate days to expiry issues in HttpsCheck
2 parents 7fdb6d2 + 79316d6 commit af35757

File tree

1 file changed

+42
-40
lines changed

1 file changed

+42
-40
lines changed

src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Umbraco.
1+
// Copyright (c) Umbraco.
22
// See LICENSE for more details.
33

44
using System;
@@ -17,7 +17,7 @@
1717
namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
1818
{
1919
/// <summary>
20-
/// Health checks for the recommended production setup regarding https.
20+
/// Health checks for the recommended production setup regarding HTTPS.
2121
/// </summary>
2222
[HealthCheck(
2323
"EB66BB3B-1BCD-4314-9531-9DA2C1D6D9A7",
@@ -26,17 +26,26 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
2626
Group = "Security")]
2727
public class HttpsCheck : HealthCheck
2828
{
29+
private const int NumberOfDaysForExpiryWarning = 14;
30+
private const string HttpPropertyKeyCertificateDaysToExpiry = "CertificateDaysToExpiry";
31+
2932
private readonly ILocalizedTextService _textService;
3033
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
3134
private readonly IHostingEnvironment _hostingEnvironment;
3235

3336
private static HttpClient s_httpClient;
34-
private static HttpClientHandler s_httpClientHandler;
35-
private static int s_certificateDaysToExpiry;
37+
38+
private static HttpClient HttpClient => s_httpClient ??= new HttpClient(new HttpClientHandler()
39+
{
40+
ServerCertificateCustomValidationCallback = ServerCertificateCustomValidation
41+
});
3642

3743
/// <summary>
38-
/// Initializes a new instance of the <see cref="HttpsCheck"/> class.
44+
/// Initializes a new instance of the <see cref="HttpsCheck" /> class.
3945
/// </summary>
46+
/// <param name="textService">The text service.</param>
47+
/// <param name="globalSettings">The global settings.</param>
48+
/// <param name="hostingEnvironment">The hosting environment.</param>
4049
public HttpsCheck(
4150
ILocalizedTextService textService,
4251
IOptionsMonitor<GlobalSettings> globalSettings,
@@ -47,33 +56,22 @@ public HttpsCheck(
4756
_hostingEnvironment = hostingEnvironment;
4857
}
4958

50-
private static HttpClient HttpClient => s_httpClient ??= new HttpClient(HttpClientHandler);
51-
52-
private static HttpClientHandler HttpClientHandler => s_httpClientHandler ??= new HttpClientHandler()
53-
{
54-
ServerCertificateCustomValidationCallback = ServerCertificateCustomValidation
55-
};
56-
57-
/// <summary>
58-
/// Get the status for this health check
59-
/// </summary>
59+
/// <inheritdoc />
6060
public override async Task<IEnumerable<HealthCheckStatus>> GetStatus() =>
6161
await Task.WhenAll(
6262
CheckIfCurrentSchemeIsHttps(),
6363
CheckHttpsConfigurationSetting(),
6464
CheckForValidCertificate());
6565

66-
/// <summary>
67-
/// Executes the action and returns it's status
68-
/// </summary>
66+
/// <inheritdoc />
6967
public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
7068
=> throw new InvalidOperationException("HttpsCheck action requested is either not executable or does not exist");
7169

7270
private static bool ServerCertificateCustomValidation(HttpRequestMessage requestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslErrors)
7371
{
74-
if (!(certificate is null) && s_certificateDaysToExpiry == default)
72+
if (certificate is not null)
7573
{
76-
s_certificateDaysToExpiry = (int)Math.Floor((certificate.NotAfter - DateTime.Now).TotalDays);
74+
requestMessage.Properties[HttpPropertyKeyCertificateDaysToExpiry] = (int)Math.Floor((certificate.NotAfter - DateTime.Now).TotalDays);
7775
}
7876

7977
return sslErrors == SslPolicyErrors.None;
@@ -84,30 +82,37 @@ private async Task<HealthCheckStatus> CheckForValidCertificate()
8482
string message;
8583
StatusResultType result;
8684

87-
// Attempt to access the site over HTTPS to see if it HTTPS is supported
88-
// and a valid certificate has been configured
89-
var url = _hostingEnvironment.ApplicationMainUrl.ToString().Replace("http:", "https:");
85+
// Attempt to access the site over HTTPS to see if it HTTPS is supported and a valid certificate has been configured
86+
var urlBuilder = new UriBuilder(_hostingEnvironment.ApplicationMainUrl)
87+
{
88+
Scheme = Uri.UriSchemeHttps
89+
};
90+
var url = urlBuilder.Uri;
9091

9192
var request = new HttpRequestMessage(HttpMethod.Head, url);
9293

9394
try
9495
{
9596
using HttpResponseMessage response = await HttpClient.SendAsync(request);
97+
9698
if (response.StatusCode == HttpStatusCode.OK)
9799
{
98-
// Got a valid response, check now for if certificate expiring within 14 days
99-
// Hat-tip: https://stackoverflow.com/a/15343898/489433
100-
const int numberOfDaysForExpiryWarning = 14;
100+
// Got a valid response, check now if the certificate is expiring within the specified amount of days
101+
int daysToExpiry = 0;
102+
if (request.Properties.TryGetValue(HttpPropertyKeyCertificateDaysToExpiry, out var certificateDaysToExpiry))
103+
{
104+
daysToExpiry = (int)certificateDaysToExpiry;
105+
}
101106

102-
if (s_certificateDaysToExpiry <= 0)
107+
if (daysToExpiry <= 0)
103108
{
104109
result = StatusResultType.Error;
105110
message = _textService.Localize("healthcheck","httpsCheckExpiredCertificate");
106111
}
107-
else if (s_certificateDaysToExpiry < numberOfDaysForExpiryWarning)
112+
else if (daysToExpiry < NumberOfDaysForExpiryWarning)
108113
{
109114
result = StatusResultType.Warning;
110-
message = _textService.Localize("healthcheck","httpsCheckExpiringCertificate", new[] { s_certificateDaysToExpiry.ToString() });
115+
message = _textService.Localize("healthcheck","httpsCheckExpiringCertificate", new[] { daysToExpiry.ToString() });
111116
}
112117
else
113118
{
@@ -118,21 +123,20 @@ private async Task<HealthCheckStatus> CheckForValidCertificate()
118123
else
119124
{
120125
result = StatusResultType.Error;
121-
message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, response.ReasonPhrase });
126+
message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url.AbsoluteUri, response.ReasonPhrase });
122127
}
123128
}
124129
catch (Exception ex)
125130
{
126-
var exception = ex as WebException;
127-
if (exception != null)
131+
if (ex is WebException exception)
128132
{
129133
message = exception.Status == WebExceptionStatus.TrustFailure
130-
? _textService.Localize("healthcheck","httpsCheckInvalidCertificate", new[] { exception.Message })
131-
: _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, exception.Message });
134+
? _textService.Localize("healthcheck", "httpsCheckInvalidCertificate", new[] { exception.Message })
135+
: _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, exception.Message });
132136
}
133137
else
134138
{
135-
message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, ex.Message });
139+
message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, ex.Message });
136140
}
137141

138142
result = StatusResultType.Error;
@@ -150,7 +154,7 @@ private async Task<HealthCheckStatus> CheckForValidCertificate()
150154
private Task<HealthCheckStatus> CheckIfCurrentSchemeIsHttps()
151155
{
152156
Uri uri = _hostingEnvironment.ApplicationMainUrl;
153-
var success = uri.Scheme == "https";
157+
var success = uri.Scheme == Uri.UriSchemeHttps;
154158

155159
return Task.FromResult(new HealthCheckStatus(_textService.Localize("healthcheck","httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" }))
156160
{
@@ -166,16 +170,14 @@ private Task<HealthCheckStatus> CheckHttpsConfigurationSetting()
166170

167171
string resultMessage;
168172
StatusResultType resultType;
169-
if (uri.Scheme != "https")
173+
if (uri.Scheme != Uri.UriSchemeHttps)
170174
{
171175
resultMessage = _textService.Localize("healthcheck","httpsCheckConfigurationRectifyNotPossible");
172176
resultType = StatusResultType.Info;
173177
}
174178
else
175179
{
176-
resultMessage = _textService.Localize(
177-
"healthcheck","httpsCheckConfigurationCheckResult",
178-
new[] { httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not" });
180+
resultMessage = _textService.Localize("healthcheck","httpsCheckConfigurationCheckResult", new[] { httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not" });
179181
resultType = httpsSettingEnabled ? StatusResultType.Success : StatusResultType.Error;
180182
}
181183

0 commit comments

Comments
 (0)