// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Umbraco.Core.Services;
using Umbraco.Web;
namespace Umbraco.Core.HealthChecks.Checks.Security
{
///
/// Provides a base class for health checks of http header values.
///
public abstract class BaseHttpHeaderCheck : HealthCheck
{
private readonly string _header;
private readonly string _value;
private readonly string _localizedTextPrefix;
private readonly bool _metaTagOptionAvailable;
private readonly IRequestAccessor _requestAccessor;
private static HttpClient s_httpClient;
///
/// Initializes a new instance of the class.
///
protected BaseHttpHeaderCheck(
IRequestAccessor requestAccessor,
ILocalizedTextService textService,
string header,
string value,
string localizedTextPrefix,
bool metaTagOptionAvailable)
{
LocalizedTextService = textService ?? throw new ArgumentNullException(nameof(textService));
_requestAccessor = requestAccessor;
_header = header;
_value = value;
_localizedTextPrefix = localizedTextPrefix;
_metaTagOptionAvailable = metaTagOptionAvailable;
}
private static HttpClient HttpClient => s_httpClient ??= new HttpClient();
///
/// Gets the localized text service.
///
protected ILocalizedTextService LocalizedTextService { get; }
///
/// Gets a link to an external read more page.
///
protected abstract string ReadMoreLink { get; }
///
/// Get the status for this health check
///
public override async Task> GetStatus() =>
await Task.WhenAll(CheckForHeader());
///
/// Executes the action and returns it's status
///
public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
=> throw new InvalidOperationException("HTTP Header action requested is either not executable or does not exist");
///
/// The actual health check method.
///
protected async Task CheckForHeader()
{
string message;
var success = false;
// Access the site home page and check for the click-jack protection header or meta tag
Uri url = _requestAccessor.GetApplicationUrl();
try
{
using HttpResponseMessage response = await HttpClient.GetAsync(url);
// Check first for header
success = HasMatchingHeader(response.Headers.Select(x => x.Key));
// If not found, and available, check for meta-tag
if (success == false && _metaTagOptionAvailable)
{
success = await DoMetaTagsContainKeyForHeader(response);
}
message = success
? LocalizedTextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderFound")
: LocalizedTextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderNotFound");
}
catch (Exception ex)
{
message = LocalizedTextService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url.ToString(), ex.Message });
}
return
new HealthCheckStatus(message)
{
ResultType = success ? StatusResultType.Success : StatusResultType.Error,
ReadMoreLink = success ? null : ReadMoreLink
};
}
private bool HasMatchingHeader(IEnumerable headerKeys)
=> headerKeys.Contains(_header, StringComparer.InvariantCultureIgnoreCase);
private async Task DoMetaTagsContainKeyForHeader(HttpResponseMessage response)
{
using (Stream stream = await response.Content.ReadAsStreamAsync())
{
if (stream == null)
{
return false;
}
using (var reader = new StreamReader(stream))
{
var html = reader.ReadToEnd();
Dictionary metaTags = ParseMetaTags(html);
return HasMatchingHeader(metaTags.Keys);
}
}
}
private static Dictionary ParseMetaTags(string html)
{
var regex = new Regex("()
.ToDictionary(m => m.Groups[1].Value, m => m.Groups[2].Value);
}
}
}