Add ForwardedHeaders to CreateDefaultBuilder #4135 (#10273)

This commit is contained in:
Chris Ross 2019-05-17 13:07:11 -07:00 committed by GitHub
parent 8b99354419
commit b670246e3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 283 additions and 67 deletions

View File

@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<UserSecretsId>aspnetcore-MetaPackagesSampleApp-20170406180413</UserSecretsId>
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -16,19 +16,16 @@ namespace SampleApp
{
public static void Main(string[] args)
{
HelloWorld();
CustomUrl();
CustomRouter();
CustomApplicationBuilder();
StartupClass(args);
HostBuilderWithWebHost(args);
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
private static void HelloWorld()
{
using (WebHost.Start(context => context.Response.WriteAsync("Hello, World!")))

View File

@ -1,9 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace SampleApp
@ -15,12 +21,50 @@ namespace SampleApp
}
public void Configure(IApplicationBuilder app)
public void Configure(IApplicationBuilder app, IConfiguration config)
{
app.Run(async (context) =>
{
await context.Response.WriteAsync($"Hello from {nameof(Startup)}!");
await context.Response.WriteAsync($"Hello from {context.Request.GetDisplayUrl()}\r\n");
await context.Response.WriteAsync("\r\n");
await context.Response.WriteAsync("Headers:\r\n");
foreach (var header in context.Request.Headers)
{
await context.Response.WriteAsync($"{header.Key}: {header.Value}\r\n");
}
await context.Response.WriteAsync("\r\n");
await context.Response.WriteAsync("Connection:\r\n");
await context.Response.WriteAsync("RemoteIp: " + context.Connection.RemoteIpAddress + "\r\n");
await context.Response.WriteAsync("RemotePort: " + context.Connection.RemotePort + "\r\n");
await context.Response.WriteAsync("LocalIp: " + context.Connection.LocalIpAddress + "\r\n");
await context.Response.WriteAsync("LocalPort: " + context.Connection.LocalPort + "\r\n");
await context.Response.WriteAsync("ClientCert: " + context.Connection.ClientCertificate + "\r\n");
await context.Response.WriteAsync("\r\n");
await context.Response.WriteAsync("Environment Variables:\r\n");
var vars = Environment.GetEnvironmentVariables();
foreach (var key in vars.Keys.Cast<string>().OrderBy(key => key, StringComparer.OrdinalIgnoreCase))
{
var value = vars[key];
await context.Response.WriteAsync($"{key}: {value}\r\n");
}
await context.Response.WriteAsync("\r\n");
await context.Response.WriteAsync("Config:\r\n");
await ShowConfig(context.Response, config);
await context.Response.WriteAsync("\r\n");
});
}
private static async Task ShowConfig(HttpResponse response, IConfiguration config)
{
foreach (var pair in config.GetChildren())
{
await response.WriteAsync($"{pair.Path}: {pair.Value}\r\n");
await ShowConfig(response, pair);
}
}
}
}

View File

@ -0,0 +1,53 @@
{
"AllowedHosts": "example.com;localhost",
"Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://localhost:5005"
},
"Https": {
"Url": "https://localhost:5006"
}
// To enable HTTPS using a certificate file, set the path to a .pfx file in
// the "Path" property below and configure the password in user secrets.
// The "Password" property should be set in user secrets.
//"HttpsInlineCertFile": {
// "Url": "http://localhost:5005"
// "Certificate": {
// "Path": "<path to .pfx file>",
// "Password: "<cert password>"
// }
//},
//"HttpsInlineCertStore": {
// "Url": "http://localhost:5005"
// "Certificate": {
// "Subject": "",
// "Store": "",
// "Location": "",
// "AllowInvalid": "" // Set to "true" to allow invalid certificates (e.g. expired)
// }
//},
// This uses the cert defined under Certificates/Default or the development cert.
//"HttpsDefaultCert": {
// "Url": "http://localhost:5005"
//}
}
},
"Certificates": {
//"Default": {
// "Path": "<file>",
// "Password": "<password>"
//},
// From cert store:
//"Default": {
// "Subject": "",
// "Store": "",
// "Location": "",
// "AllowInvalid": "" // Set to "true" to allow invalid certificates (e.g. expired certificates)
//}
}
}

View File

