// 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); } } }