2017-06-04 14:34:37 +02:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Net;
|
|
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
|
using System.Xml.Linq;
|
|
|
|
|
|
using System.Xml.XPath;
|
2018-04-19 23:41:35 +10:00
|
|
|
|
using Umbraco.Core;
|
2017-06-04 14:34:37 +02:00
|
|
|
|
using Umbraco.Core.IO;
|
|
|
|
|
|
using Umbraco.Core.Services;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Umbraco.Web.HealthCheck.Checks.Security
|
|
|
|
|
|
{
|
|
|
|
|
|
public abstract class BaseHttpHeaderCheck : HealthCheck
|
|
|
|
|
|
{
|
2018-04-19 23:41:35 +10:00
|
|
|
|
protected IRuntimeState Runtime { get; }
|
|
|
|
|
|
protected ILocalizedTextService TextService { get; }
|
2017-06-04 14:34:37 +02:00
|
|
|
|
|
|
|
|
|
|
private const string SetHeaderInConfigAction = "setHeaderInConfig";
|
|
|
|
|
|
|
|
|
|
|
|
private readonly string _header;
|
|
|
|
|
|
private readonly string _value;
|
|
|
|
|
|
private readonly string _localizedTextPrefix;
|
|
|
|
|
|
private readonly bool _metaTagOptionAvailable;
|
|
|
|
|
|
|
2018-04-19 23:41:35 +10:00
|
|
|
|
protected BaseHttpHeaderCheck(
|
|
|
|
|
|
IRuntimeState runtime,
|
|
|
|
|
|
ILocalizedTextService textService,
|
|
|
|
|
|
string header, string value, string localizedTextPrefix, bool metaTagOptionAvailable)
|
2017-06-04 14:34:37 +02:00
|
|
|
|
{
|
2018-04-19 23:41:35 +10:00
|
|
|
|
Runtime = runtime;
|
|
|
|
|
|
TextService = textService ?? throw new ArgumentNullException(nameof(textService));
|
2017-06-04 14:34:37 +02:00
|
|
|
|
|
|
|
|
|
|
_header = header;
|
|
|
|
|
|
_value = value;
|
|
|
|
|
|
_localizedTextPrefix = localizedTextPrefix;
|
|
|
|
|
|
_metaTagOptionAvailable = metaTagOptionAvailable;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Get the status for this health check
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
public override IEnumerable<HealthCheckStatus> GetStatus()
|
|
|
|
|
|
{
|
|
|
|
|
|
//return the statuses
|
|
|
|
|
|
return new[] { CheckForHeader() };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Executes the action and returns it's status
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="action"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (action.Alias)
|
|
|
|
|
|
{
|
|
|
|
|
|
case SetHeaderInConfigAction:
|
|
|
|
|
|
return SetHeaderInConfig();
|
|
|
|
|
|
default:
|
|
|
|
|
|
throw new InvalidOperationException("HTTP Header action requested is either not executable or does not exist");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected HealthCheckStatus CheckForHeader()
|
|
|
|
|
|
{
|
|
|
|
|
|
var message = string.Empty;
|
|
|
|
|
|
var success = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Access the site home page and check for the click-jack protection header or meta tag
|
2018-04-19 23:41:35 +10:00
|
|
|
|
var url = Runtime.ApplicationUrl;
|
2018-04-12 17:59:11 +02:00
|
|
|
|
var request = WebRequest.Create(url);
|
2017-06-04 14:34:37 +02:00
|
|
|
|
request.Method = "GET";
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var response = request.GetResponse();
|
|
|
|
|
|
|
|
|
|
|
|
// Check first for header
|
|
|
|
|
|
success = DoHttpHeadersContainHeader(response);
|
|
|
|
|
|
|
|
|
|
|
|
// If not found, and available, check for meta-tag
|
|
|
|
|
|
if (success == false && _metaTagOptionAvailable)
|
|
|
|
|
|
{
|
|
|
|
|
|
success = DoMetaTagsContainKeyForHeader(response);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message = success
|
2018-04-19 23:41:35 +10:00
|
|
|
|
? TextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderFound")
|
|
|
|
|
|
: TextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderNotFound");
|
2017-06-04 14:34:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2018-04-19 23:41:35 +10:00
|
|
|
|
message = TextService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url.ToString(), ex.Message });
|
2017-06-04 14:34:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var actions = new List<HealthCheckAction>();
|
|
|
|
|
|
if (success == false)
|
|
|
|
|
|
{
|
|
|
|
|
|
actions.Add(new HealthCheckAction(SetHeaderInConfigAction, Id)
|
|
|
|
|
|
{
|
2018-04-19 23:41:35 +10:00
|
|
|
|
Name = TextService.Localize("healthcheck/setHeaderInConfig"),
|
|
|
|
|
|
Description = TextService.Localize($"healthcheck/{_localizedTextPrefix}SetHeaderInConfigDescription")
|
2017-06-04 14:34:37 +02:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
new HealthCheckStatus(message)
|
|
|
|
|
|
{
|
|
|
|
|
|
ResultType = success ? StatusResultType.Success : StatusResultType.Error,
|
|
|
|
|
|
Actions = actions
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool DoHttpHeadersContainHeader(WebResponse response)
|
|
|
|
|
|
{
|
2019-10-17 15:06:43 +01:00
|
|
|
|
return response.Headers.AllKeys.Contains(_header, StringComparer.InvariantCultureIgnoreCase);
|
2017-06-04 14:34:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool DoMetaTagsContainKeyForHeader(WebResponse response)
|
|
|
|
|
|
{
|
|
|
|
|
|
using (var stream = response.GetResponseStream())
|
|
|
|
|
|
{
|
|
|
|
|
|
if (stream == null) return false;
|
|
|
|
|
|
using (var reader = new StreamReader(stream))
|
|
|
|
|
|
{
|
|
|
|
|
|
var html = reader.ReadToEnd();
|
|
|
|
|
|
var metaTags = ParseMetaTags(html);
|
|
|
|
|
|
return metaTags.ContainsKey(_header);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static Dictionary<string, string> ParseMetaTags(string html)
|
|
|
|
|
|
{
|
|
|
|
|
|
var regex = new Regex("<meta http-equiv=\"(.+?)\" content=\"(.+?)\"", RegexOptions.IgnoreCase);
|
|
|
|
|
|
|
|
|
|
|
|
return regex.Matches(html)
|
|
|
|
|
|
.Cast<Match>()
|
|
|
|
|
|
.ToDictionary(m => m.Groups[1].Value, m => m.Groups[2].Value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private HealthCheckStatus SetHeaderInConfig()
|
|
|
|
|
|
{
|
|
|
|
|
|
var errorMessage = string.Empty;
|
|
|
|
|
|
var success = SaveHeaderToConfigFile(out errorMessage);
|
|
|
|
|
|
|
|
|
|
|
|
if (success)
|
|
|
|
|
|
{
|
|
|
|
|
|
return
|
2018-04-19 23:41:35 +10:00
|
|
|
|
new HealthCheckStatus(TextService.Localize(string.Format("healthcheck/{0}SetHeaderInConfigSuccess", _localizedTextPrefix)))
|
2017-06-04 14:34:37 +02:00
|
|
|
|
{
|
|
|
|
|
|
ResultType = StatusResultType.Success
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return
|
2018-04-19 23:41:35 +10:00
|
|
|
|
new HealthCheckStatus(TextService.Localize("healthcheck/setHeaderInConfigError", new [] { errorMessage }))
|
2017-06-04 14:34:37 +02:00
|
|
|
|
{
|
|
|
|
|
|
ResultType = StatusResultType.Error
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool SaveHeaderToConfigFile(out string errorMessage)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// There don't look to be any useful classes defined in https://msdn.microsoft.com/en-us/library/system.web.configuration(v=vs.110).aspx
|
|
|
|
|
|
// for working with the customHeaders section, so working with the XML directly.
|
|
|
|
|
|
var configFile = IOHelper.MapPath("~/Web.config");
|
|
|
|
|
|
var doc = XDocument.Load(configFile);
|
|
|
|
|
|
var systemWebServerElement = doc.XPathSelectElement("/configuration/system.webServer");
|
|
|
|
|
|
var httpProtocolElement = systemWebServerElement.Element("httpProtocol");
|
|
|
|
|
|
if (httpProtocolElement == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
httpProtocolElement = new XElement("httpProtocol");
|
|
|
|
|
|
systemWebServerElement.Add(httpProtocolElement);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var customHeadersElement = httpProtocolElement.Element("customHeaders");
|
|
|
|
|
|
if (customHeadersElement == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
customHeadersElement = new XElement("customHeaders");
|
|
|
|
|
|
httpProtocolElement.Add(customHeadersElement);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var removeHeaderElement = customHeadersElement.Elements("remove")
|
2019-10-17 15:06:43 +01:00
|
|
|
|
.SingleOrDefault(x => x.Attribute("name")?.Value.Equals(_value, StringComparison.InvariantCultureIgnoreCase) == true);
|
2017-06-04 14:34:37 +02:00
|
|
|
|
if (removeHeaderElement == null)
|
|
|
|
|
|
{
|
2019-10-17 15:06:43 +01:00
|
|
|
|
customHeadersElement.Add(
|
|
|
|
|
|
new XElement("remove",
|
|
|
|
|
|
new XAttribute("name", _header)));
|
2017-06-04 14:34:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var addHeaderElement = customHeadersElement.Elements("add")
|
2019-10-17 15:06:43 +01:00
|
|
|
|
.SingleOrDefault(x => x.Attribute("name")?.Value.Equals(_header, StringComparison.InvariantCultureIgnoreCase) == true);
|
2017-06-04 14:34:37 +02:00
|
|
|
|
if (addHeaderElement == null)
|
|
|
|
|
|
{
|
2019-10-17 15:06:43 +01:00
|
|
|
|
customHeadersElement.Add(
|
|
|
|
|
|
new XElement("add",
|
|
|
|
|
|
new XAttribute("name", _header),
|
|
|
|
|
|
new XAttribute("value", _value)));
|
2017-06-04 14:34:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
doc.Save(configFile);
|
|
|
|
|
|
|
|
|
|
|
|
errorMessage = string.Empty;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
errorMessage = ex.Message;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|