@ -1,50 +1,2 @@
{
"AllowedHosts": "example.com;localhost",
"Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://localhost:5005"
}
// To enable HTTPS using a certificate file, set the path to a .pfx file in
// the "Path" property below and configure the password in user secrets.
// The "Password" property should be set in user secrets.
//"HttpsInlineCertFile": {
// "Url": "http://localhost:5005"
// "Certificate": {
// "Path": "<path to .pfx file>",
// "Password: "<cert password>"
// }
//},
//"HttpsInlineCertStore": {
// "Url": "http://localhost:5005"
// "Certificate": {
// "Subject": "",
// "Store": "",
// "Location": "",
// "AllowInvalid": "" // Set to "true" to allow invalid certificates (e.g. expired)
// }
//},
// This uses the cert defined under Certificates/Default or the development cert.
//"HttpsDefaultCert": {
// "Url": "http://localhost:5005"
//}
}
},
"Certificates": {
//"Default": {
// "Path": "<file>",
// "Password": "<password>"
//},
// From cert store:
//"Default": {
// "Subject": "",
// "Store": "",
// "Location": "",
// "AllowInvalid": "" // Set to "true" to allow invalid certificates (e.g. expired certificates)
//}
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
namespace Microsoft.AspNetCore
{
internal class ForwardedHeadersStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
app.UseForwardedHeaders();
next(app);
};
}
}
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore;
@ -15,6 +15,8 @@ namespace Microsoft.Extensions.Hosting
/// <remarks>
/// The following defaults are applied to the <see cref="IWebHostBuilder"/>:
/// use Kestrel as the web server and configure it using the application's configuration providers,
/// adds the HostFiltering middleware,
/// adds the ForwardedHeaders middleware if ASPNETCORE_FORWARDEDHEADERS_ENABLED=true,
/// and enable IIS integration.
/// </remarks>
/// <param name="builder">The <see cref="IHostBuilder" /> instance to configure</param>

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.HostFiltering;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -124,6 +125,8 @@ namespace Microsoft.AspNetCore
/// load <see cref="IConfiguration"/> from User Secrets when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development' using the entry assembly,
/// load <see cref="IConfiguration"/> from environment variables,
/// configure the <see cref="ILoggerFactory"/> to log to the console and debug output,
/// adds the HostFiltering middleware,
/// adds the ForwardedHeaders middleware if ASPNETCORE_FORWARDEDHEADERS_ENABLED=true,
/// and enable IIS integration.
/// </remarks>
/// <returns>The initialized <see cref="IWebHostBuilder"/>.</returns>
@ -142,6 +145,8 @@ namespace Microsoft.AspNetCore
/// load <see cref="IConfiguration"/> from environment variables,
/// load <see cref="IConfiguration"/> from supplied command line args,
/// configure the <see cref="ILoggerFactory"/> to log to the console and debug output,
/// adds the HostFiltering middleware,
/// adds the ForwardedHeaders middleware if ASPNETCORE_FORWARDEDHEADERS_ENABLED=true,
/// and enable IIS integration.
/// </remarks>
/// <param name="args">The command line args.</param>
@ -224,6 +229,20 @@ namespace Microsoft.AspNetCore
services.AddTransient<IStartupFilter, HostFilteringStartupFilter>();
if (string.Equals("true", hostingContext.Configuration["ForwardedHeaders_Enabled"], StringComparison.OrdinalIgnoreCase))
{
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// Only loopback proxies are allowed by default. Clear that restriction because forwarders are
// being enabled by explicit configuration.
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
services.AddTransient<IStartupFilter, ForwardedHeadersStartupFilter>();
}
services.AddRouting();
})
.UseIIS()

View File

@ -6,6 +6,7 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.TestHost" />
</ItemGroup>
</Project>

View File

@ -8,9 +8,11 @@ using System.Diagnostics.Tracing;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.HostFiltering;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -57,6 +59,35 @@ namespace Microsoft.AspNetCore.Tests
Assert.Contains("NewHost", options.AllowedHosts);
}
[Fact]
public async Task WebHostConfiguration_EnablesForwardedHeadersFromConfig()
{
using var host = WebHost.CreateDefaultBuilder()
.ConfigureAppConfiguration(configBuilder =>
{
configBuilder.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("FORWARDEDHEADERS_ENABLED", "true" ),
});
})
.UseTestServer()
.Configure(app =>
{
Assert.True(app.Properties.ContainsKey("ForwardedHeadersAdded"), "Forwarded Headers");
app.Run(context =>
{
Assert.Equal("https", context.Request.Scheme);
return Task.CompletedTask;
});
}).Build();
await host.StartAsync();
var client = host.GetTestClient();
client.DefaultRequestHeaders.Add("x-forwarded-proto", "https");
var result = await client.GetAsync("http://localhost/");
result.EnsureSuccessStatusCode();
}
[Fact]
public void CreateDefaultBuilder_RegistersRouting()
{

View File

@ -48,6 +48,8 @@ namespace Microsoft.AspNetCore.TestHost
{
public static Microsoft.AspNetCore.Hosting.IWebHostBuilder ConfigureTestContainer<TContainer>(this Microsoft.AspNetCore.Hosting.IWebHostBuilder webHostBuilder, System.Action<TContainer> servicesConfiguration) { throw null; }
public static Microsoft.AspNetCore.Hosting.IWebHostBuilder ConfigureTestServices(this Microsoft.AspNetCore.Hosting.IWebHostBuilder webHostBuilder, System.Action<Microsoft.Extensions.DependencyInjection.IServiceCollection> servicesConfiguration) { throw null; }
public static System.Net.Http.HttpClient GetTestClient(this Microsoft.AspNetCore.Hosting.IWebHost host) { throw null; }
public static Microsoft.AspNetCore.TestHost.TestServer GetTestServer(this Microsoft.AspNetCore.Hosting.IWebHost host) { throw null; }
public static Microsoft.AspNetCore.Hosting.IWebHostBuilder UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder builder, string solutionRelativePath, string solutionName = "*.sln") { throw null; }
public static Microsoft.AspNetCore.Hosting.IWebHostBuilder UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder builder, string solutionRelativePath, string applicationBasePath, string solutionName = "*.sln") { throw null; }
public static Microsoft.AspNetCore.Hosting.IWebHostBuilder UseTestServer(this Microsoft.AspNetCore.Hosting.IWebHostBuilder builder) { throw null; }

