<commit_before>﻿// Copyright (c) <NAME>, 2016. All rights reserved.
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.

namespace MartinCostello.Website.Middleware
{
    using System;
    using System.Diagnostics;
    using System.Globalization;
    using System.Text;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Http;
    using Microsoft.Extensions.Configuration;
    using Options;

    /// <summary>
    /// A class representing middleware for adding custom HTTP response headers. This class cannot be inherited.
    /// </summary>
    public sealed class CustomHttpHeadersMiddleware
    {
        /// <summary>
        /// The delegate for the next part of the pipeline. This field is read-only.
        /// </summary>
        private readonly RequestDelegate _next;

        /// <summary>
        /// The current <c>Content-Security-Policy</c> HTTP response header value. This field is read-only.
        /// </summary>
        private readonly string _contentSecurityPolicy;

        /// <summary>
        /// The current <c>Public-Key-Pins</c> HTTP response header value. This field is read-only.
        /// </summary>
        private readonly string _publicKeyPins;

        /// <summary>
        /// The name of the current hosting environment. This field is read-only.
        /// </summary>
        private readonly string _environmentName;

        /// <summary>
        /// The current datacenter name. This field is read-only.
        /// </summary>
        private readonly string _datacenter;

        /// <summary>
        /// Whether the current hosting environment is production. This field is read-only.
        /// </summary>
        private readonly bool _isProduction;

        /// <summary>
        /// Initializes a new instance of the <see cref="CustomHttpHeadersMiddleware"/> class.
        /// </summary>
        /// <param name="next">The delegate for the next part of the pipeline.</param>
        /// <param name="environment">The current hosting environment.</param>
        /// <param name="config">The current configuration.</param>
        /// <param name="options">The current site configuration options.</param>
        public CustomHttpHeadersMiddleware(
            RequestDelegate next,
            IHostingEnvironment environment,
            IConfiguration config,
            SiteOptions options)
        {
            _next = next;
            _isProduction = environment.IsProduction();
            _environmentName = _isProduction ? null : environment.EnvironmentName;
            _datacenter = config["Azure:Datacenter"] ?? "Local";
            _contentSecurityPolicy = BuildContentSecurityPolicy(_isProduction, options);
            _publicKeyPins = BuildPublicKeyPins(options);
        }

        /// <summary>
        /// Invokes the middleware asynchronously.
        /// </summary>
        /// <param name="context">The current HTTP context.</param>
        /// <returns>
        /// A <see cref="Task"/> representing the actions performed by the middleware.
        /// </returns>
        public async Task Invoke(HttpContext context)
        {
            var stopwatch = Stopwatch.StartNew();

            context.Response.OnStarting(() =>
                {
                    context.Response.Headers.Remove("Server");
                    context.Response.Headers.Remove("X-Powered-By");

                    context.Response.Headers.Add("Content-Security-Policy", _contentSecurityPolicy);
                    context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
                    context.Response.Headers.Add("X-Download-Options", "noopen");
                    context.Response.Headers.Add("X-Frame-Options", "DENY");
                    context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");

                    if (context.Request.IsHttps)
                    {
                        context.Response.Headers.Add("Strict-Transport-Security", "max-age=31536000");

                        if (!string.IsNullOrWhiteSpace(_publicKeyPins))
                        {
                            context.Response.Headers.Add("Public-Key-Pins-Report-Only", _publicKeyPins);
                        }
                    }

                    context.Response.Headers.Add("X-Datacenter", _datacenter);

#if DEBUG
                    context.Response.Headers.Add("X-Debug", "true");
#endif

                    if (_environmentName != null)
                    {
                        context.Response.Headers.Add("X-Environment", _environmentName);
                    }

                    context.Response.Headers.Add("X-Instance", Environment.MachineName);
                    context.Response.Headers.Add("X-Request-Id", context.TraceIdentifier);
                    context.Response.Headers.Add("X-Revision", GitMetadata.Commit);

                    stopwatch.Stop();

                    string duration = stopwatch.Elapsed.TotalMilliseconds.ToString(
                        "0.00ms",
                        CultureInfo.InvariantCulture);

                    context.Response.Headers.Add("X-Request-Duration", duration);

                    return Task.CompletedTask;
                });

            await _next(context);
        }

