Skip to content

Commit

Permalink
Added global HTTP response headers configuration. Resolves Azure#3788.
Browse files Browse the repository at this point in the history
  • Loading branch information
kashimiz committed Jun 19, 2019
1 parent cac3cb2 commit 55e2894
Show file tree
Hide file tree
Showing 15 changed files with 389 additions and 1 deletion.
40 changes: 40 additions & 0 deletions src/WebJobs.Script.WebHost/Middleware/CustomHeadersMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// 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.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs.Script.Middleware;
using Microsoft.Extensions.Options;

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

public CustomHeadersMiddleware(IOptions<ScriptJobHostOptions> hostOptions)
{
RequestDelegate contextNext = async context =>
{
if (context.Items.Remove(ScriptConstants.CustomHeadersMiddlewareRequestDelegate, out object requestDelegate) && requestDelegate is RequestDelegate next)
{
await next(context);
foreach (var header in hostOptions.Value.CustomHeaders)
{
context.Response.Headers[header.Key] = header.Value;
}
}
};

_invoke = contextNext;
}

public async Task Invoke(HttpContext context, RequestDelegate next)
{
context.Items.Add(ScriptConstants.CustomHeadersMiddlewareRequestDelegate, next);
await _invoke(context);
}
}
}
2 changes: 2 additions & 0 deletions src/WebJobs.Script.WebHost/WebScriptHostBuilderExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection;
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
using Microsoft.Azure.WebJobs.Script.WebHost.Management;
using Microsoft.Azure.WebJobs.Script.WebHost.Middleware;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
Expand Down Expand Up @@ -76,6 +77,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, CustomHeadersMiddleware>();
services.TryAddSingleton<IJobHostHttpMiddleware, HstsConfigurationMiddleware>();
// Make sure the registered IHostIdProvider is used
Expand Down
3 changes: 2 additions & 1 deletion src/WebJobs.Script/Config/HostJsonFileConfigurationSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ public class HostJsonFileConfigurationProvider : ConfigurationProvider
private static readonly string[] WellKnownHostJsonProperties = new[]
{
"version", "functionTimeout", "functions", "http", "watchDirectories", "queues", "serviceBus",
"eventHub", "singleton", "logging", "aggregator", "healthMonitor", "extensionBundle", "managedDependencies"
"eventHub", "singleton", "logging", "aggregator", "healthMonitor", "extensionBundle", "managedDependencies",
"customHeaders"
};

private readonly HostJsonFileConfigurationSource _configurationSource;
Expand Down
6 changes: 6 additions & 0 deletions src/WebJobs.Script/Config/ScriptJobHostOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public ScriptJobHostOptions()
FileLoggingMode = FileLoggingMode.Never;
InstanceId = Guid.NewGuid().ToString();
WatchDirectories = new Collection<string>();
CustomHeaders = new Dictionary<string, string>();
}

/// <summary>
Expand Down Expand Up @@ -122,5 +123,10 @@ public ImmutableArray<string> RootScriptDirectorySnapshot
/// locally or via CLI.
/// </summary>
public bool IsSelfHost { get; set; }

/// <summary>
/// Gets or sets the list of headers to add to every HTTP response.
/// </summary>
public IDictionary<string, string> CustomHeaders { get; set; }
}
}
1 change: 1 addition & 0 deletions src/WebJobs.Script/ScriptConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public static class ScriptConstants
public const string AzureFunctionsDuplicateHttpHeadersKey = "MS_AzureFunctionsDuplicateHttpHeaders";
public const string JobHostMiddlewarePipelineRequestDelegate = "MS_JobHostMiddlewarePipelineRequestDelegate";
public const string HstsMiddlewareRequestDelegate = "MS_HstsMiddlewareRequestDelegate";
public const string CustomHeadersMiddlewareRequestDelegate = "MS_CustomHeadersMiddlewareRequestDelegate";

public const string LogPropertyPrimaryHostKey = "MS_PrimaryHost";
public const string LogPropertySourceKey = "MS_Source";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Script.Rpc;
using Xunit;