View File

@ -4,6 +4,7 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Hosting.Server;
@ -21,6 +22,26 @@ namespace Microsoft.AspNetCore.TestHost
});
}
/// <summary>
/// Retrieves the TestServer from the host services.
/// </summary>
/// <param name="host"></param>
/// <returns></returns>
public static TestServer GetTestServer(this IWebHost host)
{
return (TestServer)host.Services.GetRequiredService<IServer>();
}
/// <summary>
/// Retrieves the test client from the TestServer in the host services.
/// </summary>
/// <param name="host"></param>
/// <returns></returns>
public static HttpClient GetTestClient(this IWebHost host)
{
return host.GetTestServer().CreateClient();
}
public static IWebHostBuilder ConfigureTestServices(this IWebHostBuilder webHostBuilder, Action<IServiceCollection> servicesConfiguration)
{
if (webHostBuilder == null)

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Builder
{
public static class ForwardedHeadersExtensions
{
private const string ForwardedHeadersAdded = "ForwardedHeadersAdded";
/// <summary>
/// Forwards proxied headers onto current request
/// </summary>
@ -21,7 +23,15 @@ namespace Microsoft.AspNetCore.Builder
throw new ArgumentNullException(nameof(builder));
}
return builder.UseMiddleware<ForwardedHeadersMiddleware>();
// Don't add more than one instance of this middleware to the pipeline using the options from the DI container.
// Doing so could cause a request to be processed multiple times and the ForwardLimit to be exceeded.
if (!builder.Properties.ContainsKey(ForwardedHeadersAdded))
{
builder.Properties[ForwardedHeadersAdded] = true;
return builder.UseMiddleware<ForwardedHeadersMiddleware>();
}
return builder;
}
/// <summary>

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Net.Http.Headers;
using Xunit;
@ -865,5 +866,66 @@ namespace Microsoft.AspNetCore.HttpOverrides
Assert.Equal(expectedRemoteIp, context.Connection.RemoteIpAddress.ToString());
}
[Theory]
[InlineData(1, "httpa, httpb, httpc", "httpc", "httpa,httpb")]
[InlineData(2, "httpa, httpb, httpc", "httpb", "httpa")]
public async Task ForwardersWithDIOptionsRunsOnce(int limit, string header, string expectedScheme, string remainingHeader)
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedProto;
options.KnownProxies.Clear();
options.KnownNetworks.Clear();
options.ForwardLimit = limit;
});
})
.Configure(app =>
{
app.UseForwardedHeaders();
app.UseForwardedHeaders();
});
var server = new TestServer(builder);
var context = await server.SendAsync(c =>
{
c.Request.Headers["X-Forwarded-Proto"] = header;
});
Assert.Equal(expectedScheme, context.Request.Scheme);
Assert.Equal(remainingHeader, context.Request.Headers["X-Forwarded-Proto"].ToString());
}
[Theory]
[InlineData(1, "httpa, httpb, httpc", "httpb", "httpa")]
[InlineData(2, "httpa, httpb, httpc", "httpa", "")]
public async Task ForwardersWithDirectOptionsRunsTwice(int limit, string header, string expectedScheme, string remainingHeader)
{
var builder = new WebHostBuilder()
.Configure(app =>
{
var options = new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedProto,
ForwardLimit = limit,
};
options.KnownProxies.Clear();
options.KnownNetworks.Clear();
app.UseForwardedHeaders(options);
app.UseForwardedHeaders(options);
});
var server = new TestServer(builder);
var context = await server.SendAsync(c =>
{
c.Request.Headers["X-Forwarded-Proto"] = header;
});
Assert.Equal(expectedScheme, context.Request.Scheme);
Assert.Equal(remainingHeader, context.Request.Headers["X-Forwarded-Proto"].ToString());
}
}
}