diff --git a/src/DefaultBuilder/samples/SampleApp/DefaultBuilder.SampleApp.csproj b/src/DefaultBuilder/samples/SampleApp/DefaultBuilder.SampleApp.csproj index 2009bfeb8043..4d24f50730d4 100644 --- a/src/DefaultBuilder/samples/SampleApp/DefaultBuilder.SampleApp.csproj +++ b/src/DefaultBuilder/samples/SampleApp/DefaultBuilder.SampleApp.csproj @@ -1,8 +1,9 @@ - + netcoreapp3.0 aspnetcore-MetaPackagesSampleApp-20170406180413 + OutOfProcess diff --git a/src/DefaultBuilder/samples/SampleApp/Program.cs b/src/DefaultBuilder/samples/SampleApp/Program.cs index 1d95226e6cde..1094fbcdc214 100644 --- a/src/DefaultBuilder/samples/SampleApp/Program.cs +++ b/src/DefaultBuilder/samples/SampleApp/Program.cs @@ -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 @@ public class Program { 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(); + }); + private static void HelloWorld() { using (WebHost.Start(context => context.Response.WriteAsync("Hello, World!"))) diff --git a/src/DefaultBuilder/samples/SampleApp/Startup.cs b/src/DefaultBuilder/samples/SampleApp/Startup.cs index d5d74c5523a9..3f68a6050c98 100644 --- a/src/DefaultBuilder/samples/SampleApp/Startup.cs +++ b/src/DefaultBuilder/samples/SampleApp/Startup.cs @@ -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 @@ public void ConfigureServices(IServiceCollection services) } - 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().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); + } + } } } diff --git a/src/DefaultBuilder/samples/SampleApp/appsettings.Development.json b/src/DefaultBuilder/samples/SampleApp/appsettings.Development.json new file mode 100644 index 000000000000..545eea7d2ec1 --- /dev/null +++ b/src/DefaultBuilder/samples/SampleApp/appsettings.Development.json @@ -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": "", + // "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": "", + // "Password": "" + //}, + + // From cert store: + //"Default": { + // "Subject": "", + // "Store": "", + // "Location": "", + // "AllowInvalid": "" // Set to "true" to allow invalid certificates (e.g. expired certificates) + //} + } +} diff --git a/src/DefaultBuilder/samples/SampleApp/appsettings.json b/src/DefaultBuilder/samples/SampleApp/appsettings.json index 6a1c35ebcf18..2c63c0851048 100644 --- a/src/DefaultBuilder/samples/SampleApp/appsettings.json +++ b/src/DefaultBuilder/samples/SampleApp/appsettings.json @@ -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": "", - // "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": "", - // "Password": "" - //}, - - // From cert store: - //"Default": { - // "Subject": "", - // "Store": "", - // "Location": "", - // "AllowInvalid": "" // Set to "true" to allow invalid certificates (e.g. expired certificates) - //} - } } diff --git a/src/DefaultBuilder/src/ForwardedHeadersStartupFilter.cs b/src/DefaultBuilder/src/ForwardedHeadersStartupFilter.cs new file mode 100644 index 000000000000..513aa5acc1fc --- /dev/null +++ b/src/DefaultBuilder/src/ForwardedHeadersStartupFilter.cs @@ -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 Configure(Action next) + { + return app => + { + app.UseForwardedHeaders(); + next(app); + }; + } + } +} diff --git a/src/DefaultBuilder/src/GenericHostBuilderExtensions.cs b/src/DefaultBuilder/src/GenericHostBuilderExtensions.cs index 80d0074e403c..2b8d3df2758e 100644 --- a/src/DefaultBuilder/src/GenericHostBuilderExtensions.cs +++ b/src/DefaultBuilder/src/GenericHostBuilderExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore; @@ -15,6 +15,8 @@ public static class GenericHostBuilderExtensions /// /// The following defaults are applied to the : /// 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. /// /// The instance to configure diff --git a/src/DefaultBuilder/src/WebHost.cs b/src/DefaultBuilder/src/WebHost.cs index 78ec34847c6c..e33593104259 100644 --- a/src/DefaultBuilder/src/WebHost.cs +++ b/src/DefaultBuilder/src/WebHost.cs @@ -8,6 +8,7 @@ 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 @@ private static IWebHost StartWith(string url, Action configu /// load from User Secrets when is 'Development' using the entry assembly, /// load from environment variables, /// configure the 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. /// /// The initialized . @@ -142,6 +145,8 @@ public static IWebHostBuilder CreateDefaultBuilder() => /// load from environment variables, /// load from supplied command line args, /// configure the 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. /// /// The command line args. @@ -224,6 +229,20 @@ internal static void ConfigureWebDefaults(IWebHostBuilder builder) services.AddTransient(); + if (string.Equals("true", hostingContext.Configuration["ForwardedHeaders_Enabled"], StringComparison.OrdinalIgnoreCase)) + { + services.Configure(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(); + } + services.AddRouting(); }) .UseIIS() diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/Microsoft.AspNetCore.Tests.csproj b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/Microsoft.AspNetCore.Tests.csproj index 40c05846ba1d..d23ae117802d 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/Microsoft.AspNetCore.Tests.csproj +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/Microsoft.AspNetCore.Tests.csproj @@ -6,6 +6,7 @@ + diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebHostTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebHostTests.cs index d6c3d255cda4..ec0d57a0e47e 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebHostTests.cs +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebHostTests.cs @@ -8,9 +8,11 @@ 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 @@ public async Task WebHostConfiguration_HostFilterOptionsAreReloadable() Assert.Contains("NewHost", options.AllowedHosts); } + [Fact] + public async Task WebHostConfiguration_EnablesForwardedHeadersFromConfig() + { + using var host = WebHost.CreateDefaultBuilder() + .ConfigureAppConfiguration(configBuilder => + { + configBuilder.AddInMemoryCollection(new[] + { + new KeyValuePair("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() { diff --git a/src/Hosting/TestHost/ref/Microsoft.AspNetCore.TestHost.netcoreapp3.0.cs b/src/Hosting/TestHost/ref/Microsoft.AspNetCore.TestHost.netcoreapp3.0.cs index 371fdc21355b..e851c00d4d95 100644 --- a/src/Hosting/TestHost/ref/Microsoft.AspNetCore.TestHost.netcoreapp3.0.cs +++ b/src/Hosting/TestHost/ref/Microsoft.AspNetCore.TestHost.netcoreapp3.0.cs @@ -48,6 +48,8 @@ public static partial class WebHostBuilderExtensions { public static Microsoft.AspNetCore.Hosting.IWebHostBuilder ConfigureTestContainer(this Microsoft.AspNetCore.Hosting.IWebHostBuilder webHostBuilder, System.Action servicesConfiguration) { throw null; } public static Microsoft.AspNetCore.Hosting.IWebHostBuilder ConfigureTestServices(this Microsoft.AspNetCore.Hosting.IWebHostBuilder webHostBuilder, System.Action 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; } diff --git a/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs b/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs index 41b7c31dba2c..7e7ba6780912 100644 --- a/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs +++ b/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs @@ -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 @@ public static IWebHostBuilder UseTestServer(this IWebHostBuilder builder) }); } + /// + /// Retrieves the TestServer from the host services. + /// + /// + /// + public static TestServer GetTestServer(this IWebHost host) + { + return (TestServer)host.Services.GetRequiredService(); + } + + /// + /// Retrieves the test client from the TestServer in the host services. + /// + /// + /// + public static HttpClient GetTestClient(this IWebHost host) + { + return host.GetTestServer().CreateClient(); + } + public static IWebHostBuilder ConfigureTestServices(this IWebHostBuilder webHostBuilder, Action servicesConfiguration) { if (webHostBuilder == null) diff --git a/src/Middleware/HttpOverrides/src/ForwardedHeadersExtensions.cs b/src/Middleware/HttpOverrides/src/ForwardedHeadersExtensions.cs index df3c221996e1..3572c3d72a3d 100644 --- a/src/Middleware/HttpOverrides/src/ForwardedHeadersExtensions.cs +++ b/src/Middleware/HttpOverrides/src/ForwardedHeadersExtensions.cs @@ -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"; + /// /// Forwards proxied headers onto current request /// @@ -21,7 +23,15 @@ public static IApplicationBuilder UseForwardedHeaders(this IApplicationBuilder b throw new ArgumentNullException(nameof(builder)); } - return builder.UseMiddleware(); + // 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(); + } + + return builder; } /// diff --git a/src/Middleware/HttpOverrides/test/ForwardedHeadersMiddlewareTest.cs b/src/Middleware/HttpOverrides/test/ForwardedHeadersMiddlewareTest.cs index 5f18bf457497..f427c5b5b404 100644 --- a/src/Middleware/HttpOverrides/test/ForwardedHeadersMiddlewareTest.cs +++ b/src/Middleware/HttpOverrides/test/ForwardedHeadersMiddlewareTest.cs @@ -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 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 @@ public async Task XForwardForIPv4ToIPv6Mapping(string forHeader, string knownPro 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(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()); + } } }