        /// <summary>
        /// Builds the Content Security Policy to use for the website.
        /// </summary>
        /// <param name="isProduction">Whether the current environment is production.</param>
        /// <param name="options">The current site configuration options.</param>
        /// <returns>
        /// A <see cref="string"/> containing the Content Security Policy to use.
        /// </returns>
        private static string BuildContentSecurityPolicy(bool isProduction, SiteOptions options)
        {
            var basePolicy = $@"
default-src 'self' maxcdn.bootstrapcdn.com;
script-src 'self' ajax.googleapis.com api.github.com cdnjs.cloudflare.com buttons.github.io maxcdn.bootstrapcdn.com platform.linkedin.com platform.twitter.com www.google-analytics.com 'unsafe-inline';
style-src 'self' ajax.googleapis.com buttons.github.io fonts.googleapis.com maxcdn.bootstrapcdn.com 'unsafe-inline';
img-src 'self' stackoverflow.com static.licdn.com stats.g.doubleclick.net syndication.twitter.com www.google-analytics.com www.linkedin.com data:;
font-src 'self' ajax.googleapis.com fonts.googleapis.com fonts.gstatic.com maxcdn.bootstrapcdn.com;
connect-src 'self' {GetApiOriginForContentSecurityPolicy(options)};
media-src 'none';
object-src ajax.cdnjs.com;
child-src buttons.github.io platform.linkedin.com platform.twitter.com;
frame-ancestors 'none';
form-action 'self';
block-all-mixed-content;
reflected-xss block;
base-uri 'self';
manifest-src 'self';";

            var builder = new StringBuilder(basePolicy.Replace(Environment.NewLine, string.Empty));<commit_msg>Fix CSP errors<commit_after>﻿// Copyright (c) <NAME>, 2016. All rights reserved.
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.

namespace MartinCostello.Website.Middleware
{
    using System;
    using System.Diagnostics;
    using System.Globalization;
    using System.Text;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Http;
    using Microsoft.Extensions.Configuration;
    using Options;

    /// <summary>
    /// A class representing middleware for adding custom HTTP response headers. This class cannot be inherited.
    /// </summary>
    public sealed class CustomHttpHeadersMiddleware
    {
        /// <summary>
        /// The delegate for the next part of the pipeline. This field is read-only.
        /// </summary>
        private readonly RequestDelegate _next;

        /// <summary>
        /// The current <c>Content-Security-Policy</c> HTTP response header value. This field is read-only.
        /// </summary>
        private readonly string _contentSecurityPolicy;

        /// <summary>
        /// The current <c>Public-Key-Pins</c> HTTP response header value. This field is read-only.
        /// </summary>
        private readonly string _publicKeyPins;

        /// <summary>
        /// The name of the current hosting environment. This field is read-only.
        /// </summary>
        private readonly string _environmentName;

        /// <summary>
        /// The current datacenter name. This field is read-only.
        /// </summary>
        private readonly string _datacenter;

        /// <summary>
        /// Whether the current hosting environment is production. This field is read-only.
        /// </summary>
        private readonly bool _isProduction;

        /// <summary>
        /// Initializes a new instance of the <see cref="CustomHttpHeadersMiddleware"/> class.
        /// </summary>
        /// <param name="next">The delegate for the next part of the pipeline.</param>
        /// <param name="environment">The current hosting environment.</param>
        /// <param name="config">The current configuration.</param>
        /// <param name="options">The current site configuration options.</param>
        public CustomHttpHeadersMiddleware(
            RequestDelegate next,
            IHostingEnvironment environment,
            IConfiguration config,
            SiteOptions options)
        {
            _next = next;
            _isProduction = environment.IsProduction();
            _environmentName = _isProduction ? null : environment.EnvironmentName;
            _datacenter = config["Azure:Datacenter"] ?? "Local";
            _contentSecurityPolicy = BuildContentSecurityPolicy(_isProduction, options);
            _publicKeyPins = BuildPublicKeyPins(options);
        }

        /// <summary>
        /// Invokes the middleware asynchronously.
        /// </summary>
        /// <param name="context">The current HTTP context.</param>
        /// <returns>
        /// A <see cref="Task"/> representing the actions performed by the middleware.
        /// </returns>
        public async Task Invoke(HttpContext context)
        {
            var stopwatch = Stopwatch.StartNew();

            context.Response.OnStarting(() =>
                {
                    context.Response.Headers.Remove("Server");
                    context.Response.Headers.Remove("X-Powered-By");

                    context.Response.Headers.Add("Content-Security-Policy", _contentSecurityPolicy);
                    context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
                    context.Response.Headers.Add("X-Download-Options", "noopen");
                    context.Response.Headers.Add("X-Frame-Options", "DENY");
                    context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");

                    if (context.Request.IsHttps)
                    {
                        context.Response.Headers.Add("Strict-Transport-Security", "max-age=31536000");

                        if (!string.IsNullOrWhiteSpace(_publicKeyPins))
                        {
                            context.Response.Headers.Add("Public-Key-Pins-Report-Only", _publicKeyPins);
                        }
                    }

                    context.Response.Headers.Add("X-Datacenter", _datacenter);

#if DEBUG
                    context.Response.Headers.Add("X-Debug", "true");
#endif

                    if (_environmentName != null)
                    {
                        context.Response.Headers.Add("X-Environment", _environmentName);
                    }

                    context.Response.Headers.Add("X-Instance", Environment.MachineName);
                    context.Response.Headers.Add("X-Request-Id", context.TraceIdentifier);
                    context.Response.Headers.Add("X-Revision", GitMetadata.Commit);

                    stopwatch.Stop();

                    string duration = stopwatch.Elapsed.TotalMilliseconds.ToString(
                        "0.00ms",
                        CultureInfo.InvariantCulture);

                    context.Response.Headers.Add("X-Request-Duration", duration);

                    return Task.CompletedTask;
                });

            await _next(context);
        }

        /// <summary>
        /// Builds the Content Security Policy to use for the website.
        /// </summary>
        /// <param name="isProduction">Whether the current environment is production.</param>
        /// <param name="options">The current site configuration options.</param>
        /// <returns>
        /// A <see cref="string"/> containing the Content Security Policy to use.
        /// </returns>
        private static string BuildContentSecurityPolicy(bool isProduction, SiteOptions options)
        {
            var basePolicy = $@"
default-src 'self' buttons.github.io maxcdn.bootstrapcdn.com platform.linkedin.com platform.twitter.com data:;
script-src 'self' ajax.googleapis.com api.github.com cdnjs.cloudflare.com buttons.github.io maxcdn.bootstrapcdn.com platform.linkedin.com platform.twitter.com www.google-analytics.com 'unsafe-inline';
style-src 'self' ajax.googleapis.com buttons.github.io fonts.googleapis.com maxcdn.bootstrapcdn.com 'unsafe-inline';
img-src 'self' stackoverflow.com static.licdn.com stats.g.doubleclick.net syndication.twitter.com www.google-analytics.com www.linkedin.com data:;
font-src 'self' ajax.googleapis.com fonts.googleapis.com fonts.gstatic.com maxcdn.bootstrapcdn.com;
connect-src 'self' {GetApiOriginForContentSecurityPolicy(options)};
media-src 'none';
object-src ajax.cdnjs.com;
child-src 'self' buttons.github.io platform.linkedin.com platform.twitter.com;
frame-ancestors 'none';
form-action 'self';
block-all-mixed-content;
reflected-xss block;
base-uri 'self';
manifest-src 'self';";

            var builder = new StringBuilder(basePolicy.Replace(Environment.NewLine, string.Empty));
{'fraction_non_alphanumeric': 0.11352743400815508, 'fraction_numerical': 0.0025752915086916088, 'mean_word_length': 2.110814419225634, 'pattern_counts': {'":': 0, '<': 95, '<?xml version=': 0, '>': 97, 'https://': 0, 'lorem ipsum': 0, 'www.': 6, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '9236', 'n_tokens_mistral': 3559, 'n_tokens_neox': 3376, 'n_words': 852}