Skip to content

Commit

Permalink
[pack] Using Job Host scoped middleware for hsts configuration (#4506)
Browse files Browse the repository at this point in the history
  • Loading branch information
soninaren committed Jun 5, 2019
1 parent 1a20328 commit 0dcb30d
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 0 deletions.
12 changes: 12 additions & 0 deletions src/WebJobs.Script.WebHost/Configuration/HostHstsOptions.cs
@@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using Microsoft.AspNetCore.HttpsPolicy;

namespace Microsoft.Azure.WebJobs.Script.WebHost.Configuration
{
public class HostHstsOptions : HstsOptions
{
public bool IsEnabled { get; set; }
}
}
27 changes: 27 additions & 0 deletions src/WebJobs.Script.WebHost/Configuration/HostHstsOptionsSetup.cs
@@ -0,0 +1,27 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using Microsoft.Azure.WebJobs.Script.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;

namespace Microsoft.Azure.WebJobs.Script.WebHost.Configuration
{
internal class HostHstsOptionsSetup : IConfigureOptions<HostHstsOptions>
{
private readonly IConfiguration _configuration;

public HostHstsOptionsSetup(IConfiguration configuration)
{
_configuration = configuration;
}

public void Configure(HostHstsOptions options)
{
IConfigurationSection jobHostSection = _configuration.GetSection(ConfigurationSectionNames.JobHost);
var hstsSection = jobHostSection.GetSection(ConfigurationSectionNames.Hsts);
hstsSection.Bind(options);
}
}
}
@@ -0,0 +1,45 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Azure.WebJobs.Script.WebHost.Configuration;
using Microsoft.Extensions.Options;

namespace Microsoft.Azure.WebJobs.Script.Middleware
{
public class HstsConfigurationMiddleware : IJobHostHttpMiddleware
{
private RequestDelegate _invoke;

public HstsConfigurationMiddleware(IOptions<HostHstsOptions> hostHstsOptions)
{
RequestDelegate contextNext = async context =>
{
if (context.Items.Remove(ScriptConstants.HstsMiddlewareRequestDelegate, out object requestDelegate) && requestDelegate is RequestDelegate next)
{
await next(context);
}
};

if (hostHstsOptions.Value.IsEnabled)
{
var hstsMiddleware = new HstsMiddleware(contextNext, hostHstsOptions);
_invoke = hstsMiddleware.Invoke;
}
else
{
_invoke = contextNext;
}
}

public async Task Invoke(HttpContext context, RequestDelegate next)
{
context.Items.Add(ScriptConstants.HstsMiddlewareRequestDelegate, next);
await _invoke(context);
}
}
}
2 changes: 2 additions & 0 deletions src/WebJobs.Script.WebHost/WebScriptHostBuilderExtension.cs
Expand Up @@ -33,6 +33,7 @@ public static class WebScriptHostBuilderExtension
// register default configuration
// must happen before the script host is added below
services.ConfigureOptions<HttpOptionsSetup>();
services.ConfigureOptions<HostHstsOptionsSetup>();
})
.AddScriptHost(webHostOptions, configLoggerFactory, webJobsBuilder =>
{
Expand Down Expand Up @@ -75,6 +76,7 @@ public static class WebScriptHostBuilderExtension
services.TryAddSingleton<IScriptWebHookProvider>(p => p.GetService<DefaultScriptWebHookProvider>());
services.TryAddSingleton<IWebHookProvider>(p => p.GetService<DefaultScriptWebHookProvider>());
services.TryAddSingleton<IJobHostMiddlewarePipeline, DefaultMiddlewarePipeline>();
services.TryAddSingleton<IJobHostHttpMiddleware, HstsConfigurationMiddleware>();
// Make sure the registered IHostIdProvider is used
IHostIdProvider provider = rootServiceProvider.GetService<IHostIdProvider>();
Expand Down
2 changes: 2 additions & 0 deletions src/WebJobs.Script/Config/ConfigurationSectionNames.cs
Expand Up @@ -13,5 +13,7 @@ public static class ConfigurationSectionNames
public const string HostIdPath = WebHost + ":hostid";
public const string ExtensionBundle = "extensionBundle";
public const string ManagedDependency = "managedDependency";
public const string Http = "http";
public const string Hsts = Http + ":hsts";
}
}
1 change: 1 addition & 0 deletions src/WebJobs.Script/ScriptConstants.cs
Expand Up @@ -20,6 +20,7 @@ public static class ScriptConstants
public const string AzureFunctionsProxyResult = "MS_AzureFunctionsProxyResult";
public const string AzureFunctionsDuplicateHttpHeadersKey = "MS_AzureFunctionsDuplicateHttpHeaders";
public const string JobHostMiddlewarePipelineRequestDelegate = "MS_JobHostMiddlewarePipelineRequestDelegate";
public const string HstsMiddlewareRequestDelegate = "MS_HstsMiddlewareRequestDelegate";