namespace Microsoft.Azure.WebJobs.Script.Tests.Middleware
{
public class CustomHeadersMiddlewareCSharpEndToEndTests :
CustomHeadersMiddlewareEndToEndTestsBase<CustomHeadersMiddlewareCSharpEndToEndTests.TestFixture>
{
public CustomHeadersMiddlewareCSharpEndToEndTests(TestFixture fixture) : base(fixture)
{
}

[Fact]
public Task CustomHeadersMiddlewareRootUrl()
{
return CustomHeadersMiddlewareRootUrlTest();
}

[Fact]
public Task CustomHeadersMiddlewareAdminUrl()
{
return CustomHeadersMiddlewareAdminUrlTest();
}

[Fact]
public Task CustomHeadersMiddlewareHttpTriggerUrl()
{
return CustomHeadersMiddlewareHttpTriggerUrlTest();
}

[Fact]
public Task CustomHeadersMiddlewareExtensionWebhookUrl()
{
return CustomHeadersMiddlewareExtensionWebhookUrlTest();
}

public class TestFixture : CustomHeadersMiddlewareTestFixture
{
private const string ScriptRoot = @"TestScripts\CustomHeadersMiddleware\CSharp";

public TestFixture() : base(ScriptRoot, "csharp", LanguageWorkerConstants.DotNetLanguageWorkerName)
{
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// 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.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Script.Models;
using Xunit;

namespace Microsoft.Azure.WebJobs.Script.Tests.Middleware
{
public abstract class CustomHeadersMiddlewareEndToEndTestsBase<TTestFixture> :
EndToEndTestsBase<TTestFixture> where TTestFixture : CustomHeadersMiddlewareTestFixture, new()
{
public CustomHeadersMiddlewareEndToEndTestsBase(TTestFixture fixture) : base(fixture)
{
}

protected async Task CustomHeadersMiddlewareRootUrlTest()
{
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, string.Empty);

HttpResponseMessage response = await Fixture.Host.HttpClient.SendAsync(request);
response.EnsureSuccessStatusCode();

Assert.Equal(HttpStatusCode.OK, response.StatusCode);

IEnumerable<string> values;
Assert.True(response.Headers.TryGetValues("X-Content-Type-Options", out values));
Assert.Equal("nosniff", values.FirstOrDefault());
}

protected async Task CustomHeadersMiddlewareAdminUrlTest()
{
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "admin/host/ping");

HttpResponseMessage response = await Fixture.Host.HttpClient.SendAsync(request);
response.EnsureSuccessStatusCode();

Assert.Equal(HttpStatusCode.OK, response.StatusCode);

IEnumerable<string> values;
Assert.True(response.Headers.TryGetValues("X-Content-Type-Options", out values));
Assert.Equal("nosniff", values.FirstOrDefault());
}

protected async Task CustomHeadersMiddlewareHttpTriggerUrlTest()
{
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "api/HttpTrigger");

HttpResponseMessage response = await Fixture.Host.HttpClient.SendAsync(request);
response.EnsureSuccessStatusCode();

Assert.Equal(HttpStatusCode.OK, response.StatusCode);

IEnumerable<string> values;
Assert.True(response.Headers.TryGetValues("X-Content-Type-Options", out values));
Assert.Equal("nosniff", values.FirstOrDefault());
}

protected async Task CustomHeadersMiddlewareExtensionWebhookUrlTest()
{
var secrets = await Fixture.Host.SecretManager.GetHostSecretsAsync();
var url = $"/runtime/webhooks/durableTask/instances?taskHub=MiddlewareTestHub&code={secrets.MasterKey}";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url);

HttpResponseMessage response = await Fixture.Host.HttpClient.SendAsync(request);
response.EnsureSuccessStatusCode();

Assert.Equal(HttpStatusCode.OK, response.StatusCode);

IEnumerable<string> values;
Assert.True(response.Headers.TryGetValues("X-Content-Type-Options", out values));
Assert.Equal("nosniff", values.FirstOrDefault());
}
}

public abstract class CustomHeadersMiddlewareTestFixture: EndToEndTestFixture
{
protected override ExtensionPackageReference[] GetExtensionsToInstall()
{
return new ExtensionPackageReference[]
{
new ExtensionPackageReference
{
Id = "Microsoft.Azure.WebJobs.Extensions.DurableTask",
Version = "1.8.2"
}
};
}

protected CustomHeadersMiddlewareTestFixture(string rootPath, string testId, string language) :
base(rootPath, testId, language)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Script.Rpc;
using Xunit;

namespace Microsoft.Azure.WebJobs.Script.Tests.Middleware
{
public class CustomHeadersMiddlewareNodeEndToEndTests :
CustomHeadersMiddlewareEndToEndTestsBase<CustomHeadersMiddlewareNodeEndToEndTests.TestFixture>
{
public CustomHeadersMiddlewareNodeEndToEndTests(TestFixture fixture) : base(fixture)
{
}

[Fact]
public Task CustomHeadersMiddlewareRootUrl()
{
return CustomHeadersMiddlewareRootUrlTest();
}

[Fact]
public Task CustomHeadersMiddlewareAdminUrl()
{
return CustomHeadersMiddlewareAdminUrlTest();
}

[Fact]
public Task CustomHeadersMiddlewareHttpTriggerUrl()
{
return CustomHeadersMiddlewareHttpTriggerUrlTest();
}

[Fact]
public Task CustomHeadersMiddlewareExtensionWebhookUrl()
{
return CustomHeadersMiddlewareExtensionWebhookUrlTest();
}

public class TestFixture : CustomHeadersMiddlewareTestFixture
{
private const string ScriptRoot = @"TestScripts\CustomHeadersMiddleware\Node";

public TestFixture() : base(ScriptRoot, "node", LanguageWorkerConstants.NodeLanguageWorkerName)
{
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"bindings": [
{
"type": "httpTrigger",
"name": "req",
"direction": "in",
"methods": [ "get" ],
"authLevel": "anonymous"
},
{
"type": "http",
"name": "$return",
"direction": "out"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;

public static IActionResult Run(HttpRequest req, TraceWriter log)
{
log.Info("C# HTTP trigger function processed a request.");

return new OkObjectResult("Success");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "2.0",
"customHeaders": {
"X-Content-Type-Options": "nosniff"
},
"extensions": {
"durableTask": {
"hubName": "CustomHeadersMiddlewareCSharpTestHub"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"bindings": [
{
"type": "httpTrigger",
"name": "req",
"direction": "in",
"methods": [ "get" ],
"authLevel": "anonymous"
},
{
"type": "http",
"name": "$return",
"direction": "out"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = function (context, req) {
context.log('Node.js HTTP trigger function processed a request.');

const res = {
status: 200,
};

context.done(null, res);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "2.0",
"customHeaders": {
"X-Content-Type-Options": "nosniff"
},
"extensions": {
"durableTask": {
"hubName": "CustomHeadersMiddlewareNodeTestHub"
}
}
}

0 comments on commit 55e2894

Please sign in to comment.