public const string LogPropertyPrimaryHostKey = "MS_PrimaryHost";
public const string LogPropertySourceKey = "MS_Source";
Expand Down
112 changes: 112 additions & 0 deletions test/WebJobs.Script.Tests/Configuration/HostHstsOptionsSetupTests.cs
@@ -0,0 +1,112 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Azure.WebJobs.Script.Config;
using Microsoft.Azure.WebJobs.Script.Configuration;
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
using Microsoft.Azure.WebJobs.Script.WebHost.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.WebJobs.Script.Tests;
using Moq;
using Xunit;
using static Microsoft.Azure.WebJobs.Script.EnvironmentSettingNames;

namespace Microsoft.Azure.WebJobs.Script.Tests.Configuration
{
public class HostHstsOptionsSetupTests
{
private readonly TestEnvironment _environment = new TestEnvironment();
private readonly TestLoggerProvider _loggerProvider = new TestLoggerProvider();
private readonly string _hostJsonFile;
private readonly string _rootPath;
private readonly ScriptApplicationHostOptions _options;

public HostHstsOptionsSetupTests()
{
_rootPath = Path.Combine(Environment.CurrentDirectory, "ScriptHostTests");
Environment.SetEnvironmentVariable(AzureWebJobsScriptRoot, _rootPath);

if (!Directory.Exists(_rootPath))
{
Directory.CreateDirectory(_rootPath);
}

_options = new ScriptApplicationHostOptions
{
ScriptPath = _rootPath
};

_hostJsonFile = Path.Combine(_rootPath, "host.json");
if (File.Exists(_hostJsonFile))
{
File.Delete(_hostJsonFile);
}
}

[Theory]
[InlineData(@"{
'version': '2.0',
}")]
[InlineData(@"{
'version': '2.0',
'http' : {
'hsts' : {
'isEnabled' : true
}
}
}")]
public void MissingOrValidHstsConfig_DoesNotThrowException(string hostJsonContent)
{
File.WriteAllText(_hostJsonFile, hostJsonContent);
var configuration = BuildHostJsonConfiguration();

HostHstsOptionsSetup setup = new HostHstsOptionsSetup(configuration);
HostHstsOptions options = new HostHstsOptions();
var ex = Record.Exception(() => setup.Configure(options));
Assert.Null(ex);
}

[Fact]
public void ValidHstsConfig_BindsToOptions()
{
string hostJsonContent = @"{
'version': '2.0',
'http': {
'hsts': {
'isEnabled': true,
'maxAge': '10'
}
}
}";
File.WriteAllText(_hostJsonFile, hostJsonContent);
var configuration = BuildHostJsonConfiguration();

HostHstsOptionsSetup setup = new HostHstsOptionsSetup(configuration);
HostHstsOptions options = new HostHstsOptions();
setup.Configure(options);
Assert.Equal(options.MaxAge, new TimeSpan(10, 0, 0, 0));
}

private IConfiguration BuildHostJsonConfiguration(IEnvironment environment = null)
{
environment = environment ?? new TestEnvironment();

var loggerFactory = new LoggerFactory();
loggerFactory.AddProvider(_loggerProvider);

var configSource = new HostJsonFileConfigurationSource(_options, environment, loggerFactory);

var configurationBuilder = new ConfigurationBuilder()
.Add(configSource);

return configurationBuilder.Build();
}
}
}
@@ -0,0 +1,112 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs.Script.Config;
using Microsoft.Azure.WebJobs.Script.Middleware;
using Microsoft.Azure.WebJobs.Script.WebHost.Configuration;
using Microsoft.Extensions.Options;
using Xunit;

namespace Microsoft.Azure.WebJobs.Script.Tests.Middleware
{
public class HstsConfigurationMiddlewareTests
{
[Fact]
public async Task Invoke_hstsEnabled_AddsResponseHeader()
{
var hstsOptions = new OptionsWrapper<HostHstsOptions>(new HostHstsOptions() { IsEnabled = true });

bool nextInvoked = false;
RequestDelegate next = (ctxt) =>
{
nextInvoked = true;
ctxt.Response.StatusCode = (int)HttpStatusCode.Accepted;
return Task.CompletedTask;
};

var middleware = new HstsConfigurationMiddleware(hstsOptions);

var httpContext = new DefaultHttpContext();
httpContext.Request.IsHttps = true;
await middleware.Invoke(httpContext, next);
Assert.True(nextInvoked);
Assert.Equal(httpContext.Response.Headers["Strict-Transport-Security"].ToString(), "max-age=2592000");
}

[Fact]
public async Task Invoke_hstsDisabled_DoesNotAddResponseHeader()
{
var hstsOptions = new OptionsWrapper<HostHstsOptions>(new HostHstsOptions() { IsEnabled = false });

bool nextInvoked = false;
RequestDelegate next = (ctxt) =>
{
nextInvoked = true;
ctxt.Response.StatusCode = (int)HttpStatusCode.Accepted;
return Task.CompletedTask;
};

var middleware = new HstsConfigurationMiddleware(hstsOptions);

var httpContext = new DefaultHttpContext();
httpContext.Request.IsHttps = true;
await middleware.Invoke(httpContext, next);
Assert.True(nextInvoked);
Assert.Equal(httpContext.Response.Headers.Count, 0);
}

[Fact]
public async Task Invoke_hstsEnabled_AddsResponseHeaderWithCorrectValue()
{
bool nextInvoked = false;
RequestDelegate next = (ctxt) =>
{
nextInvoked = true;
ctxt.Response.StatusCode = (int)HttpStatusCode.Accepted;
return Task.CompletedTask;
};

var options = new HostHstsOptions()
{
IsEnabled = true,
MaxAge = new TimeSpan(10, 0, 0, 0)
};
var hstsOptions = new OptionsWrapper<HostHstsOptions>(options);

var middleware = new HstsConfigurationMiddleware(hstsOptions);

var httpContext = new DefaultHttpContext();
httpContext.Request.IsHttps = true;

await middleware.Invoke(httpContext, next);
Assert.True(nextInvoked);
Assert.Equal(httpContext.Response.Headers["Strict-Transport-Security"].ToString(), "max-age=864000");
}

[Fact]
public async Task Invoke_hstsDisabledByDefault()
{
var hstsOptions = new OptionsWrapper<HostHstsOptions>(new HostHstsOptions());

bool nextInvoked = false;
RequestDelegate next = (ctxt) =>
{
nextInvoked = true;
ctxt.Response.StatusCode = (int)HttpStatusCode.Accepted;
return Task.CompletedTask;
};

var middleware = new HstsConfigurationMiddleware(hstsOptions);

var httpContext = new DefaultHttpContext();
httpContext.Request.IsHttps = true;
await middleware.Invoke(httpContext, next);
Assert.True(nextInvoked);
Assert.Equal(httpContext.Response.Headers.Count, 0);
}
}
}

0 comments on commit 0dcb30d

Please sign in to comment.