Merge source code from aspnet/BasicMiddleware
This commit is contained in:
commit
0843320e3e
|
|
@ -0,0 +1,26 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.HostFiltering" />
|
||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||
<Reference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Console" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="appsettings.Development.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="appsettings.Production.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// 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 Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HostFilteringSample
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
BuildWebHost(args).Run();
|
||||
}
|
||||
|
||||
public static IWebHost BuildWebHost(string[] args)
|
||||
{
|
||||
var hostBuilder = new WebHostBuilder()
|
||||
.ConfigureLogging((_, factory) =>
|
||||
{
|
||||
factory.SetMinimumLevel(LogLevel.Debug);
|
||||
factory.AddConsole();
|
||||
})
|
||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
{
|
||||
var env = hostingContext.HostingEnvironment;
|
||||
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
|
||||
})
|
||||
.UseKestrel()
|
||||
.UseStartup<Startup>();
|
||||
|
||||
return hostBuilder.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:14124/",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"HostFilteringSample": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "http://localhost:14125/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// 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.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.HostFiltering;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HostFilteringSample
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public IConfiguration Config { get; }
|
||||
|
||||
public Startup(IConfiguration config)
|
||||
{
|
||||
Config = config;
|
||||
}
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddHostFiltering(options =>
|
||||
{
|
||||
|
||||
});
|
||||
|
||||
// Fallback
|
||||
services.PostConfigure<HostFilteringOptions>(options =>
|
||||
{
|
||||
if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)
|
||||
{
|
||||
// "AllowedHosts": "localhost;127.0.0.1;[::1]"
|
||||
var hosts = Config["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
// Fall back to "*" to disable.
|
||||
options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" });
|
||||
}
|
||||
});
|
||||
// Change notification
|
||||
services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>(new ConfigurationChangeTokenSource<HostFilteringOptions>(Config));
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
||||
{
|
||||
app.UseHostFiltering();
|
||||
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello World! " + context.Request.Host);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"AllowedHosts": "localhost;127.0.0.1;[::1]"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"AllowedHosts": "example.com;localhost"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// 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.HostFiltering;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the HostFiltering middleware.
|
||||
/// </summary>
|
||||
public static class HostFilteringBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds middleware for filtering requests by allowed host headers. Invalid requests will be rejected with a
|
||||
/// 400 status code.
|
||||
/// </summary>
|
||||
/// <param name="app">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
|
||||
/// <returns>The original <see cref="IApplicationBuilder"/>.</returns>
|
||||
public static IApplicationBuilder UseHostFiltering(this IApplicationBuilder app)
|
||||
{
|
||||
if (app == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(app));
|
||||
}
|
||||
|
||||
app.UseMiddleware<HostFilteringMiddleware>();
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.HostFiltering
|
||||
{
|
||||
/// <summary>
|
||||
/// A middleware used to filter requests by their Host header.
|
||||
/// </summary>
|
||||
public class HostFilteringMiddleware
|
||||
{
|
||||
// Matches Http.Sys.
|
||||
private static readonly byte[] DefaultResponse = Encoding.ASCII.GetBytes(
|
||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\"http://www.w3.org/TR/html4/strict.dtd\">\r\n"
|
||||
+ "<HTML><HEAD><TITLE>Bad Request</TITLE>\r\n"
|
||||
+ "<META HTTP-EQUIV=\"Content-Type\" Content=\"text/html; charset=us-ascii\"></ HEAD >\r\n"
|
||||
+ "<BODY><h2>Bad Request - Invalid Hostname</h2>\r\n"
|
||||
+ "<hr><p>HTTP Error 400. The request hostname is invalid.</p>\r\n"
|
||||
+ "</BODY></HTML>");
|
||||
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<HostFilteringMiddleware> _logger;
|
||||
private readonly IOptionsMonitor<HostFilteringOptions> _optionsMonitor;
|
||||
private HostFilteringOptions _options;
|
||||
private IList<StringSegment> _allowedHosts;
|
||||
private bool? _allowAnyNonEmptyHost;
|
||||
|
||||
/// <summary>
|
||||
/// A middleware used to filter requests by their Host header.
|
||||
/// </summary>
|
||||
/// <param name="next"></param>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="optionsMonitor"></param>
|
||||
public HostFilteringMiddleware(RequestDelegate next, ILogger<HostFilteringMiddleware> logger,
|
||||
IOptionsMonitor<HostFilteringOptions> optionsMonitor)
|
||||
{
|
||||
_next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
|
||||
_options = _optionsMonitor.CurrentValue;
|
||||
_optionsMonitor.OnChange(options =>
|
||||
{
|
||||
// Clear the cached settings so the next EnsureConfigured will re-evaluate.
|
||||
_options = options;
|
||||
_allowedHosts = new List<StringSegment>();
|
||||
_allowAnyNonEmptyHost = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes requests
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public Task Invoke(HttpContext context)
|
||||
{
|
||||
var allowedHosts = EnsureConfigured();
|
||||
|
||||
if (!CheckHost(context, allowedHosts))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
if (_options.IncludeFailureMessage)
|
||||
{
|
||||
context.Response.ContentLength = DefaultResponse.Length;
|
||||
context.Response.ContentType = "text/html";
|
||||
return context.Response.Body.WriteAsync(DefaultResponse, 0, DefaultResponse.Length);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return _next(context);
|
||||
}
|
||||
|
||||
private IList<StringSegment> EnsureConfigured()
|
||||
{
|
||||
if (_allowAnyNonEmptyHost == true || _allowedHosts?.Count > 0)
|
||||
{
|
||||
return _allowedHosts;
|
||||
}
|
||||
|
||||
var allowedHosts = new List<StringSegment>();
|
||||
if (_options.AllowedHosts?.Count > 0 && !TryProcessHosts(_options.AllowedHosts, allowedHosts))
|
||||
{
|
||||
_logger.LogDebug("Wildcard detected, all requests with hosts will be allowed.");
|
||||
_allowedHosts = allowedHosts;
|
||||
_allowAnyNonEmptyHost = true;
|
||||
return _allowedHosts;
|
||||
}
|
||||
|
||||
if (allowedHosts.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No allowed hosts were configured.");
|
||||
}
|
||||
|
||||
_logger.LogDebug("Allowed hosts: " + string.Join("; ", allowedHosts));
|
||||
_allowedHosts = allowedHosts;
|
||||
return _allowedHosts;
|
||||
}
|
||||
|
||||
// returns false if any wildcards were found
|
||||
private bool TryProcessHosts(IEnumerable<string> incoming, IList<StringSegment> results)
|
||||
{
|
||||
foreach (var entry in incoming)
|
||||
{
|
||||
// Punycode. Http.Sys requires you to register Unicode hosts, but the headers contain punycode.
|
||||
var host = new HostString(entry).ToUriComponent();
|
||||
|
||||
if (IsTopLevelWildcard(host))
|
||||
{
|
||||
// Disable filtering
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!results.Contains(host, StringSegmentComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
results.Add(host);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsTopLevelWildcard(string host)
|
||||
{
|
||||
return (string.Equals("*", host, StringComparison.Ordinal) // HttpSys wildcard
|
||||
|| string.Equals("[::]", host, StringComparison.Ordinal) // Kestrel wildcard, IPv6 Any
|
||||
|| string.Equals("0.0.0.0", host, StringComparison.Ordinal)); // IPv4 Any
|
||||
}
|
||||
|
||||
// This does not duplicate format validations that are expected to be performed by the host.
|
||||
private bool CheckHost(HttpContext context, IList<StringSegment> allowedHosts)
|
||||
{
|
||||
var host = new StringSegment(context.Request.Headers[HeaderNames.Host].ToString()).Trim();
|
||||
|
||||
if (StringSegment.IsNullOrEmpty(host))
|
||||
{
|
||||
// Http/1.0 does not require the host header.
|
||||
// Http/1.1 requires the header but the value may be empty.
|
||||
if (!_options.AllowEmptyHosts)
|
||||
{
|
||||
_logger.LogInformation($"{context.Request.Protocol} request rejected due to missing or empty host header.");
|
||||
return false;
|
||||
}
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug($"{context.Request.Protocol} request allowed with missing or empty host header.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_allowAnyNonEmptyHost == true)
|
||||
{
|
||||
_logger.LogTrace($"All hosts are allowed.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (HostString.MatchesAny(host, allowedHosts))
|
||||
{
|
||||
_logger.LogTrace($"The host '{host}' matches an allowed host.");
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.LogInformation($"The host '{host}' does not match an allowed host.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// 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.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.HostFiltering
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the HostFiltering middleware
|
||||
/// </summary>
|
||||
public class HostFilteringOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The hosts headers that are allowed to access this site. At least one value is required.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Port numbers must be excluded.</description></item>
|
||||
/// <item><description>A top level wildcard "*" allows all non-empty hosts.</description></item>
|
||||
/// <item><description>Subdomain wildcards are permitted. E.g. "*.example.com" matches subdomains like foo.example.com,
|
||||
/// but not the parent domain example.com.</description></item>
|
||||
/// <item><description>Unicode host names are allowed but will be converted to punycode for matching.</description></item>
|
||||
/// <item><description>IPv6 addresses must include their bounding brackets and be in their normalized form.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public IList<string> AllowedHosts { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if requests without hosts are allowed. The default is true.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// HTTP/1.0 does not require a host header.
|
||||
/// Http/1.1 requires a host header, but says the value may be empty.
|
||||
/// </remarks>
|
||||
public bool AllowEmptyHosts { get; set; } = true;
|
||||
|
||||
// Note if this were disabled then things like the status code middleware may try to re-execute
|
||||
// the request. This is a low level protocol violation, pretty error pages should not be required.
|
||||
/// <summary>
|
||||
/// Indicates if the 400 response should include a default message or be empty. This is enabled by default.
|
||||
/// </summary>
|
||||
public bool IncludeFailureMessage { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
// 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.HostFiltering;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the host filtering middleware.
|
||||
/// </summary>
|
||||
public static class HostFilteringServicesExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds services and options for the host filtering middleware.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
|
||||
/// <param name="configureOptions">A delegate to configure the <see cref="HostFilteringOptions"/>.</param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddHostFiltering(this IServiceCollection services, Action<HostFilteringOptions> configureOptions)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
if (configureOptions == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configureOptions));
|
||||
}
|
||||
|
||||
services.Configure(configureOptions);
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
ASP.NET Core middleware for filtering out requests with unknown HTTP host headers.
|
||||
</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>aspnetcore</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Http" />
|
||||
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
|
||||
<Reference Include="Microsoft.Extensions.Options" />
|
||||
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
{
|
||||
"AssemblyIdentity": "Microsoft.AspNetCore.HostFiltering, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||
"Types": [
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Invoke",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "next",
|
||||
"Type": "Microsoft.AspNetCore.Http.RequestDelegate"
|
||||
},
|
||||
{
|
||||
"Name": "logger",
|
||||
"Type": "Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware>"
|
||||
},
|
||||
{
|
||||
"Name": "optionsMonitor",
|
||||
"Type": "Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.AspNetCore.HostFiltering.HostFilteringOptions>"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HostFiltering.HostFilteringOptions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_AllowedHosts",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Collections.Generic.IList<System.String>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_AllowedHosts",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Collections.Generic.IList<System.String>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_AllowEmptyHosts",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_AllowEmptyHosts",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Boolean"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_IncludeFailureMessage",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_IncludeFailureMessage",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Boolean"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.HostFilteringBuilderExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseHostFiltering",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "app",
|
||||
"Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.HostFilteringServicesExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "AddHostFiltering",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "services",
|
||||
"Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
|
||||
},
|
||||
{
|
||||
"Name": "configureOptions",
|
||||
"Type": "System.Action<Microsoft.AspNetCore.HostFiltering.HostFilteringOptions>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.HostFiltering
|
||||
{
|
||||
public class HostFilteringMiddlewareTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task MissingConfigThrows()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHostFiltering();
|
||||
});
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => new TestServer(builder).SendAsync(_ => { }));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, 200)]
|
||||
[InlineData(false, 400)]
|
||||
public async Task AllowsMissingHost(bool allowed, int status)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHostFiltering(options =>
|
||||
{
|
||||
options.AllowEmptyHosts = allowed;
|
||||
options.AllowedHosts.Add("Localhost");
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Use((ctx, next) =>
|
||||
{
|
||||
ctx.Request.Headers.Remove(HeaderNames.Host);
|
||||
return next();
|
||||
});
|
||||
app.UseHostFiltering();
|
||||
app.Run(c =>
|
||||
{
|
||||
Assert.False(c.Request.Headers.TryGetValue(HeaderNames.Host, out var host));
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var response = await server.CreateClient().GetAsync("/");
|
||||
Assert.Equal(status, (int)response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, 200)]
|
||||
[InlineData(false, 400)]
|
||||
public async Task AllowsEmptyHost(bool allowed, int status)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHostFiltering(options =>
|
||||
{
|
||||
options.AllowEmptyHosts = allowed;
|
||||
options.AllowedHosts.Add("Localhost");
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Use((ctx, next) =>
|
||||
{
|
||||
ctx.Request.Headers[HeaderNames.Host] = " ";
|
||||
return next();
|
||||
});
|
||||
app.UseHostFiltering();
|
||||
app.Run(c =>
|
||||
{
|
||||
Assert.True(c.Request.Headers.TryGetValue(HeaderNames.Host, out var host));
|
||||
Assert.True(StringValues.Equals(" ", host));
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
app.Run(c => Task.CompletedTask);
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var response = await server.CreateClient().GetAsync("/");
|
||||
Assert.Equal(status, (int)response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("localHost", "localhost")]
|
||||
[InlineData("localHost", "*")] // Any - Used by HttpSys
|
||||
[InlineData("localHost", "[::]")] // IPv6 Any - This is what Kestrel reports when binding to *
|
||||
[InlineData("localHost", "0.0.0.0")] // IPv4 Any
|
||||
[InlineData("localhost:9090", "example.com;localHost")]
|
||||
[InlineData("example.com:443", "example.com;localhost")]
|
||||
[InlineData("localHost:80", "localhost;")]
|
||||
[InlineData("foo.eXample.com:443", "*.exampLe.com")]
|
||||
[InlineData("f.eXample.com:443", "*.exampLe.com")]
|
||||
[InlineData("127.0.0.1", "127.0.0.1")]
|
||||
[InlineData("127.0.0.1:443", "127.0.0.1")]
|
||||
[InlineData("xn--c1yn36f:443", "xn--c1yn36f")]
|
||||
[InlineData("xn--c1yn36f:443", "點看")]
|
||||
[InlineData("[::ABC]", "[::aBc]")]
|
||||
[InlineData("[::1]:80", "[::1]")]
|
||||
public async Task AllowsSpecifiedHost(string host, string allowedHost)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHostFiltering(options =>
|
||||
{
|
||||
options.AllowedHosts = allowedHost.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Use((ctx, next) =>
|
||||
{
|
||||
// TestHost's ClientHandler doesn't let you set the host header, only the host in the URI
|
||||
// and that would over-normalize some of our test conditions like casing.
|
||||
ctx.Request.Headers[HeaderNames.Host] = host;
|
||||
return next();
|
||||
});
|
||||
app.UseHostFiltering();
|
||||
app.Run(c => Task.CompletedTask);
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var response = await server.CreateRequest("/").GetAsync();
|
||||
Assert.Equal(200, (int)response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("example.com", "localhost")]
|
||||
[InlineData("localhost:9090", "example.com;")]
|
||||
[InlineData(";", "example.com;localhost")]
|
||||
[InlineData(";:80", "example.com;localhost")]
|
||||
[InlineData(":80", "localhost")]
|
||||
[InlineData(":", "localhost")]
|
||||
[InlineData("example.com:443", "*.example.com")]
|
||||
[InlineData(".example.com:443", "*.example.com")]
|
||||
[InlineData("foo.com:443", "*.example.com")]
|
||||
[InlineData("foo.example.com.bar:443", "*.example.com")]
|
||||
[InlineData(".com:443", "*.com")]
|
||||
// Unicode in the host shouldn't be allowed without punycode anyways. This match fails because the middleware converts
|
||||
// its input to punycode.
|
||||
[InlineData("點看", "點看")]
|
||||
[InlineData("[::1", "[::1]")]
|
||||
[InlineData("[::1:80", "[::1]")]
|
||||
public async Task RejectsMismatchedHosts(string host, string allowedHost)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHostFiltering(options =>
|
||||
{
|
||||
options.AllowedHosts = allowedHost.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Use((ctx, next) =>
|
||||
{
|
||||
// TestHost's ClientHandler doesn't let you set the host header, only the host in the URI
|
||||
// and that would reject some of our test conditions.
|
||||
ctx.Request.Headers[HeaderNames.Host] = host;
|
||||
return next();
|
||||
});
|
||||
app.UseHostFiltering();
|
||||
app.Run(c => throw new NotImplementedException("App"));
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var response = await server.CreateRequest("/").GetAsync();
|
||||
Assert.Equal(400, (int)response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SupportsDynamicOptionsReload()
|
||||
{
|
||||
var config = new ConfigurationBuilder().Add(new ReloadableMemorySource()).Build();
|
||||
config["AllowedHosts"] = "localhost";
|
||||
var currentHost = "otherHost";
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHostFiltering(options =>
|
||||
{
|
||||
options.AllowedHosts = new[] { config["AllowedHosts"] };
|
||||
});
|
||||
services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>(new ConfigurationChangeTokenSource<HostFilteringOptions>(config));
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Use((ctx, next) =>
|
||||
{
|
||||
ctx.Request.Headers[HeaderNames.Host] = currentHost;
|
||||
return next();
|
||||
});
|
||||
app.UseHostFiltering();
|
||||
app.Run(c => Task.CompletedTask);
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var response = await server.CreateRequest("/").GetAsync();
|
||||
Assert.Equal(400, (int)response.StatusCode);
|
||||
|
||||
config["AllowedHosts"] = "otherHost";
|
||||
|
||||
response = await server.CreateRequest("/").GetAsync();
|
||||
Assert.Equal(200, (int)response.StatusCode);
|
||||
}
|
||||
|
||||
private class ReloadableMemorySource : IConfigurationSource
|
||||
{
|
||||
public IConfigurationProvider Build(IConfigurationBuilder builder)
|
||||
{
|
||||
return new ReloadableMemoryProvider();
|
||||
}
|
||||
}
|
||||
|
||||
internal class ReloadableMemoryProvider : ConfigurationProvider
|
||||
{
|
||||
public override void Set(string key, string value)
|
||||
{
|
||||
base.Set(key, value);
|
||||
OnReload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.HostFiltering" />
|
||||
<Reference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.HttpOverrides" />
|
||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:1658/",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"commandName": "web",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
|
||||
namespace HttpOverridesSample
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||
});
|
||||
app.UseHttpMethodOverride();
|
||||
|
||||
app.Run(async (context) =>
|
||||
{
|
||||
foreach (var header in context.Request.Headers)
|
||||
{
|
||||
await context.Response.WriteAsync($"{header.Key}: {header.Value}\r\n");
|
||||
}
|
||||
await context.Response.WriteAsync($"Method: {context.Request.Method}\r\n");
|
||||
await context.Response.WriteAsync($"Scheme: {context.Request.Scheme}\r\n");
|
||||
await context.Response.WriteAsync($"RemoteIP: {context.Connection.RemoteIpAddress}\r\n");
|
||||
await context.Response.WriteAsync($"RemotePort: {context.Connection.RemotePort}\r\n");
|
||||
});
|
||||
}
|
||||
|
||||
// Entry point for the application.
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var host = new WebHostBuilder()
|
||||
.UseKestrel()
|
||||
// .UseIIS() // This repo can no longer reference IIS because IISIntegration depends on it.
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
|
||||
host.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpOverrides
|
||||
{
|
||||
[Flags]
|
||||
public enum ForwardedHeaders
|
||||
{
|
||||
None = 0,
|
||||
XForwardedFor = 1 << 0,
|
||||
XForwardedHost = 1 << 1,
|
||||
XForwardedProto = 1 << 2,
|
||||
All = XForwardedFor | XForwardedHost | XForwardedProto
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpOverrides
|
||||
{
|
||||
/// <summary>
|
||||
/// Default values related to <see cref="ForwardedHeadersMiddleware"/> middleware
|
||||
/// </summary>
|
||||
/// <seealso cref="Microsoft.AspNetCore.Builder.ForwardedHeadersOptions"/>
|
||||
public static class ForwardedHeadersDefaults
|
||||
{
|
||||
/// <summary>
|
||||
/// X-Forwarded-For
|
||||
/// </summary>
|
||||
public static string XForwardedForHeaderName { get; } = "X-Forwarded-For";
|
||||
|
||||
/// <summary>
|
||||
/// X-Forwarded-Host
|
||||
/// </summary>
|
||||
public static string XForwardedHostHeaderName { get; } = "X-Forwarded-Host";
|
||||
|
||||
/// <summary>
|
||||
/// X-Forwarded-Proto
|
||||
/// </summary>
|
||||
public static string XForwardedProtoHeaderName { get; } = "X-Forwarded-Proto";
|
||||
|
||||
/// <summary>
|
||||
/// X-Original-For
|
||||
/// </summary>
|
||||
public static string XOriginalForHeaderName { get; } = "X-Original-For";
|
||||
|
||||
/// <summary>
|
||||
/// X-Original-Host
|
||||
/// </summary>
|
||||
public static string XOriginalHostHeaderName { get; } = "X-Original-Host";
|
||||
|
||||
/// <summary>
|
||||
/// X-Original-Proto
|
||||
/// </summary>
|
||||
public static string XOriginalProtoHeaderName { get; } = "X-Original-Proto";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
// 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.HttpOverrides;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
public static class ForwardedHeadersExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Forwards proxied headers onto current request
|
||||
/// </summary>
|
||||
/// <param name="builder"></param>
|
||||
/// <returns></returns>
|
||||
public static IApplicationBuilder UseForwardedHeaders(this IApplicationBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
return builder.UseMiddleware<ForwardedHeadersMiddleware>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forwards proxied headers onto current request
|
||||
/// </summary>
|
||||
/// <param name="builder"></param>
|
||||
/// <param name="options">Enables the different forwarding options.</param>
|
||||
/// <returns></returns>
|
||||
public static IApplicationBuilder UseForwardedHeaders(this IApplicationBuilder builder, ForwardedHeadersOptions options)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
return builder.UseMiddleware<ForwardedHeadersMiddleware>(Options.Create(options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,493 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpOverrides.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpOverrides
|
||||
{
|
||||
public class ForwardedHeadersMiddleware
|
||||
{
|
||||
private static readonly bool[] HostCharValidity = new bool[127];
|
||||
private static readonly bool[] SchemeCharValidity = new bool[123];
|
||||
|
||||
private readonly ForwardedHeadersOptions _options;
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger _logger;
|
||||
private bool _allowAllHosts;
|
||||
private IList<StringSegment> _allowedHosts;
|
||||
|
||||
static ForwardedHeadersMiddleware()
|
||||
{
|
||||
// RFC 3986 scheme = ALPHA * (ALPHA / DIGIT / "+" / "-" / ".")
|
||||
SchemeCharValidity['+'] = true;
|
||||
SchemeCharValidity['-'] = true;
|
||||
SchemeCharValidity['.'] = true;
|
||||
|
||||
// Host Matches Http.Sys and Kestrel
|
||||
// Host Matches RFC 3986 except "*" / "+" / "," / ";" / "=" and "%" HEXDIG HEXDIG which are not allowed by Http.Sys
|
||||
HostCharValidity['!'] = true;
|
||||
HostCharValidity['$'] = true;
|
||||
HostCharValidity['&'] = true;
|
||||
HostCharValidity['\''] = true;
|
||||
HostCharValidity['('] = true;
|
||||
HostCharValidity[')'] = true;
|
||||
HostCharValidity['-'] = true;
|
||||
HostCharValidity['.'] = true;
|
||||
HostCharValidity['_'] = true;
|
||||
HostCharValidity['~'] = true;
|
||||
for (var ch = '0'; ch <= '9'; ch++)
|
||||
{
|
||||
SchemeCharValidity[ch] = true;
|
||||
HostCharValidity[ch] = true;
|
||||
}
|
||||
for (var ch = 'A'; ch <= 'Z'; ch++)
|
||||
{
|
||||
SchemeCharValidity[ch] = true;
|
||||
HostCharValidity[ch] = true;
|
||||
}
|
||||
for (var ch = 'a'; ch <= 'z'; ch++)
|
||||
{
|
||||
SchemeCharValidity[ch] = true;
|
||||
HostCharValidity[ch] = true;
|
||||
}
|
||||
}
|
||||
|
||||
public ForwardedHeadersMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<ForwardedHeadersOptions> options)
|
||||
{
|
||||
if (next == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(next));
|
||||
}
|
||||
if (loggerFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
}
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
// Make sure required options is not null or whitespace
|
||||
EnsureOptionNotNullorWhitespace(options.Value.ForwardedForHeaderName, nameof(options.Value.ForwardedForHeaderName));
|
||||
EnsureOptionNotNullorWhitespace(options.Value.ForwardedHostHeaderName, nameof(options.Value.ForwardedHostHeaderName));
|
||||
EnsureOptionNotNullorWhitespace(options.Value.ForwardedProtoHeaderName, nameof(options.Value.ForwardedProtoHeaderName));
|
||||
EnsureOptionNotNullorWhitespace(options.Value.OriginalForHeaderName, nameof(options.Value.OriginalForHeaderName));
|
||||
EnsureOptionNotNullorWhitespace(options.Value.OriginalHostHeaderName, nameof(options.Value.OriginalHostHeaderName));
|
||||
EnsureOptionNotNullorWhitespace(options.Value.OriginalProtoHeaderName, nameof(options.Value.OriginalProtoHeaderName));
|
||||
|
||||
_options = options.Value;
|
||||
_logger = loggerFactory.CreateLogger<ForwardedHeadersMiddleware>();
|
||||
_next = next;
|
||||
|
||||
PreProcessHosts();
|
||||
}
|
||||
|
||||
private static void EnsureOptionNotNullorWhitespace(string value, string propertyName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException($"options.{propertyName} is required", "options");
|
||||
}
|
||||
}
|
||||
|
||||
private void PreProcessHosts()
|
||||
{
|
||||
if (_options.AllowedHosts == null || _options.AllowedHosts.Count == 0)
|
||||
{
|
||||
_allowAllHosts = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var allowedHosts = new List<StringSegment>();
|
||||
foreach (var entry in _options.AllowedHosts)
|
||||
{
|
||||
// Punycode. Http.Sys requires you to register Unicode hosts, but the headers contain punycode.
|
||||
var host = new HostString(entry).ToUriComponent();
|
||||
|
||||
if (IsTopLevelWildcard(host))
|
||||
{
|
||||
// Disable filtering
|
||||
_allowAllHosts = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allowedHosts.Contains(host, StringSegmentComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
allowedHosts.Add(host);
|
||||
}
|
||||
}
|
||||
|
||||
_allowedHosts = allowedHosts;
|
||||
}
|
||||
|
||||
private bool IsTopLevelWildcard(string host)
|
||||
{
|
||||
return (string.Equals("*", host, StringComparison.Ordinal) // HttpSys wildcard
|
||||
|| string.Equals("[::]", host, StringComparison.Ordinal) // Kestrel wildcard, IPv6 Any
|
||||
|| string.Equals("0.0.0.0", host, StringComparison.Ordinal)); // IPv4 Any
|
||||
}
|
||||
|
||||
public Task Invoke(HttpContext context)
|
||||
{
|
||||
ApplyForwarders(context);
|
||||
return _next(context);
|
||||
}
|
||||
|
||||
public void ApplyForwarders(HttpContext context)
|
||||
{
|
||||
// Gather expected headers. Enabled headers must have the same number of entries.
|
||||
string[] forwardedFor = null, forwardedProto = null, forwardedHost = null;
|
||||
bool checkFor = false, checkProto = false, checkHost = false;
|
||||
int entryCount = 0;
|
||||
|
||||
if ((_options.ForwardedHeaders & ForwardedHeaders.XForwardedFor) == ForwardedHeaders.XForwardedFor)
|
||||
{
|
||||
checkFor = true;
|
||||
forwardedFor = context.Request.Headers.GetCommaSeparatedValues(_options.ForwardedForHeaderName);
|
||||
entryCount = Math.Max(forwardedFor.Length, entryCount);
|
||||
}
|
||||
|
||||
if ((_options.ForwardedHeaders & ForwardedHeaders.XForwardedProto) == ForwardedHeaders.XForwardedProto)
|
||||
{
|
||||
checkProto = true;
|
||||
forwardedProto = context.Request.Headers.GetCommaSeparatedValues(_options.ForwardedProtoHeaderName);
|
||||
if (_options.RequireHeaderSymmetry && checkFor && forwardedFor.Length != forwardedProto.Length)
|
||||
{
|
||||
_logger.LogWarning(1, "Parameter count mismatch between X-Forwarded-For and X-Forwarded-Proto.");
|
||||
return;
|
||||
}
|
||||
entryCount = Math.Max(forwardedProto.Length, entryCount);
|
||||
}
|
||||
|
||||
if ((_options.ForwardedHeaders & ForwardedHeaders.XForwardedHost) == ForwardedHeaders.XForwardedHost)
|
||||
{
|
||||
checkHost = true;
|
||||
forwardedHost = context.Request.Headers.GetCommaSeparatedValues(_options.ForwardedHostHeaderName);
|
||||
if (_options.RequireHeaderSymmetry
|
||||
&& ((checkFor && forwardedFor.Length != forwardedHost.Length)
|
||||
|| (checkProto && forwardedProto.Length != forwardedHost.Length)))
|
||||
{
|
||||
_logger.LogWarning(1, "Parameter count mismatch between X-Forwarded-Host and X-Forwarded-For or X-Forwarded-Proto.");
|
||||
return;
|
||||
}
|
||||
entryCount = Math.Max(forwardedHost.Length, entryCount);
|
||||
}
|
||||
|
||||
// Apply ForwardLimit, if any
|
||||
if (_options.ForwardLimit.HasValue && entryCount > _options.ForwardLimit)
|
||||
{
|
||||
entryCount = _options.ForwardLimit.Value;
|
||||
}
|
||||
|
||||
// Group the data together.
|
||||
var sets = new SetOfForwarders[entryCount];
|
||||
for (int i = 0; i < sets.Length; i++)
|
||||
{
|
||||
// They get processed in reverse order, right to left.
|
||||
var set = new SetOfForwarders();
|
||||
if (checkFor && i < forwardedFor.Length)
|
||||
{
|
||||
set.IpAndPortText = forwardedFor[forwardedFor.Length - i - 1];
|
||||
}
|
||||
if (checkProto && i < forwardedProto.Length)
|
||||
{
|
||||
set.Scheme = forwardedProto[forwardedProto.Length - i - 1];
|
||||
}
|
||||
if (checkHost && i < forwardedHost.Length)
|
||||
{
|
||||
set.Host = forwardedHost[forwardedHost.Length - i - 1];
|
||||
}
|
||||
sets[i] = set;
|
||||
}
|
||||
|
||||
// Gather initial values
|
||||
var connection = context.Connection;
|
||||
var request = context.Request;
|
||||
var currentValues = new SetOfForwarders()
|
||||
{
|
||||
RemoteIpAndPort = connection.RemoteIpAddress != null ? new IPEndPoint(connection.RemoteIpAddress, connection.RemotePort) : null,
|
||||
// Host and Scheme initial values are never inspected, no need to set them here.
|
||||
};
|
||||
|
||||
var checkKnownIps = _options.KnownNetworks.Count > 0 || _options.KnownProxies.Count > 0;
|
||||
bool applyChanges = false;
|
||||
int entriesConsumed = 0;
|
||||
|
||||
for ( ; entriesConsumed < sets.Length; entriesConsumed++)
|
||||
{
|
||||
var set = sets[entriesConsumed];
|
||||
if (checkFor)
|
||||
{
|
||||
// For the first instance, allow remoteIp to be null for servers that don't support it natively.
|
||||
if (currentValues.RemoteIpAndPort != null && checkKnownIps && !CheckKnownAddress(currentValues.RemoteIpAndPort.Address))
|
||||
{
|
||||
// Stop at the first unknown remote IP, but still apply changes processed so far.
|
||||
_logger.LogDebug(1, $"Unknown proxy: {currentValues.RemoteIpAndPort}");
|
||||
break;
|
||||
}
|
||||
|
||||
IPEndPoint parsedEndPoint;
|
||||
if (IPEndPointParser.TryParse(set.IpAndPortText, out parsedEndPoint))
|
||||
{
|
||||
applyChanges = true;
|
||||
set.RemoteIpAndPort = parsedEndPoint;
|
||||
currentValues.IpAndPortText = set.IpAndPortText;
|
||||
currentValues.RemoteIpAndPort = set.RemoteIpAndPort;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(set.IpAndPortText))
|
||||
{
|
||||
// Stop at the first unparsable IP, but still apply changes processed so far.
|
||||
_logger.LogDebug(1, $"Unparsable IP: {set.IpAndPortText}");
|
||||
break;
|
||||
}
|
||||
else if (_options.RequireHeaderSymmetry)
|
||||
{
|
||||
_logger.LogWarning(2, $"Missing forwarded IPAddress.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkProto)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(set.Scheme) && TryValidateScheme(set.Scheme))
|
||||
{
|
||||
applyChanges = true;
|
||||
currentValues.Scheme = set.Scheme;
|
||||
}
|
||||
else if (_options.RequireHeaderSymmetry)
|
||||
{
|
||||
_logger.LogWarning(3, $"Forwarded scheme is not present, this is required by {nameof(_options.RequireHeaderSymmetry)}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkHost)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(set.Host) && TryValidateHost(set.Host)
|
||||
&& (_allowAllHosts || HostString.MatchesAny(set.Host, _allowedHosts)))
|
||||
{
|
||||
applyChanges = true;
|
||||
currentValues.Host = set.Host;
|
||||
}
|
||||
else if (_options.RequireHeaderSymmetry)
|
||||
{
|
||||
_logger.LogWarning(4, $"Incorrect number of x-forwarded-proto header values, see {nameof(_options.RequireHeaderSymmetry)}.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (applyChanges)
|
||||
{
|
||||
if (checkFor && currentValues.RemoteIpAndPort != null)
|
||||
{
|
||||
if (connection.RemoteIpAddress != null)
|
||||
{
|
||||
// Save the original
|
||||
request.Headers[_options.OriginalForHeaderName] = new IPEndPoint(connection.RemoteIpAddress, connection.RemotePort).ToString();
|
||||
}
|
||||
if (forwardedFor.Length > entriesConsumed)
|
||||
{
|
||||
// Truncate the consumed header values
|
||||
request.Headers[_options.ForwardedForHeaderName] = forwardedFor.Take(forwardedFor.Length - entriesConsumed).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
// All values were consumed
|
||||
request.Headers.Remove(_options.ForwardedForHeaderName);
|
||||
}
|
||||
connection.RemoteIpAddress = currentValues.RemoteIpAndPort.Address;
|
||||
connection.RemotePort = currentValues.RemoteIpAndPort.Port;
|
||||
}
|
||||
|
||||
if (checkProto && currentValues.Scheme != null)
|
||||
{
|
||||
// Save the original
|
||||
request.Headers[_options.OriginalProtoHeaderName] = request.Scheme;
|
||||
if (forwardedProto.Length > entriesConsumed)
|
||||
{
|
||||
// Truncate the consumed header values
|
||||
request.Headers[_options.ForwardedProtoHeaderName] = forwardedProto.Take(forwardedProto.Length - entriesConsumed).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
// All values were consumed
|
||||
request.Headers.Remove(_options.ForwardedProtoHeaderName);
|
||||
}
|
||||
request.Scheme = currentValues.Scheme;
|
||||
}
|
||||
|
||||
if (checkHost && currentValues.Host != null)
|
||||
{
|
||||
// Save the original
|
||||
request.Headers[_options.OriginalHostHeaderName] = request.Host.ToString();
|
||||
if (forwardedHost.Length > entriesConsumed)
|
||||
{
|
||||
// Truncate the consumed header values
|
||||
request.Headers[_options.ForwardedHostHeaderName] = forwardedHost.Take(forwardedHost.Length - entriesConsumed).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
// All values were consumed
|
||||
request.Headers.Remove(_options.ForwardedHostHeaderName);
|
||||
}
|
||||
request.Host = HostString.FromUriComponent(currentValues.Host);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckKnownAddress(IPAddress address)
|
||||
{
|
||||
if (_options.KnownProxies.Contains(address))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
foreach (var network in _options.KnownNetworks)
|
||||
{
|
||||
if (network.Contains(address))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private struct SetOfForwarders
|
||||
{
|
||||
public string IpAndPortText;
|
||||
public IPEndPoint RemoteIpAndPort;
|
||||
public string Host;
|
||||
public string Scheme;
|
||||
}
|
||||
|
||||
// Empty was checked for by the caller
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool TryValidateScheme(string scheme)
|
||||
{
|
||||
for (var i = 0; i < scheme.Length; i++)
|
||||
{
|
||||
if (!IsValidSchemeChar(scheme[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsValidSchemeChar(char ch)
|
||||
{
|
||||
return ch < SchemeCharValidity.Length && SchemeCharValidity[ch];
|
||||
}
|
||||
|
||||
// Empty was checked for by the caller
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool TryValidateHost(string host)
|
||||
{
|
||||
if (host[0] == '[')
|
||||
{
|
||||
return TryValidateIPv6Host(host);
|
||||
}
|
||||
|
||||
if (host[0] == ':')
|
||||
{
|
||||
// Only a port
|
||||
return false;
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
for (; i < host.Length; i++)
|
||||
{
|
||||
if (!IsValidHostChar(host[i]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return TryValidateHostPort(host, i);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsValidHostChar(char ch)
|
||||
{
|
||||
return ch < HostCharValidity.Length && HostCharValidity[ch];
|
||||
}
|
||||
|
||||
// The lead '[' was already checked
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool TryValidateIPv6Host(string hostText)
|
||||
{
|
||||
for (var i = 1; i < hostText.Length; i++)
|
||||
{
|
||||
var ch = hostText[i];
|
||||
if (ch == ']')
|
||||
{
|
||||
// [::1] is the shortest valid IPv6 host
|
||||
if (i < 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return TryValidateHostPort(hostText, i + 1);
|
||||
}
|
||||
|
||||
if (!IsHex(ch) && ch != ':' && ch != '.')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Must contain a ']'
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool TryValidateHostPort(string hostText, int offset)
|
||||
{
|
||||
if (offset == hostText.Length)
|
||||
{
|
||||
// No port
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hostText[offset] != ':' || hostText.Length == offset + 1)
|
||||
{
|
||||
// Must have at least one number after the colon if present.
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = offset + 1; i < hostText.Length; i++)
|
||||
{
|
||||
if (!IsNumeric(hostText[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool IsNumeric(char ch)
|
||||
{
|
||||
return '0' <= ch && ch <= '9';
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool IsHex(char ch)
|
||||
{
|
||||
return IsNumeric(ch)
|
||||
|| ('a' <= ch && ch <= 'f')
|
||||
|| ('A' <= ch && ch <= 'F');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
public class ForwardedHeadersOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Use this header instead of <see cref="ForwardedHeadersDefaults.XForwardedForHeaderName"/>
|
||||
/// </summary>
|
||||
/// <seealso cref="Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersDefaults"/>
|
||||
public string ForwardedForHeaderName { get; set; } = ForwardedHeadersDefaults.XForwardedForHeaderName;
|
||||
|
||||
/// <summary>
|
||||
/// Use this header instead of <see cref="ForwardedHeadersDefaults.XForwardedHostHeaderName"/>
|
||||
/// </summary>
|
||||
/// <seealso cref="ForwardedHeadersDefaults"/>
|
||||
public string ForwardedHostHeaderName { get; set; } = ForwardedHeadersDefaults.XForwardedHostHeaderName;
|
||||
|
||||
/// <summary>
|
||||
/// Use this header instead of <see cref="ForwardedHeadersDefaults.XForwardedProtoHeaderName"/>
|
||||
/// </summary>
|
||||
/// <seealso cref="ForwardedHeadersDefaults"/>
|
||||
public string ForwardedProtoHeaderName { get; set; } = ForwardedHeadersDefaults.XForwardedProtoHeaderName;
|
||||
|
||||
/// <summary>
|
||||
/// Use this header instead of <see cref="ForwardedHeadersDefaults.XOriginalForHeaderName"/>
|
||||
/// </summary>
|
||||
/// <seealso cref="ForwardedHeadersDefaults"/>
|
||||
public string OriginalForHeaderName { get; set; } = ForwardedHeadersDefaults.XOriginalForHeaderName;
|
||||
|
||||
/// <summary>
|
||||
/// Use this header instead of <see cref="ForwardedHeadersDefaults.XOriginalHostHeaderName"/>
|
||||
/// </summary>
|
||||
/// <seealso cref="ForwardedHeadersDefaults"/>
|
||||
public string OriginalHostHeaderName { get; set; } = ForwardedHeadersDefaults.XOriginalHostHeaderName;
|
||||
|
||||
/// <summary>
|
||||
/// Use this header instead of <see cref="ForwardedHeadersDefaults.XOriginalProtoHeaderName"/>
|
||||
/// </summary>
|
||||
/// <seealso cref="ForwardedHeadersDefaults"/>
|
||||
public string OriginalProtoHeaderName { get; set; } = ForwardedHeadersDefaults.XOriginalProtoHeaderName;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies which forwarders should be processed.
|
||||
/// </summary>
|
||||
public ForwardedHeaders ForwardedHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Limits the number of entries in the headers that will be processed. The default value is 1.
|
||||
/// Set to null to disable the limit, but this should only be done if
|
||||
/// KnownProxies or KnownNetworks are configured.
|
||||
/// </summary>
|
||||
public int? ForwardLimit { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Addresses of known proxies to accept forwarded headers from.
|
||||
/// </summary>
|
||||
public IList<IPAddress> KnownProxies { get; } = new List<IPAddress>() { IPAddress.IPv6Loopback };
|
||||
|
||||
/// <summary>
|
||||
/// Address ranges of known proxies to accept forwarded headers from.
|
||||
/// </summary>
|
||||
public IList<IPNetwork> KnownNetworks { get; } = new List<IPNetwork>() { new IPNetwork(IPAddress.Loopback, 8) };
|
||||
|
||||
/// <summary>
|
||||
/// The allowed values from x-forwarded-host. If the list is empty then all hosts are allowed.
|
||||
/// Failing to restrict this these values may allow an attacker to spoof links generated by your service.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Port numbers must be excluded.</description></item>
|
||||
/// <item><description>A top level wildcard "*" allows all non-empty hosts.</description></item>
|
||||
/// <item><description>Subdomain wildcards are permitted. E.g. "*.example.com" matches subdomains like foo.example.com,
|
||||
/// but not the parent domain example.com.</description></item>
|
||||
/// <item><description>Unicode host names are allowed but will be converted to punycode for matching.</description></item>
|
||||
/// <item><description>IPv6 addresses must include their bounding brackets and be in their normalized form.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public IList<string> AllowedHosts { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Require the number of header values to be in sync between the different headers being processed.
|
||||
/// The default is 'false'.
|
||||
/// </summary>
|
||||
public bool RequireHeaderSymmetry { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// 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.HttpOverrides;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
public static class HttpMethodOverrideExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows incoming POST request to override method type with type specified in header.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
|
||||
public static IApplicationBuilder UseHttpMethodOverride(this IApplicationBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
return builder.UseMiddleware<HttpMethodOverrideMiddleware>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows incoming POST request to override method type with type specified in form.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
|
||||
/// <param name="options">The <see cref="HttpMethodOverrideOptions"/>.</param>
|
||||
public static IApplicationBuilder UseHttpMethodOverride(this IApplicationBuilder builder, HttpMethodOverrideOptions options)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
return builder.UseMiddleware<HttpMethodOverrideMiddleware>(Options.Create(options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpOverrides
|
||||
{
|
||||
public class HttpMethodOverrideMiddleware
|
||||
{
|
||||
private const string xHttpMethodOverride = "X-Http-Method-Override";
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly HttpMethodOverrideOptions _options;
|
||||
|
||||
public HttpMethodOverrideMiddleware(RequestDelegate next, IOptions<HttpMethodOverrideOptions> options)
|
||||
{
|
||||
if (next == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(next));
|
||||
}
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
_next = next;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
if (string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (_options.FormFieldName != null)
|
||||
{
|
||||
if (context.Request.HasFormContentType)
|
||||
{
|
||||
var form = await context.Request.ReadFormAsync();
|
||||
var methodType = form[_options.FormFieldName];
|
||||
if (!string.IsNullOrEmpty(methodType))
|
||||
{
|
||||
context.Request.Method = methodType;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var xHttpMethodOverrideValue = context.Request.Headers[xHttpMethodOverride];
|
||||
if (!string.IsNullOrEmpty(xHttpMethodOverrideValue))
|
||||
{
|
||||
context.Request.Method = xHttpMethodOverrideValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
public class HttpMethodOverrideOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Denotes the form element that contains the name of the resulting method type.
|
||||
/// If not set the X-Http-Method-Override header will be used.
|
||||
/// </summary>
|
||||
public string FormFieldName { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
// 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.Net;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpOverrides
|
||||
{
|
||||
public class IPNetwork
|
||||
{
|
||||
public IPNetwork(IPAddress prefix, int prefixLength)
|
||||
{
|
||||
Prefix = prefix;
|
||||
PrefixLength = prefixLength;
|
||||
PrefixBytes = Prefix.GetAddressBytes();
|
||||
Mask = CreateMask();
|
||||
}
|
||||
|
||||
public IPAddress Prefix { get; }
|
||||
|
||||
private byte[] PrefixBytes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The CIDR notation of the subnet mask
|
||||
/// </summary>
|
||||
public int PrefixLength { get; }
|
||||
|
||||
private byte[] Mask { get; }
|
||||
|
||||
public bool Contains(IPAddress address)
|
||||
{
|
||||
if (Prefix.AddressFamily != address.AddressFamily)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var addressBytes = address.GetAddressBytes();
|
||||
for (int i = 0; i < PrefixBytes.Length && Mask[i] != 0; i++)
|
||||
{
|
||||
if (PrefixBytes[i] != (addressBytes[i] & Mask[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private byte[] CreateMask()
|
||||
{
|
||||
var mask = new byte[PrefixBytes.Length];
|
||||
int remainingBits = PrefixLength;
|
||||
int i = 0;
|
||||
while (remainingBits >= 8)
|
||||
{
|
||||
mask[i] = 0xFF;
|
||||
i++;
|
||||
remainingBits -= 8;
|
||||
}
|
||||
if (remainingBits > 0)
|
||||
{
|
||||
mask[i] = (byte)(0xFF << (8 - remainingBits));
|
||||
}
|
||||
|
||||
return mask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
// 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.Net;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpOverrides.Internal
|
||||
{
|
||||
public static class IPEndPointParser
|
||||
{
|
||||
public static bool TryParse(string addressWithPort, out IPEndPoint endpoint)
|
||||
{
|
||||
string addressPart = null;
|
||||
string portPart = null;
|
||||
IPAddress address;
|
||||
endpoint = null;
|
||||
|
||||
if (string.IsNullOrEmpty(addressWithPort))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lastColonIndex = addressWithPort.LastIndexOf(':');
|
||||
if (lastColonIndex > 0)
|
||||
{
|
||||
// IPv4 with port or IPv6
|
||||
var closingIndex = addressWithPort.LastIndexOf(']');
|
||||
if (closingIndex > 0)
|
||||
{
|
||||
// IPv6 with brackets
|
||||
addressPart = addressWithPort.Substring(1, closingIndex - 1);
|
||||
if (closingIndex < lastColonIndex)
|
||||
{
|
||||
// IPv6 with port [::1]:80
|
||||
portPart = addressWithPort.Substring(lastColonIndex + 1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// IPv6 without port or IPv4
|
||||
var firstColonIndex = addressWithPort.IndexOf(':');
|
||||
if (firstColonIndex != lastColonIndex)
|
||||
{
|
||||
// IPv6 ::1
|
||||
addressPart = addressWithPort;
|
||||
}
|
||||
else
|
||||
{
|
||||
// IPv4 with port 127.0.0.1:123
|
||||
addressPart = addressWithPort.Substring(0, firstColonIndex);
|
||||
portPart = addressWithPort.Substring(firstColonIndex + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// IPv4 without port
|
||||
addressPart = addressWithPort;
|
||||
}
|
||||
|
||||
if (IPAddress.TryParse(addressPart, out address))
|
||||
{
|
||||
if (portPart != null)
|
||||
{
|
||||
int port;
|
||||
if (int.TryParse(portPart, out port))
|
||||
{
|
||||
endpoint = new IPEndPoint(address, port);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
endpoint = new IPEndPoint(address, 0);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>ASP.NET Core basic middleware for supporting HTTP method overrides. Includes:
|
||||
* X-Forwarded-* headers to forward headers from a proxy.
|
||||
* HTTP method override header.</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>aspnetcore;proxy;headers;xforwarded</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<Reference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,452 @@
|
|||
{
|
||||
"AssemblyIdentity": "Microsoft.AspNetCore.HttpOverrides, Version=1.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||
"Types": [
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.ForwardedHeadersExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseForwardedHeaders",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "builder",
|
||||
"Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseForwardedHeaders",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "builder",
|
||||
"Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
|
||||
},
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.AspNetCore.Builder.ForwardedHeadersOptions"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.ForwardedHeadersOptions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ForwardedHeaders",
|
||||
"Parameters": [],
|
||||
"ReturnType": "Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ForwardedHeaders",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ForwardLimit",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Nullable<System.Int32>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ForwardLimit",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Nullable<System.Int32>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_KnownProxies",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Collections.Generic.IList<System.Net.IPAddress>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_KnownNetworks",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Collections.Generic.IList<Microsoft.AspNetCore.HttpOverrides.IPNetwork>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_RequireHeaderSymmetry",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_RequireHeaderSymmetry",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Boolean"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.HttpMethodOverrideExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseHttpMethodOverride",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "builder",
|
||||
"Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseHttpMethodOverride",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "builder",
|
||||
"Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
|
||||
},
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.AspNetCore.Builder.HttpMethodOverrideOptions"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.HttpMethodOverrideOptions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_FormFieldName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_FormFieldName",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Enumeration",
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Field",
|
||||
"Name": "None",
|
||||
"Parameters": [],
|
||||
"GenericParameter": [],
|
||||
"Literal": "0"
|
||||
},
|
||||
{
|
||||
"Kind": "Field",
|
||||
"Name": "XForwardedFor",
|
||||
"Parameters": [],
|
||||
"GenericParameter": [],
|
||||
"Literal": "1"
|
||||
},
|
||||
{
|
||||
"Kind": "Field",
|
||||
"Name": "XForwardedHost",
|
||||
"Parameters": [],
|
||||
"GenericParameter": [],
|
||||
"Literal": "2"
|
||||
},
|
||||
{
|
||||
"Kind": "Field",
|
||||
"Name": "XForwardedProto",
|
||||
"Parameters": [],
|
||||
"GenericParameter": [],
|
||||
"Literal": "4"
|
||||
},
|
||||
{
|
||||
"Kind": "Field",
|
||||
"Name": "All",
|
||||
"Parameters": [],
|
||||
"GenericParameter": [],
|
||||
"Literal": "7"
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Invoke",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "ApplyForwarders",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "next",
|
||||
"Type": "Microsoft.AspNetCore.Http.RequestDelegate"
|
||||
},
|
||||
{
|
||||
"Name": "loggerFactory",
|
||||
"Type": "Microsoft.Extensions.Logging.ILoggerFactory"
|
||||
},
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Builder.ForwardedHeadersOptions>"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HttpOverrides.HttpMethodOverrideMiddleware",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Invoke",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "next",
|
||||
"Type": "Microsoft.AspNetCore.Http.RequestDelegate"
|
||||
},
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Builder.HttpMethodOverrideOptions>"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HttpOverrides.IPNetwork",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_Prefix",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Net.IPAddress",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_PrefixLength",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Int32",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Contains",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "address",
|
||||
"Type": "System.Net.IPAddress"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "prefix",
|
||||
"Type": "System.Net.IPAddress"
|
||||
},
|
||||
{
|
||||
"Name": "prefixLength",
|
||||
"Type": "System.Int32"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HttpOverrides.Internal.IPEndPointParser",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "TryParse",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "addressWithPort",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "endpoint",
|
||||
"Type": "System.Net.IPEndPoint",
|
||||
"Direction": "Out"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Static": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,634 @@
|
|||
{
|
||||
"AssemblyIdentity": "Microsoft.AspNetCore.HttpOverrides, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||
"Types": [
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.ForwardedHeadersExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseForwardedHeaders",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "builder",
|
||||
"Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseForwardedHeaders",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "builder",
|
||||
"Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
|
||||
},
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.AspNetCore.Builder.ForwardedHeadersOptions"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.ForwardedHeadersOptions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ForwardedForHeaderName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ForwardedForHeaderName",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ForwardedHostHeaderName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ForwardedHostHeaderName",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ForwardedProtoHeaderName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ForwardedProtoHeaderName",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_OriginalForHeaderName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_OriginalForHeaderName",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_OriginalHostHeaderName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_OriginalHostHeaderName",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_OriginalProtoHeaderName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_OriginalProtoHeaderName",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ForwardedHeaders",
|
||||
"Parameters": [],
|
||||
"ReturnType": "Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ForwardedHeaders",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ForwardLimit",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Nullable<System.Int32>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ForwardLimit",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Nullable<System.Int32>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_KnownProxies",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Collections.Generic.IList<System.Net.IPAddress>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_KnownNetworks",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Collections.Generic.IList<Microsoft.AspNetCore.HttpOverrides.IPNetwork>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_AllowedHosts",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Collections.Generic.IList<System.String>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_AllowedHosts",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Collections.Generic.IList<System.String>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_RequireHeaderSymmetry",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_RequireHeaderSymmetry",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Boolean"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.HttpMethodOverrideExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseHttpMethodOverride",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "builder",
|
||||
"Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseHttpMethodOverride",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "builder",
|
||||
"Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
|
||||
},
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.AspNetCore.Builder.HttpMethodOverrideOptions"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.HttpMethodOverrideOptions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_FormFieldName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_FormFieldName",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Enumeration",
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Field",
|
||||
"Name": "None",
|
||||
"Parameters": [],
|
||||
"GenericParameter": [],
|
||||
"Literal": "0"
|
||||
},
|
||||
{
|
||||
"Kind": "Field",
|
||||
"Name": "XForwardedFor",
|
||||
"Parameters": [],
|
||||
"GenericParameter": [],
|
||||
"Literal": "1"
|
||||
},
|
||||
{
|
||||
"Kind": "Field",
|
||||
"Name": "XForwardedHost",
|
||||
"Parameters": [],
|
||||
"GenericParameter": [],
|
||||
"Literal": "2"
|
||||
},
|
||||
{
|
||||
"Kind": "Field",
|
||||
"Name": "XForwardedProto",
|
||||
"Parameters": [],
|
||||
"GenericParameter": [],
|
||||
"Literal": "4"
|
||||
},
|
||||
{
|
||||
"Kind": "Field",
|
||||
"Name": "All",
|
||||
"Parameters": [],
|
||||
"GenericParameter": [],
|
||||
"Literal": "7"
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersDefaults",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_XForwardedForHeaderName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Static": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_XForwardedHostHeaderName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Static": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_XForwardedProtoHeaderName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Static": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_XOriginalForHeaderName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Static": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_XOriginalHostHeaderName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Static": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_XOriginalProtoHeaderName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Static": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Invoke",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "ApplyForwarders",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "next",
|
||||
"Type": "Microsoft.AspNetCore.Http.RequestDelegate"
|
||||
},
|
||||
{
|
||||
"Name": "loggerFactory",
|
||||
"Type": "Microsoft.Extensions.Logging.ILoggerFactory"
|
||||
},
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Builder.ForwardedHeadersOptions>"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HttpOverrides.HttpMethodOverrideMiddleware",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Invoke",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "next",
|
||||
"Type": "Microsoft.AspNetCore.Http.RequestDelegate"
|
||||
},
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Builder.HttpMethodOverrideOptions>"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HttpOverrides.IPNetwork",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_Prefix",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Net.IPAddress",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_PrefixLength",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Int32",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Contains",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "address",
|
||||
"Type": "System.Net.IPAddress"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "prefix",
|
||||
"Type": "System.Net.IPAddress"
|
||||
},
|
||||
{
|
||||
"Name": "prefixLength",
|
||||
"Type": "System.Int32"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,826 @@
|
|||
// 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.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpOverrides
|
||||
{
|
||||
public class ForwardedHeadersMiddlewareTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task XForwardedForDefaultSettingsChangeRemoteIpAndPort()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var context = await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-For"] = "11.111.111.11:9090";
|
||||
});
|
||||
|
||||
Assert.Equal("11.111.111.11", context.Connection.RemoteIpAddress.ToString());
|
||||
Assert.Equal(9090, context.Connection.RemotePort);
|
||||
// No Original set if RemoteIpAddress started null.
|
||||
Assert.False(context.Request.Headers.ContainsKey("X-Original-For"));
|
||||
// Should have been consumed and removed
|
||||
Assert.False(context.Request.Headers.ContainsKey("X-Forwarded-For"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, "11.111.111.11.12345", "10.0.0.1", 99)] // Invalid
|
||||
public async Task XForwardedForFirstValueIsInvalid(int limit, string header, string expectedIp, int expectedPort)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor,
|
||||
ForwardLimit = limit,
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var context = await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-For"] = header;
|
||||
c.Connection.RemoteIpAddress = IPAddress.Parse("10.0.0.1");
|
||||
c.Connection.RemotePort = 99;
|
||||
});
|
||||
|
||||
Assert.Equal(expectedIp, context.Connection.RemoteIpAddress.ToString());
|
||||
Assert.Equal(expectedPort, context.Connection.RemotePort);
|
||||
Assert.False(context.Request.Headers.ContainsKey("X-Original-For"));
|
||||
Assert.True(context.Request.Headers.ContainsKey("X-Forwarded-For"));
|
||||
Assert.Equal(header, context.Request.Headers["X-Forwarded-For"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, "11.111.111.11:12345", "11.111.111.11", 12345, "", false)]
|
||||
[InlineData(1, "11.111.111.11:12345", "11.111.111.11", 12345, "", true)]
|
||||
[InlineData(10, "11.111.111.11:12345", "11.111.111.11", 12345, "", false)]
|
||||
[InlineData(10, "11.111.111.11:12345", "11.111.111.11", 12345, "", true)]
|
||||
[InlineData(1, "12.112.112.12:23456, 11.111.111.11:12345", "11.111.111.11", 12345, "12.112.112.12:23456", false)]
|
||||
[InlineData(1, "12.112.112.12:23456, 11.111.111.11:12345", "11.111.111.11", 12345, "12.112.112.12:23456", true)]
|
||||
[InlineData(2, "12.112.112.12:23456, 11.111.111.11:12345", "12.112.112.12", 23456, "", false)]
|
||||
[InlineData(2, "12.112.112.12:23456, 11.111.111.11:12345", "12.112.112.12", 23456, "", true)]
|
||||
[InlineData(10, "12.112.112.12:23456, 11.111.111.11:12345", "12.112.112.12", 23456, "", false)]
|
||||
[InlineData(10, "12.112.112.12:23456, 11.111.111.11:12345", "12.112.112.12", 23456, "", true)]
|
||||
[InlineData(10, "12.112.112.12.23456, 11.111.111.11:12345", "11.111.111.11", 12345, "12.112.112.12.23456", false)] // Invalid 2nd value
|
||||
[InlineData(10, "12.112.112.12.23456, 11.111.111.11:12345", "11.111.111.11", 12345, "12.112.112.12.23456", true)] // Invalid 2nd value
|
||||
[InlineData(10, "13.113.113.13:34567, 12.112.112.12.23456, 11.111.111.11:12345", "11.111.111.11", 12345, "13.113.113.13:34567,12.112.112.12.23456", false)] // Invalid 2nd value
|
||||
[InlineData(10, "13.113.113.13:34567, 12.112.112.12.23456, 11.111.111.11:12345", "11.111.111.11", 12345, "13.113.113.13:34567,12.112.112.12.23456", true)] // Invalid 2nd value
|
||||
[InlineData(2, "13.113.113.13:34567, 12.112.112.12:23456, 11.111.111.11:12345", "12.112.112.12", 23456, "13.113.113.13:34567", false)]
|
||||
[InlineData(2, "13.113.113.13:34567, 12.112.112.12:23456, 11.111.111.11:12345", "12.112.112.12", 23456, "13.113.113.13:34567", true)]
|
||||
[InlineData(3, "13.113.113.13:34567, 12.112.112.12:23456, 11.111.111.11:12345", "13.113.113.13", 34567, "", false)]
|
||||
[InlineData(3, "13.113.113.13:34567, 12.112.112.12:23456, 11.111.111.11:12345", "13.113.113.13", 34567, "", true)]
|
||||
public async Task XForwardedForForwardLimit(int limit, string header, string expectedIp, int expectedPort, string remainingHeader, bool requireSymmetry)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
var options = new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor,
|
||||
RequireHeaderSymmetry = requireSymmetry,
|
||||
ForwardLimit = limit,
|
||||
};
|
||||
options.KnownProxies.Clear();
|
||||
options.KnownNetworks.Clear();
|
||||
app.UseForwardedHeaders(options);
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var context = await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-For"] = header;
|
||||
c.Connection.RemoteIpAddress = IPAddress.Parse("10.0.0.1");
|
||||
c.Connection.RemotePort = 99;
|
||||
});
|
||||
|
||||
Assert.Equal(expectedIp, context.Connection.RemoteIpAddress.ToString());
|
||||
Assert.Equal(expectedPort, context.Connection.RemotePort);
|
||||
Assert.Equal(remainingHeader, context.Request.Headers["X-Forwarded-For"].ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("11.111.111.11", false)]
|
||||
[InlineData("127.0.0.1", true)]
|
||||
[InlineData("127.0.1.1", true)]
|
||||
[InlineData("::1", true)]
|
||||
[InlineData("::", false)]
|
||||
public async Task XForwardedForLoopback(string originalIp, bool expectForwarded)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor,
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var context = await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-For"] = "10.0.0.1:1234";
|
||||
c.Connection.RemoteIpAddress = IPAddress.Parse(originalIp);
|
||||
c.Connection.RemotePort = 99;
|
||||
});
|
||||
|
||||
if (expectForwarded)
|
||||
{
|
||||
Assert.Equal("10.0.0.1", context.Connection.RemoteIpAddress.ToString());
|
||||
Assert.Equal(1234, context.Connection.RemotePort);
|
||||
Assert.True(context.Request.Headers.ContainsKey("X-Original-For"));
|
||||
Assert.Equal(new IPEndPoint(IPAddress.Parse(originalIp), 99).ToString(),
|
||||
context.Request.Headers["X-Original-For"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(originalIp, context.Connection.RemoteIpAddress.ToString());
|
||||
Assert.Equal(99, context.Connection.RemotePort);
|
||||
Assert.False(context.Request.Headers.ContainsKey("X-Original-For"));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, "11.111.111.11:12345", "20.0.0.1", "10.0.0.1", 99, false)]
|
||||
[InlineData(1, "11.111.111.11:12345", "20.0.0.1", "10.0.0.1", 99, true)]
|
||||
[InlineData(1, "", "10.0.0.1", "10.0.0.1", 99, false)]
|
||||
[InlineData(1, "", "10.0.0.1", "10.0.0.1", 99, true)]
|
||||
[InlineData(1, "11.111.111.11:12345", "10.0.0.1", "11.111.111.11", 12345, false)]
|
||||
[InlineData(1, "11.111.111.11:12345", "10.0.0.1", "11.111.111.11", 12345, true)]
|
||||
[InlineData(1, "12.112.112.12:23456, 11.111.111.11:12345", "10.0.0.1", "11.111.111.11", 12345, false)]
|
||||
[InlineData(1, "12.112.112.12:23456, 11.111.111.11:12345", "10.0.0.1", "11.111.111.11", 12345, true)]
|
||||
[InlineData(1, "12.112.112.12:23456, 11.111.111.11:12345", "10.0.0.1,11.111.111.11", "11.111.111.11", 12345, false)]
|
||||
[InlineData(1, "12.112.112.12:23456, 11.111.111.11:12345", "10.0.0.1,11.111.111.11", "11.111.111.11", 12345, true)]
|
||||
[InlineData(2, "12.112.112.12:23456, 11.111.111.11:12345", "10.0.0.1,11.111.111.11", "12.112.112.12", 23456, false)]
|
||||
[InlineData(2, "12.112.112.12:23456, 11.111.111.11:12345", "10.0.0.1,11.111.111.11", "12.112.112.12", 23456, true)]
|
||||
[InlineData(1, "12.112.112.12:23456, 11.111.111.11:12345", "10.0.0.1,11.111.111.11,12.112.112.12", "11.111.111.11", 12345, false)]
|
||||
[InlineData(1, "12.112.112.12:23456, 11.111.111.11:12345", "10.0.0.1,11.111.111.11,12.112.112.12", "11.111.111.11", 12345, true)]
|
||||
[InlineData(2, "12.112.112.12:23456, 11.111.111.11:12345", "10.0.0.1,11.111.111.11,12.112.112.12", "12.112.112.12", 23456, false)]
|
||||
[InlineData(2, "12.112.112.12:23456, 11.111.111.11:12345", "10.0.0.1,11.111.111.11,12.112.112.12", "12.112.112.12", 23456, true)]
|
||||
[InlineData(3, "13.113.113.13:34567, 12.112.112.12:23456, 11.111.111.11:12345", "10.0.0.1,11.111.111.11,12.112.112.12", "13.113.113.13", 34567, false)]
|
||||
[InlineData(3, "13.113.113.13:34567, 12.112.112.12:23456, 11.111.111.11:12345", "10.0.0.1,11.111.111.11,12.112.112.12", "13.113.113.13", 34567, true)]
|
||||
[InlineData(3, "13.113.113.13:34567, 12.112.112.12;23456, 11.111.111.11:12345", "10.0.0.1,11.111.111.11,12.112.112.12", "11.111.111.11", 12345, false)] // Invalid 2nd IP
|
||||
[InlineData(3, "13.113.113.13:34567, 12.112.112.12;23456, 11.111.111.11:12345", "10.0.0.1,11.111.111.11,12.112.112.12", "11.111.111.11", 12345, true)] // Invalid 2nd IP
|
||||
[InlineData(3, "13.113.113.13;34567, 12.112.112.12:23456, 11.111.111.11:12345", "10.0.0.1,11.111.111.11,12.112.112.12", "12.112.112.12", 23456, false)] // Invalid 3rd IP
|
||||
[InlineData(3, "13.113.113.13;34567, 12.112.112.12:23456, 11.111.111.11:12345", "10.0.0.1,11.111.111.11,12.112.112.12", "12.112.112.12", 23456, true)] // Invalid 3rd IP
|
||||
public async Task XForwardedForForwardKnownIps(int limit, string header, string knownIPs, string expectedIp, int expectedPort, bool requireSymmetry)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
var options = new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor,
|
||||
RequireHeaderSymmetry = requireSymmetry,
|
||||
ForwardLimit = limit,
|
||||
};
|
||||
foreach (var ip in knownIPs.Split(',').Select(text => IPAddress.Parse(text)))
|
||||
{
|
||||
options.KnownProxies.Add(ip);
|
||||
}
|
||||
app.UseForwardedHeaders(options);
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var context = await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-For"] = header;
|
||||
c.Connection.RemoteIpAddress = IPAddress.Parse("10.0.0.1");
|
||||
c.Connection.RemotePort = 99;
|
||||
});
|
||||
|
||||
Assert.Equal(expectedIp, context.Connection.RemoteIpAddress.ToString());
|
||||
Assert.Equal(expectedPort, context.Connection.RemotePort);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task XForwardedForOverrideBadIpDoesntChangeRemoteIp()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var context = await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-For"] = "BAD-IP";
|
||||
});
|
||||
|
||||
Assert.Null(context.Connection.RemoteIpAddress);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task XForwardedHostOverrideChangesRequestHost()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedHost
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var context = await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-Host"] = "testhost";
|
||||
});
|
||||
|
||||
Assert.Equal("testhost", context.Request.Host.ToString());
|
||||
}
|
||||
|
||||
public static TheoryData<string> HostHeaderData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string>() {
|
||||
"z",
|
||||
"1",
|
||||
"y:1",
|
||||
"1:1",
|
||||
"[ABCdef]",
|
||||
"[abcDEF]:0",
|
||||
"[abcdef:127.2355.1246.114]:0",
|
||||
"[::1]:80",
|
||||
"127.0.0.1:80",
|
||||
"900.900.900.900:9523547852",
|
||||
"foo",
|
||||
"foo:234",
|
||||
"foo.bar.baz",
|
||||
"foo.BAR.baz:46245",
|
||||
"foo.ba-ar.baz:46245",
|
||||
"-foo:1234",
|
||||
"xn--c1yn36f:134",
|
||||
"-",
|
||||
"_",
|
||||
"~",
|
||||
"!",
|
||||
"$",
|
||||
"'",
|
||||
"(",
|
||||
")",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(HostHeaderData))]
|
||||
public async Task XForwardedHostAllowsValidCharacters(string host)
|
||||
{
|
||||
var assertsExecuted = false;
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedHost
|
||||
});
|
||||
app.Run(context =>
|
||||
{
|
||||
Assert.Equal(host, context.Request.Host.ToString());
|
||||
assertsExecuted = true;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-Host"] = host;
|
||||
});
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
public static TheoryData<string> HostHeaderInvalidData
|
||||
{
|
||||
get
|
||||
{
|
||||
// see https://tools.ietf.org/html/rfc7230#section-5.4
|
||||
var data = new TheoryData<string>() {
|
||||
"", // Empty
|
||||
"[]", // Too short
|
||||
"[::]", // Too short
|
||||
"[ghijkl]", // Non-hex
|
||||
"[afd:adf:123", // Incomplete
|
||||
"[afd:adf]123", // Missing :
|
||||
"[afd:adf]:", // Missing port digits
|
||||
"[afd adf]", // Space
|
||||
"[ad-314]", // dash
|
||||
":1234", // Missing host
|
||||
"a:b:c", // Missing []
|
||||
"::1", // Missing []
|
||||
"::", // Missing everything
|
||||
"abcd:1abcd", // Letters in port
|
||||
"abcd:1.2", // Dot in port
|
||||
"1.2.3.4:", // Missing port digits
|
||||
"1.2 .4", // Space
|
||||
};
|
||||
|
||||
// These aren't allowed anywhere in the host header
|
||||
var invalid = "\"#%*+/;<=>?@[]\\^`{}|";
|
||||
foreach (var ch in invalid)
|
||||
{
|
||||
data.Add(ch.ToString());
|
||||
}
|
||||
|
||||
invalid = "!\"#$%&'()*+,/;<=>?@[]\\^_`{}|~-";
|
||||
foreach (var ch in invalid)
|
||||
{
|
||||
data.Add("[abd" + ch + "]:1234");
|
||||
}
|
||||
|
||||
invalid = "!\"#$%&'()*+/;<=>?@[]\\^_`{}|~:abcABC-.";
|
||||
foreach (var ch in invalid)
|
||||
{
|
||||
data.Add("a.b.c:" + ch);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(HostHeaderInvalidData))]
|
||||
public async Task XForwardedHostFailsForInvalidCharacters(string host)
|
||||
{
|
||||
var assertsExecuted = false;
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedHost
|
||||
});
|
||||
app.Run(context =>
|
||||
{
|
||||
Assert.NotEqual(host, context.Request.Host.Value);
|
||||
assertsExecuted = true;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-Host"] = host;
|
||||
});
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("localHost", "localhost")]
|
||||
[InlineData("localHost", "*")] // Any - Used by HttpSys
|
||||
[InlineData("localHost", "[::]")] // IPv6 Any - This is what Kestrel reports when binding to *
|
||||
[InlineData("localHost", "0.0.0.0")] // IPv4 Any
|
||||
[InlineData("localhost:9090", "example.com;localHost")]
|
||||
[InlineData("example.com:443", "example.com;localhost")]
|
||||
[InlineData("localHost:80", "localhost;")]
|
||||
[InlineData("foo.eXample.com:443", "*.exampLe.com")]
|
||||
[InlineData("f.eXample.com:443", "*.exampLe.com")]
|
||||
[InlineData("127.0.0.1", "127.0.0.1")]
|
||||
[InlineData("127.0.0.1:443", "127.0.0.1")]
|
||||
[InlineData("xn--c1yn36f:443", "xn--c1yn36f")]
|
||||
[InlineData("xn--c1yn36f:443", "點看")]
|
||||
[InlineData("[::ABC]", "[::aBc]")]
|
||||
[InlineData("[::1]:80", "[::1]")]
|
||||
public async Task XForwardedHostAllowsSpecifiedHost(string host, string allowedHost)
|
||||
{
|
||||
bool assertsExecuted = false;
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedHost,
|
||||
AllowedHosts = allowedHost.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
});
|
||||
app.Run(context =>
|
||||
{
|
||||
Assert.Equal(host, context.Request.Headers[HeaderNames.Host]);
|
||||
assertsExecuted = true;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var response = await server.SendAsync(ctx =>
|
||||
{
|
||||
ctx.Request.Headers["X-forwarded-Host"] = host;
|
||||
});
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("example.com", "localhost")]
|
||||
[InlineData("localhost:9090", "example.com;")]
|
||||
[InlineData(";", "example.com;localhost")]
|
||||
[InlineData(";:80", "example.com;localhost")]
|
||||
[InlineData(":80", "localhost")]
|
||||
[InlineData(":", "localhost")]
|
||||
[InlineData("example.com:443", "*.example.com")]
|
||||
[InlineData(".example.com:443", "*.example.com")]
|
||||
[InlineData("foo.com:443", "*.example.com")]
|
||||
[InlineData("foo.example.com.bar:443", "*.example.com")]
|
||||
[InlineData(".com:443", "*.com")]
|
||||
// Unicode in the host shouldn't be allowed without punycode anyways. This match fails because the middleware converts
|
||||
// its input to punycode.
|
||||
[InlineData("點看", "點看")]
|
||||
[InlineData("[::1", "[::1]")]
|
||||
[InlineData("[::1:80", "[::1]")]
|
||||
public async Task XForwardedHostFailsMismatchedHosts(string host, string allowedHost)
|
||||
{
|
||||
bool assertsExecuted = false;
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedHost,
|
||||
AllowedHosts = new[] { allowedHost }
|
||||
});
|
||||
app.Run(context =>
|
||||
{
|
||||
Assert.NotEqual<string>(host, context.Request.Headers[HeaderNames.Host]);
|
||||
assertsExecuted = true;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var response = await server.SendAsync(ctx =>
|
||||
{
|
||||
ctx.Request.Headers["X-forwarded-Host"] = host;
|
||||
});
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task XForwardedHostStopsAtFirstUnspecifiedHost()
|
||||
{
|
||||
bool assertsExecuted = false;
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedHost,
|
||||
ForwardLimit = 10,
|
||||
AllowedHosts = new[] { "bar.com", "*.foo.com" }
|
||||
});
|
||||
app.Run(context =>
|
||||
{
|
||||
Assert.Equal("bar.foo.com:432", context.Request.Headers[HeaderNames.Host]);
|
||||
assertsExecuted = true;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var response = await server.SendAsync(ctx =>
|
||||
{
|
||||
ctx.Request.Headers["X-forwarded-Host"] = "stuff:523, bar.foo.com:432, bar.com:80";
|
||||
});
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, "h1", "http")]
|
||||
[InlineData(1, "", "http")]
|
||||
[InlineData(1, "h1", "h1")]
|
||||
[InlineData(3, "h1", "h1")]
|
||||
[InlineData(1, "h2, h1", "h1")]
|
||||
[InlineData(2, "h2, h1", "h2")]
|
||||
[InlineData(10, "h3, h2, h1", "h3")]
|
||||
public async Task XForwardedProtoOverrideChangesRequestProtocol(int limit, string header, string expected)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedProto,
|
||||
ForwardLimit = limit,
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var context = await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-Proto"] = header;
|
||||
});
|
||||
|
||||
Assert.Equal(expected, context.Request.Scheme);
|
||||
}
|
||||
|
||||
public static TheoryData<string> ProtoHeaderData
|
||||
{
|
||||
get
|
||||
{
|
||||
// ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
|
||||
return new TheoryData<string>() {
|
||||
"z",
|
||||
"Z",
|
||||
"1",
|
||||
"y+",
|
||||
"1-",
|
||||
"a.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ProtoHeaderData))]
|
||||
public async Task XForwardedProtoAcceptsValidProtocols(string scheme)
|
||||
{
|
||||
var assertsExecuted = false;
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedProto
|
||||
});
|
||||
app.Run(context =>
|
||||
{
|
||||
Assert.Equal(scheme, context.Request.Scheme);
|
||||
assertsExecuted = true;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-Proto"] = scheme;
|
||||
});
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
public static TheoryData<string> ProtoHeaderInvalidData
|
||||
{
|
||||
get
|
||||
{
|
||||
// ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
|
||||
var data = new TheoryData<string>() {
|
||||
"a b", // Space
|
||||
};
|
||||
|
||||
// These aren't allowed anywhere in the scheme header
|
||||
var invalid = "!\"#$%&'()*/:;<=>?@[]\\^_`{}|~";
|
||||
foreach (var ch in invalid)
|
||||
{
|
||||
data.Add(ch.ToString());
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ProtoHeaderInvalidData))]
|
||||
public async Task XForwardedProtoRejectsInvalidProtocols(string scheme)
|
||||
{
|
||||
var assertsExecuted = false;
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedProto,
|
||||
});
|
||||
app.Run(context =>
|
||||
{
|
||||
Assert.Equal("http", context.Request.Scheme);
|
||||
assertsExecuted = true;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-Proto"] = scheme;
|
||||
});
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, "h1", "::1", "http")]
|
||||
[InlineData(1, "", "::1", "http")]
|
||||
[InlineData(1, "h1", "::1", "h1")]
|
||||
[InlineData(3, "h1", "::1", "h1")]
|
||||
[InlineData(3, "h2, h1", "::1", "http")]
|
||||
[InlineData(5, "h2, h1", "::1, ::1", "h2")]
|
||||
[InlineData(10, "h3, h2, h1", "::1, ::1, ::1", "h3")]
|
||||
[InlineData(10, "h3, h2, h1", "::1, badip, ::1", "h1")]
|
||||
public async Task XForwardedProtoOverrideLimitedByXForwardedForCount(int limit, string protoHeader, string forHeader, string expected)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor,
|
||||
RequireHeaderSymmetry = true,
|
||||
ForwardLimit = limit,
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var context = await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-Proto"] = protoHeader;
|
||||
c.Request.Headers["X-Forwarded-For"] = forHeader;
|
||||
});
|
||||
|
||||
Assert.Equal(expected, context.Request.Scheme);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, "h1", "::1", "http")]
|
||||
[InlineData(1, "", "::1", "http")]
|
||||
[InlineData(1, "h1", "", "h1")]
|
||||
[InlineData(1, "h1", "::1", "h1")]
|
||||
[InlineData(3, "h1", "::1", "h1")]
|
||||
[InlineData(3, "h1", "::1, ::1", "h1")]
|
||||
[InlineData(3, "h2, h1", "::1", "h2")]
|
||||
[InlineData(5, "h2, h1", "::1, ::1", "h2")]
|
||||
[InlineData(10, "h3, h2, h1", "::1, ::1, ::1", "h3")]
|
||||
[InlineData(10, "h3, h2, h1", "::1, badip, ::1", "h1")]
|
||||
public async Task XForwardedProtoOverrideCanBeIndependentOfXForwardedForCount(int limit, string protoHeader, string forHeader, string expected)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor,
|
||||
RequireHeaderSymmetry = false,
|
||||
ForwardLimit = limit,
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var context = await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-Proto"] = protoHeader;
|
||||
c.Request.Headers["X-Forwarded-For"] = forHeader;
|
||||
});
|
||||
|
||||
Assert.Equal(expected, context.Request.Scheme);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", "", "::1", false, "http")]
|
||||
[InlineData("h1", "", "::1", false, "http")]
|
||||
[InlineData("h1", "F::", "::1", false, "h1")]
|
||||
[InlineData("h1", "F::", "E::", false, "h1")]
|
||||
[InlineData("", "", "::1", true, "http")]
|
||||
[InlineData("h1", "", "::1", true, "http")]
|
||||
[InlineData("h1", "F::", "::1", true, "h1")]
|
||||
[InlineData("h1", "", "F::", true, "http")]
|
||||
[InlineData("h1", "E::", "F::", true, "http")]
|
||||
[InlineData("h2, h1", "", "::1", true, "http")]
|
||||
[InlineData("h2, h1", "F::, D::", "::1", true, "h1")]
|
||||
[InlineData("h2, h1", "E::, D::", "F::", true, "http")]
|
||||
public async Task XForwardedProtoOverrideLimitedByLoopback(string protoHeader, string forHeader, string remoteIp, bool loopback, string expected)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
var options = new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor,
|
||||
RequireHeaderSymmetry = true,
|
||||
ForwardLimit = 5,
|
||||
};
|
||||
if (!loopback)
|
||||
{
|
||||
options.KnownNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
}
|
||||
app.UseForwardedHeaders(options);
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var context = await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-Proto"] = protoHeader;
|
||||
c.Request.Headers["X-Forwarded-For"] = forHeader;
|
||||
c.Connection.RemoteIpAddress = IPAddress.Parse(remoteIp);
|
||||
});
|
||||
|
||||
Assert.Equal(expected, context.Request.Scheme);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllForwardsDisabledByDefault()
|
||||
{
|
||||
var options = new ForwardedHeadersOptions();
|
||||
Assert.True(options.ForwardedHeaders == ForwardedHeaders.None);
|
||||
Assert.Equal(1, options.ForwardLimit);
|
||||
Assert.Single(options.KnownNetworks);
|
||||
Assert.Single(options.KnownProxies);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllForwardsEnabledChangeRequestRemoteIpHostandProtocol()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.All
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var context = await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-Proto"] = "Protocol";
|
||||
c.Request.Headers["X-Forwarded-For"] = "11.111.111.11";
|
||||
c.Request.Headers["X-Forwarded-Host"] = "testhost";
|
||||
});
|
||||
|
||||
Assert.Equal("11.111.111.11", context.Connection.RemoteIpAddress.ToString());
|
||||
Assert.Equal("testhost", context.Request.Host.ToString());
|
||||
Assert.Equal("Protocol", context.Request.Scheme);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllOptionsDisabledRequestDoesntChange()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.None
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var context = await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-Proto"] = "Protocol";
|
||||
c.Request.Headers["X-Forwarded-For"] = "11.111.111.11";
|
||||
c.Request.Headers["X-Forwarded-Host"] = "otherhost";
|
||||
});
|
||||
|
||||
Assert.Null(context.Connection.RemoteIpAddress);
|
||||
Assert.Equal("localhost", context.Request.Host.ToString());
|
||||
Assert.Equal("http", context.Request.Scheme);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartiallyEnabledForwardsPartiallyChangesRequest()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var context = await server.SendAsync(c =>
|
||||
{
|
||||
c.Request.Headers["X-Forwarded-Proto"] = "Protocol";
|
||||
c.Request.Headers["X-Forwarded-For"] = "11.111.111.11";
|
||||
});
|
||||
|
||||
Assert.Equal("11.111.111.11", context.Connection.RemoteIpAddress.ToString());
|
||||
Assert.Equal("localhost", context.Request.Host.ToString());
|
||||
Assert.Equal("Protocol", context.Request.Scheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpOverrides
|
||||
{
|
||||
public class HttpMethodOverrideMiddlewareTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task XHttpMethodOverrideHeaderAvaiableChangesRequestMethod()
|
||||
{
|
||||
var assertsExecuted = false;
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpMethodOverride();
|
||||
app.Run(context =>
|
||||
{
|
||||
assertsExecuted = true;
|
||||
Assert.Equal("DELETE", context.Request.Method);
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, "");
|
||||
req.Headers.Add("X-Http-Method-Override", "DELETE");
|
||||
await server.CreateClient().SendAsync(req);
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task XHttpMethodOverrideHeaderUnavaiableDoesntChangeRequestMethod()
|
||||
{
|
||||
var assertsExecuted = false;
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpMethodOverride();
|
||||
app.Run(context =>
|
||||
{
|
||||
Assert.Equal("POST",context.Request.Method);
|
||||
assertsExecuted = true;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, "");
|
||||
await server.CreateClient().SendAsync(req);
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task XHttpMethodOverrideFromGetRequestDoesntChangeMethodType()
|
||||
{
|
||||
var assertsExecuted = false;
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpMethodOverride();
|
||||
app.Run(context =>
|
||||
{
|
||||
Assert.Equal("GET", context.Request.Method);
|
||||
assertsExecuted = true;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
await server.CreateClient().SendAsync(req);
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task FormFieldAvailableChangesRequestMethod()
|
||||
{
|
||||
var assertsExecuted = false;
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpMethodOverride(new HttpMethodOverrideOptions()
|
||||
{
|
||||
FormFieldName = "_METHOD"
|
||||
});
|
||||
app.Run(context =>
|
||||
{
|
||||
Assert.Equal("DELETE", context.Request.Method);
|
||||
assertsExecuted = true;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, "");
|
||||
req.Content = new FormUrlEncodedContent(new Dictionary<string, string>()
|
||||
{
|
||||
{ "_METHOD", "DELETE" }
|
||||
});
|
||||
|
||||
|
||||
await server.CreateClient().SendAsync(req);
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FormFieldUnavailableDoesNotChangeRequestMethod()
|
||||
{
|
||||
var assertsExecuted = false;
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpMethodOverride(new HttpMethodOverrideOptions()
|
||||
{
|
||||
FormFieldName = "_METHOD"
|
||||
});
|
||||
app.Run(context =>
|
||||
{
|
||||
Assert.Equal("POST", context.Request.Method);
|
||||
assertsExecuted = true;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, "");
|
||||
req.Content = new FormUrlEncodedContent(new Dictionary<string, string>()
|
||||
{
|
||||
});
|
||||
|
||||
|
||||
await server.CreateClient().SendAsync(req);
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FormFieldEmptyDoesNotChangeRequestMethod()
|
||||
{
|
||||
var assertsExecuted = false;
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpMethodOverride(new HttpMethodOverrideOptions()
|
||||
{
|
||||
FormFieldName = "_METHOD"
|
||||
});
|
||||
app.Run(context =>
|
||||
{
|
||||
Assert.Equal("POST", context.Request.Method);
|
||||
assertsExecuted = true;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, "");
|
||||
req.Content = new FormUrlEncodedContent(new Dictionary<string, string>()
|
||||
{
|
||||
{ "_METHOD", "" }
|
||||
});
|
||||
|
||||
|
||||
await server.CreateClient().SendAsync(req);
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
// 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.Net;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpOverrides.Internal
|
||||
{
|
||||
public class IPEndPointParserTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("127.0.0.1", "127.0.0.1", 0)]
|
||||
[InlineData("127.0.0.1:1", "127.0.0.1", 1)]
|
||||
[InlineData("1", "0.0.0.1", 0)]
|
||||
[InlineData("1:1", "0.0.0.1", 1)]
|
||||
[InlineData("::1", "::1", 0)]
|
||||
[InlineData("[::1]", "::1", 0)]
|
||||
[InlineData("[::1]:1", "::1", 1)]
|
||||
public void ParsesCorrectly(string input, string expectedAddress, int expectedPort)
|
||||
{
|
||||
IPEndPoint endpoint;
|
||||
var success = IPEndPointParser.TryParse(input, out endpoint);
|
||||
Assert.True(success);
|
||||
Assert.Equal(expectedAddress, endpoint.Address.ToString());
|
||||
Assert.Equal(expectedPort, endpoint.Port);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("[::1]:")]
|
||||
[InlineData("[::1:")]
|
||||
[InlineData("::1:")]
|
||||
[InlineData("127:")]
|
||||
[InlineData("127.0.0.1:")]
|
||||
[InlineData("")]
|
||||
[InlineData("[]")]
|
||||
[InlineData("]")]
|
||||
[InlineData("]:1")]
|
||||
public void ShouldNotParse(string input)
|
||||
{
|
||||
IPEndPoint endpoint;
|
||||
var success = IPEndPointParser.TryParse(input, out endpoint);
|
||||
Assert.False(success);
|
||||
Assert.Null(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// 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.Net;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpOverrides
|
||||
{
|
||||
public class IPNetworkTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("10.1.1.0", 8, "10.1.1.10")]
|
||||
[InlineData("174.0.0.0", 7, "175.1.1.10")]
|
||||
[InlineData("10.174.0.0", 15, "10.175.1.10")]
|
||||
[InlineData("10.168.0.0", 14, "10.171.1.10")]
|
||||
public void Contains_Positive(string prefixText, int length, string addressText)
|
||||
{
|
||||
var network = new IPNetwork(IPAddress.Parse(prefixText), length);
|
||||
Assert.True(network.Contains(IPAddress.Parse(addressText)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("10.1.0.0", 16, "10.2.1.10")]
|
||||
[InlineData("174.0.0.0", 7, "173.1.1.10")]
|
||||
[InlineData("10.174.0.0", 15, "10.173.1.10")]
|
||||
[InlineData("10.168.0.0", 14, "10.172.1.10")]
|
||||
public void Contains_Negative(string prefixText, int length, string addressText)
|
||||
{
|
||||
var network = new IPNetwork(IPAddress.Parse(prefixText), length);
|
||||
Assert.False(network.Contains(IPAddress.Parse(addressText)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.HttpOverrides" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Https" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Console" />
|
||||
<Reference Include="Microsoft.AspNetCore.HttpsPolicy" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:31894/",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"HttpsSample": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "http://localhost:5000/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpsPolicy;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HttpsSample
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddHttpsRedirection(options =>
|
||||
{
|
||||
options.RedirectStatusCode = StatusCodes.Status301MovedPermanently;
|
||||
options.HttpsPort = 5001;
|
||||
});
|
||||
|
||||
services.AddHsts(options =>
|
||||
{
|
||||
options.MaxAge = TimeSpan.FromDays(30);
|
||||
options.Preload = true;
|
||||
options.IncludeSubDomains = true;
|
||||
});
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IHostingEnvironment environment)
|
||||
{
|
||||
if (!environment.IsDevelopment())
|
||||
{
|
||||
app.UseHsts();
|
||||
}
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.Run(async context =>
|
||||
{
|
||||
await context.Response.WriteAsync("Hello world!");
|
||||
});
|
||||
}
|
||||
|
||||
// Entry point for the application.
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var host = new WebHostBuilder()
|
||||
.UseKestrel(
|
||||
options =>
|
||||
{
|
||||
options.Listen(new IPEndPoint(IPAddress.Loopback, 5001), listenOptions =>
|
||||
{
|
||||
listenOptions.UseHttps("testCert.pfx", "testPassword");
|
||||
});
|
||||
options.Listen(new IPEndPoint(IPAddress.Loopback, 5000), listenOptions =>
|
||||
{
|
||||
});
|
||||
})
|
||||
.UseContentRoot(Directory.GetCurrentDirectory()) // for the cert file
|
||||
.ConfigureLogging(factory =>
|
||||
{
|
||||
factory.SetMinimumLevel(LogLevel.Debug);
|
||||
factory.AddConsole();
|
||||
})
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
|
||||
host.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,30 @@
|
|||
// 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.HttpsPolicy;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the HSTS middleware.
|
||||
/// </summary>
|
||||
public static class HstsBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds middleware for using HSTS, which adds the Strict-Transport-Security header.
|
||||
/// </summary>
|
||||
/// <param name="app">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
|
||||
public static IApplicationBuilder UseHsts(this IApplicationBuilder app)
|
||||
{
|
||||
if (app == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(app));
|
||||
}
|
||||
|
||||
return app.UseMiddleware<HstsMiddleware>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpsPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables HTTP Strict Transport Security (HSTS)
|
||||
/// See https://tools.ietf.org/html/rfc6797.
|
||||
/// </summary>
|
||||
public class HstsMiddleware
|
||||
{
|
||||
private const string IncludeSubDomains = "; includeSubDomains";
|
||||
private const string Preload = "; preload";
|
||||
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly StringValues _strictTransportSecurityValue;
|
||||
private readonly IList<string> _excludedHosts;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the HSTS middleware.
|
||||
/// </summary>
|
||||
/// <param name="next"></param>
|
||||
/// <param name="options"></param>
|
||||
public HstsMiddleware(RequestDelegate next, IOptions<HstsOptions> options)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
_next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
|
||||
var hstsOptions = options.Value;
|
||||
var maxAge = Convert.ToInt64(Math.Floor(hstsOptions.MaxAge.TotalSeconds))
|
||||
.ToString(CultureInfo.InvariantCulture);
|
||||
var includeSubdomains = hstsOptions.IncludeSubDomains ? IncludeSubDomains : StringSegment.Empty;
|
||||
var preload = hstsOptions.Preload ? Preload : StringSegment.Empty;
|
||||
_strictTransportSecurityValue = new StringValues($"max-age={maxAge}{includeSubdomains}{preload}");
|
||||
_excludedHosts = hstsOptions.ExcludedHosts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the middleware.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="HttpContext"/>.</param>
|
||||
/// <returns></returns>
|
||||
public Task Invoke(HttpContext context)
|
||||
{
|
||||
if (context.Request.IsHttps && !IsHostExcluded(context.Request.Host.Host))
|
||||
{
|
||||
context.Response.Headers[HeaderNames.StrictTransportSecurity] = _strictTransportSecurityValue;
|
||||
}
|
||||
|
||||
return _next(context);
|
||||
}
|
||||
|
||||
private bool IsHostExcluded(string host)
|
||||
{
|
||||
if (_excludedHosts == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _excludedHosts.Count; i++)
|
||||
{
|
||||
if (string.Equals(host, _excludedHosts[i], StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
// 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.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpsPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the Hsts Middleware
|
||||
/// </summary>
|
||||
public class HstsOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the max-age parameter of the Strict-Transport-Security header.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Max-age is required; defaults to 30 days.
|
||||
/// See: https://tools.ietf.org/html/rfc6797#section-6.1.1
|
||||
/// </remarks>
|
||||
public TimeSpan MaxAge { get; set; } = TimeSpan.FromDays(30);
|
||||
|
||||
/// <summary>
|
||||
/// Enables includeSubDomain parameter of the Strict-Transport-Security header.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See: https://tools.ietf.org/html/rfc6797#section-6.1.2
|
||||
/// </remarks>
|
||||
public bool IncludeSubDomains { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the preload parameter of the Strict-Transport-Security header.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Preload is not part of the RFC specification, but is supported by web browsers
|
||||
/// to preload HSTS sites on fresh install. See https://hstspreload.org/.
|
||||
/// </remarks>
|
||||
public bool Preload { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of host names that will not add the HSTS header.
|
||||
/// </summary>
|
||||
public IList<string> ExcludedHosts { get; } = new List<string>
|
||||
{
|
||||
"localhost",
|
||||
"127.0.0.1", // ipv4
|
||||
"[::1]" // ipv6
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
// 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.HttpsPolicy;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the HSTS middleware.
|
||||
/// </summary>
|
||||
public static class HstsServicesExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds HSTS services.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
|
||||
/// <param name="configureOptions">A delegate to configure the <see cref="HstsOptions"/>.</param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddHsts(this IServiceCollection services, Action<HstsOptions> configureOptions)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
if (configureOptions == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configureOptions));
|
||||
}
|
||||
|
||||
services.Configure(configureOptions);
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
// 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.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.HttpsPolicy;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the HttpsRedirection middleware.
|
||||
/// </summary>
|
||||
public static class HttpsPolicyBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds middleware for redirecting HTTP Requests to HTTPS.
|
||||
/// </summary>
|
||||
/// <param name="app">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
|
||||
/// <returns>The <see cref="IApplicationBuilder"/> for HttpsRedirection.</returns>
|
||||
public static IApplicationBuilder UseHttpsRedirection(this IApplicationBuilder app)
|
||||
{
|
||||
if (app == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(app));
|
||||
}
|
||||
|
||||
var serverAddressFeature = app.ServerFeatures.Get<IServerAddressesFeature>();
|
||||
if (serverAddressFeature != null)
|
||||
{
|
||||
app.UseMiddleware<HttpsRedirectionMiddleware>(serverAddressFeature);
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseMiddleware<HttpsRedirectionMiddleware>();
|
||||
}
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.HttpsPolicy.Internal;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpsPolicy
|
||||
{
|
||||
public class HttpsRedirectionMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private bool _portEvaluated = false;
|
||||
private int? _httpsPort;
|
||||
private readonly int _statusCode;
|
||||
|
||||
private readonly IServerAddressesFeature _serverAddressesFeature;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the HttpsRedirectionMiddleware
|
||||
/// </summary>
|
||||
/// <param name="next"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="config"></param>
|
||||
/// <param name="loggerFactory"></param>
|
||||
public HttpsRedirectionMiddleware(RequestDelegate next, IOptions<HttpsRedirectionOptions> options, IConfiguration config, ILoggerFactory loggerFactory)
|
||||
|
||||
{
|
||||
_next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
var httpsRedirectionOptions = options.Value;
|
||||
_httpsPort = httpsRedirectionOptions.HttpsPort;
|
||||
_portEvaluated = _httpsPort.HasValue;
|
||||
_statusCode = httpsRedirectionOptions.RedirectStatusCode;
|
||||
_logger = loggerFactory.CreateLogger<HttpsRedirectionMiddleware>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the HttpsRedirectionMiddleware
|
||||
/// </summary>
|
||||
/// <param name="next"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="config"></param>
|
||||
/// <param name="loggerFactory"></param>
|
||||
/// <param name="serverAddressesFeature">The</param>
|
||||
public HttpsRedirectionMiddleware(RequestDelegate next, IOptions<HttpsRedirectionOptions> options, IConfiguration config, ILoggerFactory loggerFactory,
|
||||
IServerAddressesFeature serverAddressesFeature)
|
||||
: this(next, options, config, loggerFactory)
|
||||
{
|
||||
_serverAddressesFeature = serverAddressesFeature ?? throw new ArgumentNullException(nameof(serverAddressesFeature));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the HttpsRedirectionMiddleware
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public Task Invoke(HttpContext context)
|
||||
{
|
||||
if (context.Request.IsHttps || !TryGetHttpsPort(out var port))
|
||||
{
|
||||
return _next(context);
|
||||
}
|
||||
|
||||
var host = context.Request.Host;
|
||||
if (port != 443)
|
||||
{
|
||||
host = new HostString(host.Host, port);
|
||||
}
|
||||
else
|
||||
{
|
||||
host = new HostString(host.Host);
|
||||
}
|
||||
|
||||
var request = context.Request;
|
||||
var redirectUrl = UriHelper.BuildAbsolute(
|
||||
"https",
|
||||
host,
|
||||
request.PathBase,
|
||||
request.Path,
|
||||
request.QueryString);
|
||||
|
||||
context.Response.StatusCode = _statusCode;
|
||||
context.Response.Headers[HeaderNames.Location] = redirectUrl;
|
||||
|
||||
_logger.RedirectingToHttps(redirectUrl);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool TryGetHttpsPort(out int port)
|
||||
{
|
||||
// The IServerAddressesFeature will not be ready until the middleware is Invoked,
|
||||
// Order for finding the HTTPS port:
|
||||
// 1. Set in the HttpsRedirectionOptions
|
||||
// 2. HTTPS_PORT environment variable
|
||||
// 3. IServerAddressesFeature
|
||||
// 4. Fail if not set
|
||||
|
||||
port = -1;
|
||||
|
||||
if (_portEvaluated)
|
||||
{
|
||||
port = _httpsPort ?? port;
|
||||
return _httpsPort.HasValue;
|
||||
}
|
||||
_portEvaluated = true;
|
||||
|
||||
_httpsPort = _config.GetValue<int?>("HTTPS_PORT");
|
||||
if (_httpsPort.HasValue)
|
||||
{
|
||||
port = _httpsPort.Value;
|
||||
_logger.PortLoadedFromConfig(port);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_serverAddressesFeature == null)
|
||||
{
|
||||
_logger.FailedToDeterminePort();
|
||||
return false;
|
||||
}
|
||||
|
||||
int? httpsPort = null;
|
||||
foreach (var address in _serverAddressesFeature.Addresses)
|
||||
{
|
||||
var bindingAddress = BindingAddress.Parse(address);
|
||||
if (bindingAddress.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// If we find multiple different https ports specified, throw
|
||||
if (httpsPort.HasValue && httpsPort != bindingAddress.Port)
|
||||
{
|
||||
_logger.FailedMultiplePorts();
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
httpsPort = bindingAddress.Port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (httpsPort.HasValue)
|
||||
{
|
||||
_httpsPort = httpsPort;
|
||||
port = _httpsPort.Value;
|
||||
_logger.PortFromServer(port);
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.FailedToDeterminePort();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// 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 Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpsPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the HttpsRedirection middleware
|
||||
/// </summary>
|
||||
public class HttpsRedirectionOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The status code used for the redirect response. The default is 307.
|
||||
/// </summary>
|
||||
public int RedirectStatusCode { get; set; } = StatusCodes.Status307TemporaryRedirect;
|
||||
|
||||
/// <summary>
|
||||
/// The HTTPS port to be added to the redirected URL.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the HttpsPort is not set, we will try to get the HttpsPort from the following:
|
||||
/// 1. HTTPS_PORT environment variable
|
||||
/// 2. IServerAddressesFeature
|
||||
/// 3. 443 (or not set)
|
||||
/// </remarks>
|
||||
public int? HttpsPort { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// 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.HttpsPolicy;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the HttpsRedirection middleware.
|
||||
/// </summary>
|
||||
public static class HttpsRedirectionServicesExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds HTTPS redirection services.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
|
||||
/// <param name="configureOptions">A delegate to configure the <see cref="HttpsRedirectionOptions"/>.</param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddHttpsRedirection(this IServiceCollection services, Action<HttpsRedirectionOptions> configureOptions)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
if (configureOptions == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configureOptions));
|
||||
}
|
||||
services.Configure(configureOptions);
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
ASP.NET Core basic middleware for supporting HTTPS Redirection and HTTP Strict-Transport-Security.
|
||||
</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>aspnetcore;https;hsts</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Http" />
|
||||
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
|
||||
<Reference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<Reference Include="Microsoft.Extensions.Options" />
|
||||
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,378 @@
|
|||
{
|
||||
"AssemblyIdentity": "Microsoft.AspNetCore.HttpsPolicy, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||
"Types": [
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HttpsPolicy.HstsMiddleware",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Invoke",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "next",
|
||||
"Type": "Microsoft.AspNetCore.Http.RequestDelegate"
|
||||
},
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.HttpsPolicy.HstsOptions>"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HttpsPolicy.HstsOptions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_MaxAge",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.TimeSpan",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_MaxAge",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.TimeSpan"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_IncludeSubDomains",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_IncludeSubDomains",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Boolean"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_Preload",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_Preload",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Boolean"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ExcludedHosts",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Collections.Generic.IList<System.String>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Invoke",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "next",
|
||||
"Type": "Microsoft.AspNetCore.Http.RequestDelegate"
|
||||
},
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionOptions>"
|
||||
},
|
||||
{
|
||||
"Name": "config",
|
||||
"Type": "Microsoft.Extensions.Configuration.IConfiguration"
|
||||
},
|
||||
{
|
||||
"Name": "loggerFactory",
|
||||
"Type": "Microsoft.Extensions.Logging.ILoggerFactory"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "next",
|
||||
"Type": "Microsoft.AspNetCore.Http.RequestDelegate"
|
||||
},
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionOptions>"
|
||||
},
|
||||
{
|
||||
"Name": "config",
|
||||
"Type": "Microsoft.Extensions.Configuration.IConfiguration"
|
||||
},
|
||||
{
|
||||
"Name": "loggerFactory",
|
||||
"Type": "Microsoft.Extensions.Logging.ILoggerFactory"
|
||||
},
|
||||
{
|
||||
"Name": "serverAddressesFeature",
|
||||
"Type": "Microsoft.AspNetCore.Hosting.Server.Features.IServerAddressesFeature"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionOptions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_RedirectStatusCode",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Int32",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_RedirectStatusCode",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Int32"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_HttpsPort",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Nullable<System.Int32>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_HttpsPort",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Nullable<System.Int32>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.HstsBuilderExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseHsts",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "app",
|
||||
"Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.HstsServicesExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "AddHsts",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "services",
|
||||
"Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
|
||||
},
|
||||
{
|
||||
"Name": "configureOptions",
|
||||
"Type": "System.Action<Microsoft.AspNetCore.HttpsPolicy.HstsOptions>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.HttpsPolicyBuilderExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseHttpsRedirection",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "app",
|
||||
"Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.HttpsRedirectionServicesExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "AddHttpsRedirection",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "services",
|
||||
"Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
|
||||
},
|
||||
{
|
||||
"Name": "configureOptions",
|
||||
"Type": "System.Action<Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionOptions>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// 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.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpsPolicy.Internal
|
||||
{
|
||||
internal static class HttpsLoggingExtensions
|
||||
{
|
||||
private static readonly Action<ILogger, string, Exception> _redirectingToHttps;
|
||||
private static readonly Action<ILogger, int, Exception> _portLoadedFromConfig;
|
||||
private static readonly Action<ILogger, Exception> _failedToDeterminePort;
|
||||
private static readonly Action<ILogger, Exception> _failedMultiplePorts;
|
||||
private static readonly Action<ILogger, int, Exception> _portFromServer;
|
||||
|
||||
static HttpsLoggingExtensions()
|
||||
{
|
||||
_redirectingToHttps = LoggerMessage.Define<string>(LogLevel.Debug, 1, "Redirecting to '{redirect}'.");
|
||||
_portLoadedFromConfig = LoggerMessage.Define<int>(LogLevel.Debug, 2, "Https port '{port}' loaded from configuration.");
|
||||
_failedToDeterminePort = LoggerMessage.Define(LogLevel.Warning, 3, "Failed to determine the https port for redirect.");
|
||||
_failedMultiplePorts = LoggerMessage.Define(LogLevel.Warning, 4,
|
||||
"Cannot determine the https port from IServerAddressesFeature, multiple values were found. " +
|
||||
"Please set the desired port explicitly on HttpsRedirectionOptions.HttpsPort.");
|
||||
_portFromServer = LoggerMessage.Define<int>(LogLevel.Debug, 5, "Https port '{httpsPort}' discovered from server endpoints.");
|
||||
}
|
||||
|
||||
public static void RedirectingToHttps(this ILogger logger, string redirect)
|
||||
{
|
||||
_redirectingToHttps(logger, redirect, null);
|
||||
}
|
||||
|
||||
public static void PortLoadedFromConfig(this ILogger logger, int port)
|
||||
{
|
||||
_portLoadedFromConfig(logger, port, null);
|
||||
}
|
||||
|
||||
public static void FailedToDeterminePort(this ILogger logger)
|
||||
{
|
||||
_failedToDeterminePort(logger, null);
|
||||
}
|
||||
|
||||
public static void FailedMultiplePorts(this ILogger logger)
|
||||
{
|
||||
_failedMultiplePorts(logger, null);
|
||||
}
|
||||
|
||||
public static void PortFromServer(this ILogger logger, int port)
|
||||
{
|
||||
_portFromServer(logger, port, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpsPolicy.Tests
|
||||
{
|
||||
public class HstsMiddlewareTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SetOptionsWithDefault_SetsMaxAgeToCorrectValue()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHsts();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
client.BaseAddress = new Uri("https://example.com:5050");
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal("max-age=2592000", response.Headers.GetValues(HeaderNames.StrictTransportSecurity).FirstOrDefault());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, false, false, "max-age=0")]
|
||||
[InlineData(-1, false, false, "max-age=-1")]
|
||||
[InlineData(0, true, false, "max-age=0; includeSubDomains")]
|
||||
[InlineData(50000, false, true, "max-age=50000; preload")]
|
||||
[InlineData(0, true, true, "max-age=0; includeSubDomains; preload")]
|
||||
[InlineData(50000, true, true, "max-age=50000; includeSubDomains; preload")]
|
||||
public async Task SetOptionsThroughConfigure_SetsHeaderCorrectly(int maxAge, bool includeSubDomains, bool preload, string expected)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.Configure<HstsOptions>(options => {
|
||||
options.Preload = preload;
|
||||
options.IncludeSubDomains = includeSubDomains;
|
||||
options.MaxAge = TimeSpan.FromSeconds(maxAge);
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHsts();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
client.BaseAddress = new Uri("https://example.com:5050");
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(expected, response.Headers.GetValues(HeaderNames.StrictTransportSecurity).FirstOrDefault());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, false, false, "max-age=0")]
|
||||
[InlineData(-1, false, false, "max-age=-1")]
|
||||
[InlineData(0, true, false, "max-age=0; includeSubDomains")]
|
||||
[InlineData(50000, false, true, "max-age=50000; preload")]
|
||||
[InlineData(0, true, true, "max-age=0; includeSubDomains; preload")]
|
||||
[InlineData(50000, true, true, "max-age=50000; includeSubDomains; preload")]
|
||||
public async Task SetOptionsThroughHelper_SetsHeaderCorrectly(int maxAge, bool includeSubDomains, bool preload, string expected)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHsts(options => {
|
||||
options.Preload = preload;
|
||||
options.IncludeSubDomains = includeSubDomains;
|
||||
options.MaxAge = TimeSpan.FromSeconds(maxAge);
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHsts();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
client.BaseAddress = new Uri("https://example.com:5050");
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(expected, response.Headers.GetValues(HeaderNames.StrictTransportSecurity).FirstOrDefault());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("localhost")]
|
||||
[InlineData("Localhost")]
|
||||
[InlineData("LOCALHOST")]
|
||||
[InlineData("127.0.0.1")]
|
||||
[InlineData("[::1]")]
|
||||
public async Task DefaultExcludesCommonLocalhostDomains_DoesNotSetHstsHeader(string host)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHsts();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
client.BaseAddress = new Uri($"https://{host}:5050");
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Empty(response.Headers);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("localhost")]
|
||||
[InlineData("127.0.0.1")]
|
||||
[InlineData("[::1]")]
|
||||
public async Task AllowLocalhostDomainsIfListIsReset_SetHstsHeader(string host)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHsts(options =>
|
||||
{
|
||||
options.ExcludedHosts.Clear();
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHsts();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
client.BaseAddress = new Uri($"https://{host}:5050");
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Single(response.Headers);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("example.com")]
|
||||
[InlineData("Example.com")]
|
||||
[InlineData("EXAMPLE.COM")]
|
||||
public async Task AddExcludedDomains_DoesNotAddHstsHeader(string host)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHsts(options => {
|
||||
options.ExcludedHosts.Add(host);
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHsts();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
client.BaseAddress = new Uri($"https://{host}:5050");
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Empty(response.Headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// 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.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpsPolicy.Tests
|
||||
{
|
||||
public class HttpsPolicyTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(302, 443, 2592000, false, false, "max-age=2592000", "https://localhost/")]
|
||||
[InlineData(301, 5050, 2592000, false, false, "max-age=2592000", "https://localhost:5050/")]
|
||||
[InlineData(301, 443, 2592000, false, false, "max-age=2592000", "https://localhost/")]
|
||||
[InlineData(301, 443, 2592000, true, false, "max-age=2592000; includeSubDomains", "https://localhost/")]
|
||||
[InlineData(301, 443, 2592000, false, true, "max-age=2592000; preload", "https://localhost/")]
|
||||
[InlineData(301, 443, 2592000, true, true, "max-age=2592000; includeSubDomains; preload", "https://localhost/")]
|
||||
[InlineData(302, 5050, 2592000, true, true, "max-age=2592000; includeSubDomains; preload", "https://localhost:5050/")]
|
||||
public async Task SetsBothHstsAndHttpsRedirection_RedirectOnFirstRequest_HstsOnSecondRequest(int statusCode, int? tlsPort, int maxAge, bool includeSubDomains, bool preload, string expectedHstsHeader, string expectedUrl)
|
||||
{
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.Configure<HttpsRedirectionOptions>(options =>
|
||||
{
|
||||
options.RedirectStatusCode = statusCode;
|
||||
options.HttpsPort = tlsPort;
|
||||
});
|
||||
services.Configure<HstsOptions>(options =>
|
||||
{
|
||||
options.IncludeSubDomains = includeSubDomains;
|
||||
options.MaxAge = TimeSpan.FromSeconds(maxAge);
|
||||
options.Preload = preload;
|
||||
options.ExcludedHosts.Clear(); // allowing localhost for testing
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpsRedirection();
|
||||
app.UseHsts();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
|
||||
var featureCollection = new FeatureCollection();
|
||||
featureCollection.Set<IServerAddressesFeature>(new ServerAddressesFeature());
|
||||
var server = new TestServer(builder, featureCollection);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(statusCode, (int)response.StatusCode);
|
||||
Assert.Equal(expectedUrl, response.Headers.Location.ToString());
|
||||
|
||||
client = server.CreateClient();
|
||||
client.BaseAddress = new Uri(response.Headers.Location.ToString());
|
||||
request = new HttpRequestMessage(HttpMethod.Get, expectedUrl);
|
||||
response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(expectedHstsHeader, response.Headers.GetValues(HeaderNames.StrictTransportSecurity).FirstOrDefault());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,420 @@
|
|||
// 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.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.HttpsPolicy.Tests
|
||||
{
|
||||
public class HttpsRedirectionMiddlewareTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SetOptions_NotEnabledByDefault()
|
||||
{
|
||||
var sink = new TestSink(
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>,
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>);
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpsRedirection();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var logMessages = sink.Writes.ToList();
|
||||
|
||||
Assert.Single(logMessages);
|
||||
var message = logMessages.Single();
|
||||
Assert.Equal(LogLevel.Warning, message.LogLevel);
|
||||
Assert.Equal("Failed to determine the https port for redirect.", message.State.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(302, 5001, "https://localhost:5001/")]
|
||||
[InlineData(307, 1, "https://localhost:1/")]
|
||||
[InlineData(308, 3449, "https://localhost:3449/")]
|
||||
[InlineData(301, 5050, "https://localhost:5050/")]
|
||||
[InlineData(301, 443, "https://localhost/")]
|
||||
public async Task SetOptions_SetStatusCodeHttpsPort(int statusCode, int? httpsPort, string expected)
|
||||
{
|
||||
var sink = new TestSink(
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>,
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>);
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||
services.Configure<HttpsRedirectionOptions>(options =>
|
||||
{
|
||||
options.RedirectStatusCode = statusCode;
|
||||
options.HttpsPort = httpsPort;
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpsRedirection();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(statusCode, (int)response.StatusCode);
|
||||
Assert.Equal(expected, response.Headers.Location.ToString());
|
||||
|
||||
var logMessages = sink.Writes.ToList();
|
||||
|
||||
Assert.Single(logMessages);
|
||||
var message = logMessages.Single();
|
||||
Assert.Equal(LogLevel.Debug, message.LogLevel);
|
||||
Assert.Equal($"Redirecting to '{expected}'.", message.State.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(302, 5001, "https://localhost:5001/")]
|
||||
[InlineData(307, 1, "https://localhost:1/")]
|
||||
[InlineData(308, 3449, "https://localhost:3449/")]
|
||||
[InlineData(301, 5050, "https://localhost:5050/")]
|
||||
[InlineData(301, 443, "https://localhost/")]
|
||||
public async Task SetOptionsThroughHelperMethod_SetStatusCodeAndHttpsPort(int statusCode, int? httpsPort, string expectedUrl)
|
||||
{
|
||||
var sink = new TestSink(
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>,
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>);
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||
services.AddHttpsRedirection(options =>
|
||||
{
|
||||
options.RedirectStatusCode = statusCode;
|
||||
options.HttpsPort = httpsPort;
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpsRedirection();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(statusCode, (int)response.StatusCode);
|
||||
Assert.Equal(expectedUrl, response.Headers.Location.ToString());
|
||||
|
||||
var logMessages = sink.Writes.ToList();
|
||||
|
||||
Assert.Single(logMessages);
|
||||
var message = logMessages.Single();
|
||||
Assert.Equal(LogLevel.Debug, message.LogLevel);
|
||||
Assert.Equal($"Redirecting to '{expectedUrl}'.", message.State.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, null, "https://localhost:4444/", "https://localhost:4444/")]
|
||||
[InlineData(null, null, "https://localhost:443/", "https://localhost/")]
|
||||
[InlineData(null, null, "https://localhost/", "https://localhost/")]
|
||||
[InlineData(null, "5000", "https://localhost:4444/", "https://localhost:5000/")]
|
||||
[InlineData(null, "443", "https://localhost:4444/", "https://localhost/")]
|
||||
[InlineData(443, "5000", "https://localhost:4444/", "https://localhost/")]
|
||||
[InlineData(4000, "5000", "https://localhost:4444/", "https://localhost:4000/")]
|
||||
[InlineData(5000, null, "https://localhost:4444/", "https://localhost:5000/")]
|
||||
public async Task SetHttpsPortEnvironmentVariableAndServerFeature_ReturnsCorrectStatusCodeOnResponse(
|
||||
int? optionsHttpsPort, string configHttpsPort, string serverAddressFeatureUrl, string expectedUrl)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHttpsRedirection(options =>
|
||||
{
|
||||
options.HttpsPort = optionsHttpsPort;
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpsRedirection();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
|
||||
builder.UseSetting("HTTPS_PORT", configHttpsPort);
|
||||
|
||||
var featureCollection = new FeatureCollection();
|
||||
featureCollection.Set<IServerAddressesFeature>(new ServerAddressesFeature());
|
||||
|
||||
var server = new TestServer(builder, featureCollection);
|
||||
if (serverAddressFeatureUrl != null)
|
||||
{
|
||||
server.Features.Get<IServerAddressesFeature>().Addresses.Add(serverAddressFeatureUrl);
|
||||
}
|
||||
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(expectedUrl, response.Headers.Location.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetServerAddressesFeature_SingleHttpsAddress_Success()
|
||||
{
|
||||
var sink = new TestSink(
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>,
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>);
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpsRedirection();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
|
||||
var featureCollection = new FeatureCollection();
|
||||
featureCollection.Set<IServerAddressesFeature>(new ServerAddressesFeature());
|
||||
var server = new TestServer(builder, featureCollection);
|
||||
|
||||
server.Features.Get<IServerAddressesFeature>().Addresses.Add("https://localhost:5050");
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal("https://localhost:5050/", response.Headers.Location.ToString());
|
||||
|
||||
var logMessages = sink.Writes.ToList();
|
||||
|
||||
Assert.Equal(2, logMessages.Count);
|
||||
var message = logMessages.First();
|
||||
Assert.Equal(LogLevel.Debug, message.LogLevel);
|
||||
Assert.Equal("Https port '5050' discovered from server endpoints.", message.State.ToString());
|
||||
|
||||
message = logMessages.Skip(1).First();
|
||||
Assert.Equal(LogLevel.Debug, message.LogLevel);
|
||||
Assert.Equal("Redirecting to 'https://localhost:5050/'.", message.State.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetServerAddressesFeature_MultipleHttpsAddresses_LogsAndFailsToRedirect()
|
||||
{
|
||||
var sink = new TestSink(
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>,
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>);
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpsRedirection();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
|
||||
var featureCollection = new FeatureCollection();
|
||||
featureCollection.Set<IServerAddressesFeature>(new ServerAddressesFeature());
|
||||
var server = new TestServer(builder, featureCollection);
|
||||
|
||||
server.Features.Get<IServerAddressesFeature>().Addresses.Add("https://localhost:5050");
|
||||
server.Features.Get<IServerAddressesFeature>().Addresses.Add("https://localhost:5051");
|
||||
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
Assert.Equal(200, (int)response.StatusCode);
|
||||
|
||||
var logMessages = sink.Writes.ToList();
|
||||
|
||||
Assert.Single(logMessages);
|
||||
var message = logMessages.First();
|
||||
Assert.Equal(LogLevel.Warning, message.LogLevel);
|
||||
Assert.Equal("Cannot determine the https port from IServerAddressesFeature, multiple values were found. " +
|
||||
"Please set the desired port explicitly on HttpsRedirectionOptions.HttpsPort.", message.State.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetServerAddressesFeature_MultipleHttpsAddressesWithSamePort_Success()
|
||||
{
|
||||
var sink = new TestSink(
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>,
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>);
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpsRedirection();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
|
||||
var featureCollection = new FeatureCollection();
|
||||
featureCollection.Set<IServerAddressesFeature>(new ServerAddressesFeature());
|
||||
var server = new TestServer(builder, featureCollection);
|
||||
|
||||
server.Features.Get<IServerAddressesFeature>().Addresses.Add("https://localhost:5050");
|
||||
server.Features.Get<IServerAddressesFeature>().Addresses.Add("https://example.com:5050");
|
||||
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal("https://localhost:5050/", response.Headers.Location.ToString());
|
||||
|
||||
var logMessages = sink.Writes.ToList();
|
||||
|
||||
Assert.Equal(2, logMessages.Count);
|
||||
var message = logMessages.First();
|
||||
Assert.Equal(LogLevel.Debug, message.LogLevel);
|
||||
Assert.Equal("Https port '5050' discovered from server endpoints.", message.State.ToString());
|
||||
|
||||
message = logMessages.Skip(1).First();
|
||||
Assert.Equal(LogLevel.Debug, message.LogLevel);
|
||||
Assert.Equal("Redirecting to 'https://localhost:5050/'.", message.State.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoServerAddressFeature_DoesNotThrow_DoesNotRedirect()
|
||||
{
|
||||
var sink = new TestSink(
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>,
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>);
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpsRedirection();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
var response = await client.SendAsync(request);
|
||||
Assert.Equal(200, (int)response.StatusCode);
|
||||
|
||||
var logMessages = sink.Writes.ToList();
|
||||
|
||||
Assert.Single(logMessages);
|
||||
var message = logMessages.First();
|
||||
Assert.Equal(LogLevel.Warning, message.LogLevel);
|
||||
Assert.Equal("Failed to determine the https port for redirect.", message.State.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetNullAddressFeature_DoesNotThrow()
|
||||
{
|
||||
var sink = new TestSink(
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>,
|
||||
TestSink.EnableWithTypeName<HttpsRedirectionMiddleware>);
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<ILoggerFactory>(loggerFactory);
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHttpsRedirection();
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync("Hello world");
|
||||
});
|
||||
});
|
||||
|
||||
var featureCollection = new FeatureCollection();
|
||||
featureCollection.Set<IServerAddressesFeature>(null);
|
||||
var server = new TestServer(builder, featureCollection);
|
||||
|
||||
var client = server.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
var response = await client.SendAsync(request);
|
||||
Assert.Equal(200, (int)response.StatusCode);
|
||||
|
||||
var logMessages = sink.Writes.ToList();
|
||||
|
||||
Assert.Single(logMessages);
|
||||
var message = logMessages.First();
|
||||
Assert.Equal(LogLevel.Warning, message.LogLevel);
|
||||
Assert.Equal("Failed to determine the https port for redirect.", message.State.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1</TargetFrameworks>
|
||||
<TargetFrameworks Condition=" '$(DeveloperBuild)' != 'true' ">$(TargetFrameworks);netcoreapp2.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.HttpsPolicy" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Testing" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
using System.IO;
|
||||
using Microsoft.AspNetCore.ResponseCompression;
|
||||
|
||||
namespace ResponseCompressionSample
|
||||
{
|
||||
public class CustomCompressionProvider : ICompressionProvider
|
||||
{
|
||||
public string EncodingName => "custom";
|
||||
|
||||
public bool SupportsFlush => true;
|
||||
|
||||
public Stream CreateStream(Stream outputStream)
|
||||
{
|
||||
// Create a custom compression stream wrapper here
|
||||
return outputStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// 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.
|
||||
|
||||
namespace ResponseCompressionSample
|
||||
{
|
||||
internal static class LoremIpsum
|
||||
{
|
||||
internal const string Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit." +
|
||||
"Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar.Nulla sollicitudin.Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula.Pellentesque rhoncus nunc et augue.Integer id felis.Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas.Lorem ipsum dolor sit amet, consectetuer adipiscing elit.Morbi vel erat non mauris convallis vehicula.Nulla et sapien.Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam.Mauris ullamcorper felis vitae erat.Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna." +
|
||||
"Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien.Vivamus leo. Aliquam euismod libero eu enim.Nulla nec felis sed leo placerat imperdiet.Aenean suscipit nulla in justo.Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:6164/",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"ResponseCompressionSample": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "http://localhost:5000/",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="testfile1kb.txt" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.ResponseCompression" />
|
||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Console" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// 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.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.ResponseCompression;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ResponseCompressionSample
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.Configure<GzipCompressionProviderOptions>(options => options.Level = CompressionLevel.Fastest);
|
||||
services.AddResponseCompression(options =>
|
||||
{
|
||||
options.Providers.Add<GzipCompressionProvider>();
|
||||
options.Providers.Add<CustomCompressionProvider>();
|
||||
// .Append(TItem) is only available on Core.
|
||||
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "image/svg+xml" });
|
||||
});
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
|
||||
app.Map("/testfile1kb.txt", fileApp =>
|
||||
{
|
||||
fileApp.Run(context =>
|
||||
{
|
||||
context.Response.ContentType = "text/plain";
|
||||
return context.Response.SendFileAsync("testfile1kb.txt");
|
||||
});
|
||||
});
|
||||
|
||||
app.Map("/trickle", trickleApp =>
|
||||
{
|
||||
trickleApp.Run(async context =>
|
||||
{
|
||||
context.Response.ContentType = "text/plain";
|
||||
// Disables compression on net451 because that GZipStream does not implement Flush.
|
||||
context.Features.Get<IHttpBufferingFeature>()?.DisableResponseBuffering();
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
await context.Response.WriteAsync("a");
|
||||
await context.Response.Body.FlushAsync();
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.Run(async context =>
|
||||
{
|
||||
context.Response.ContentType = "text/plain";
|
||||
await context.Response.WriteAsync(LoremIpsum.Text);
|
||||
});
|
||||
}
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var host = new WebHostBuilder()
|
||||
.UseKestrel()
|
||||
.ConfigureLogging(factory =>
|
||||
{
|
||||
factory.AddConsole()
|
||||
.SetMinimumLevel(LogLevel.Debug);
|
||||
})
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
|
||||
host.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
|
|
@ -0,0 +1,326 @@
|
|||
// 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.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCompression
|
||||
{
|
||||
/// <summary>
|
||||
/// Stream wrapper that create specific compression stream only if necessary.
|
||||
/// </summary>
|
||||
internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature
|
||||
{
|
||||
private readonly HttpContext _context;
|
||||
private readonly Stream _bodyOriginalStream;
|
||||
private readonly IResponseCompressionProvider _provider;
|
||||
private readonly IHttpBufferingFeature _innerBufferFeature;
|
||||
private readonly IHttpSendFileFeature _innerSendFileFeature;
|
||||
|
||||
private ICompressionProvider _compressionProvider = null;
|
||||
private bool _compressionChecked = false;
|
||||
private Stream _compressionStream = null;
|
||||
private bool _providerCreated = false;
|
||||
private bool _autoFlush = false;
|
||||
|
||||
internal BodyWrapperStream(HttpContext context, Stream bodyOriginalStream, IResponseCompressionProvider provider,
|
||||
IHttpBufferingFeature innerBufferFeature, IHttpSendFileFeature innerSendFileFeature)
|
||||
{
|
||||
_context = context;
|
||||
_bodyOriginalStream = bodyOriginalStream;
|
||||
_provider = provider;
|
||||
_innerBufferFeature = innerBufferFeature;
|
||||
_innerSendFileFeature = innerSendFileFeature;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (_compressionStream != null)
|
||||
{
|
||||
_compressionStream.Dispose();
|
||||
_compressionStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanRead => false;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => _bodyOriginalStream.CanWrite;
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get { throw new NotSupportedException(); }
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get { throw new NotSupportedException(); }
|
||||
set { throw new NotSupportedException(); }
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
if (!_compressionChecked)
|
||||
{
|
||||
OnWrite();
|
||||
// Flush the original stream to send the headers. Flushing the compression stream won't
|
||||
// flush the original stream if no data has been written yet.
|
||||
_bodyOriginalStream.Flush();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_compressionStream != null)
|
||||
{
|
||||
_compressionStream.Flush();
|
||||
}
|
||||
else
|
||||
{
|
||||
_bodyOriginalStream.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_compressionChecked)
|
||||
{
|
||||
OnWrite();
|
||||
// Flush the original stream to send the headers. Flushing the compression stream won't
|
||||
// flush the original stream if no data has been written yet.
|
||||
return _bodyOriginalStream.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
if (_compressionStream != null)
|
||||
{
|
||||
return _compressionStream.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return _bodyOriginalStream.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
OnWrite();
|
||||
|
||||
if (_compressionStream != null)
|
||||
{
|
||||
_compressionStream.Write(buffer, offset, count);
|
||||
if (_autoFlush)
|
||||
{
|
||||
_compressionStream.Flush();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_bodyOriginalStream.Write(buffer, offset, count);
|
||||
}
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, Object state)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<object>(state);
|
||||
InternalWriteAsync(buffer, offset, count, callback, tcs);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private async void InternalWriteAsync(byte[] buffer, int offset, int count, AsyncCallback callback, TaskCompletionSource<object> tcs)
|
||||
{
|
||||
try
|
||||
{
|
||||
await WriteAsync(buffer, offset, count);
|
||||
tcs.TrySetResult(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
|
||||
if (callback != null)
|
||||
{
|
||||
// Offload callbacks to avoid stack dives on sync completions.
|
||||
var ignored = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
callback(tcs.Task);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Suppress exceptions on background threads.
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
if (asyncResult == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(asyncResult));
|
||||
}
|
||||
|
||||
var task = (Task)asyncResult;
|
||||
task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
OnWrite();
|
||||
|
||||
if (_compressionStream != null)
|
||||
{
|
||||
await _compressionStream.WriteAsync(buffer, offset, count, cancellationToken);
|
||||
if (_autoFlush)
|
||||
{
|
||||
await _compressionStream.FlushAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _bodyOriginalStream.WriteAsync(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWrite()
|
||||
{
|
||||
if (!_compressionChecked)
|
||||
{
|
||||
_compressionChecked = true;
|
||||
if (_provider.ShouldCompressResponse(_context))
|
||||
{
|
||||
// If the MIME type indicates that the response could be compressed, caches will need to vary by the Accept-Encoding header
|
||||
var varyValues = _context.Response.Headers.GetCommaSeparatedValues(HeaderNames.Vary);
|
||||
var varyByAcceptEncoding = false;
|
||||
|
||||
for (var i = 0; i < varyValues.Length; i++)
|
||||
{
|
||||
if (string.Equals(varyValues[i], HeaderNames.AcceptEncoding, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
varyByAcceptEncoding = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!varyByAcceptEncoding)
|
||||
{
|
||||
_context.Response.Headers.Append(HeaderNames.Vary, HeaderNames.AcceptEncoding);
|
||||
}
|
||||
|
||||
var compressionProvider = ResolveCompressionProvider();
|
||||
if (compressionProvider != null)
|
||||
{
|
||||
_context.Response.Headers.Append(HeaderNames.ContentEncoding, compressionProvider.EncodingName);
|
||||
_context.Response.Headers.Remove(HeaderNames.ContentMD5); // Reset the MD5 because the content changed.
|
||||
_context.Response.Headers.Remove(HeaderNames.ContentLength);
|
||||
|
||||
_compressionStream = compressionProvider.CreateStream(_bodyOriginalStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ICompressionProvider ResolveCompressionProvider()
|
||||
{
|
||||
if (!_providerCreated)
|
||||
{
|
||||
_providerCreated = true;
|
||||
_compressionProvider = _provider.GetCompressionProvider(_context);
|
||||
}
|
||||
|
||||
return _compressionProvider;
|
||||
}
|
||||
|
||||
public void DisableRequestBuffering()
|
||||
{
|
||||
// Unrelated
|
||||
_innerBufferFeature?.DisableRequestBuffering();
|
||||
}
|
||||
|
||||
// For this to be effective it needs to be called before the first write.
|
||||
public void DisableResponseBuffering()
|
||||
{
|
||||
if (ResolveCompressionProvider()?.SupportsFlush == false)
|
||||
{
|
||||
// Don't compress, some of the providers don't implement Flush (e.g. .NET 4.5.1 GZip/Deflate stream)
|
||||
// which would block real-time responses like SignalR.
|
||||
_compressionChecked = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_autoFlush = true;
|
||||
}
|
||||
_innerBufferFeature?.DisableResponseBuffering();
|
||||
}
|
||||
|
||||
// The IHttpSendFileFeature feature will only be registered if _innerSendFileFeature exists.
|
||||
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
|
||||
{
|
||||
OnWrite();
|
||||
|
||||
if (_compressionStream != null)
|
||||
{
|
||||
return InnerSendFileAsync(path, offset, count, cancellation);
|
||||
}
|
||||
|
||||
return _innerSendFileFeature.SendFileAsync(path, offset, count, cancellation);
|
||||
}
|
||||
|
||||
private async Task InnerSendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
|
||||
{
|
||||
cancellation.ThrowIfCancellationRequested();
|
||||
|
||||
var fileInfo = new FileInfo(path);
|
||||
if (offset < 0 || offset > fileInfo.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
|
||||
}
|
||||
if (count.HasValue &&
|
||||
(count.Value < 0 || count.Value > fileInfo.Length - offset))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
|
||||
}
|
||||
|
||||
int bufferSize = 1024 * 16;
|
||||
|
||||
var fileStream = new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite,
|
||||
bufferSize: bufferSize,
|
||||
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||
|
||||
using (fileStream)
|
||||
{
|
||||
fileStream.Seek(offset, SeekOrigin.Begin);
|
||||
await StreamCopyOperation.CopyToAsync(fileStream, _compressionStream, count, cancellation);
|
||||
|
||||
if (_autoFlush)
|
||||
{
|
||||
await _compressionStream.FlushAsync(cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// 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.Collections.ObjectModel;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCompression
|
||||
{
|
||||
/// <summary>
|
||||
/// A Collection of ICompressionProvider's that also allows them to be instantiated from an <see cref="IServiceProvider" />.
|
||||
/// </summary>
|
||||
public class CompressionProviderCollection : Collection<ICompressionProvider>
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a type representing an <see cref="ICompressionProvider"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Provider instances will be created using an <see cref="IServiceProvider" />.
|
||||
/// </remarks>
|
||||
public void Add<TCompressionProvider>() where TCompressionProvider : ICompressionProvider
|
||||
{
|
||||
Add(typeof(TCompressionProvider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a type representing an <see cref="ICompressionProvider"/>.
|
||||
/// </summary>
|
||||
/// <param name="providerType">Type representing an <see cref="ICompressionProvider"/>.</param>
|
||||
/// <remarks>
|
||||
/// Provider instances will be created using an <see cref="IServiceProvider" />.
|
||||
/// </remarks>
|
||||
public void Add(Type providerType)
|
||||
{
|
||||
if (providerType == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(providerType));
|
||||
}
|
||||
|
||||
if (!typeof(ICompressionProvider).IsAssignableFrom(providerType))
|
||||
{
|
||||
throw new ArgumentException($"The provider must implement {nameof(ICompressionProvider)}.", nameof(providerType));
|
||||
}
|
||||
|
||||
var factory = new CompressionProviderFactory(providerType);
|
||||
Add(factory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// 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.IO;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCompression
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a placeholder for the CompressionProviderCollection that allows creating the given type via
|
||||
/// an <see cref="IServiceProvider" />.
|
||||
/// </summary>
|
||||
internal class CompressionProviderFactory : ICompressionProvider
|
||||
{
|
||||
public CompressionProviderFactory(Type providerType)
|
||||
{
|
||||
ProviderType = providerType;
|
||||
}
|
||||
|
||||
private Type ProviderType { get; }
|
||||
|
||||
public ICompressionProvider CreateInstance(IServiceProvider serviceProvider)
|
||||
{
|
||||
if (serviceProvider == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serviceProvider));
|
||||
}
|
||||
|
||||
return (ICompressionProvider)ActivatorUtilities.CreateInstance(serviceProvider, ProviderType, Type.EmptyTypes);
|
||||
}
|
||||
|
||||
string ICompressionProvider.EncodingName
|
||||
{
|
||||
get { throw new NotSupportedException(); }
|
||||
}
|
||||
|
||||
bool ICompressionProvider.SupportsFlush
|
||||
{
|
||||
get { throw new NotSupportedException(); }
|
||||
}
|
||||
|
||||
Stream ICompressionProvider.CreateStream(Stream outputStream)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// 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.IO;
|
||||
using System.IO.Compression;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCompression
|
||||
{
|
||||
/// <summary>
|
||||
/// GZIP compression provider.
|
||||
/// </summary>
|
||||
public class GzipCompressionProvider : ICompressionProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of GzipCompressionProvider with options.
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
public GzipCompressionProvider(IOptions<GzipCompressionProviderOptions> options)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
Options = options.Value;
|
||||
}
|
||||
|
||||
private GzipCompressionProviderOptions Options { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string EncodingName => "gzip";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SupportsFlush
|
||||
{
|
||||
get
|
||||
{
|
||||
#if NET461
|
||||
return false;
|
||||
#elif NETSTANDARD2_0
|
||||
return true;
|
||||
#else
|
||||
#error target frameworks need to be updated
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Stream CreateStream(Stream outputStream)
|
||||
{
|
||||
return new GZipStream(outputStream, Options.Level, leaveOpen: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// 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.IO.Compression;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCompression
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the GzipCompressionProvider
|
||||
/// </summary>
|
||||
public class GzipCompressionProviderOptions : IOptions<GzipCompressionProviderOptions>
|
||||
{
|
||||
/// <summary>
|
||||
/// What level of compression to use for the stream. The default is Fastest.
|
||||
/// </summary>
|
||||
public CompressionLevel Level { get; set; } = CompressionLevel.Fastest;
|
||||
|
||||
/// <inheritdoc />
|
||||
GzipCompressionProviderOptions IOptions<GzipCompressionProviderOptions>.Value => this;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// 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.IO;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCompression
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a specific compression implementation to compress HTTP responses.
|
||||
/// </summary>
|
||||
public interface ICompressionProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// The encoding name used in the 'Accept-Encoding' request header and 'Content-Encoding' response header.
|
||||
/// </summary>
|
||||
string EncodingName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the given provider supports Flush and FlushAsync. If not, compression may be disabled in some scenarios.
|
||||
/// </summary>
|
||||
bool SupportsFlush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new compression stream.
|
||||
/// </summary>
|
||||
/// <param name="outputStream">The stream where the compressed data have to be written.</param>
|
||||
/// <returns>The compression stream.</returns>
|
||||
Stream CreateStream(Stream outputStream);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// 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 Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCompression
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to examine requests and responses to see if compression should be enabled.
|
||||
/// </summary>
|
||||
public interface IResponseCompressionProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Examines the request and selects an acceptable compression provider, if any.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns>A compression provider or null if compression should not be used.</returns>
|
||||
ICompressionProvider GetCompressionProvider(HttpContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Examines the response on first write to see if compression should be used.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
bool ShouldCompressResponse(HttpContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Examines the request to see if compression should be used for response.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
bool CheckRequestAcceptsCompression(HttpContext context);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>ASP.NET Core middleware for HTTP Response compression.</Description>
|
||||
<TargetFrameworks>net461;netstandard2.0</TargetFrameworks>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>aspnetcore</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
|
||||
<Reference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// 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.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.ResponseCompression.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// 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.ResponseCompression;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the ResponseCompression middleware.
|
||||
/// </summary>
|
||||
public static class ResponseCompressionBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds middleware for dynamically compressing HTTP Responses.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
|
||||
public static IApplicationBuilder UseResponseCompression(this IApplicationBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
return builder.UseMiddleware<ResponseCompressionMiddleware>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// 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.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCompression
|
||||
{
|
||||
/// <summary>
|
||||
/// Defaults for the ResponseCompressionMiddleware
|
||||
/// </summary>
|
||||
public class ResponseCompressionDefaults
|
||||
{
|
||||
/// <summary>
|
||||
/// Default MIME types to compress responses for.
|
||||
/// </summary>
|
||||
// This list is not intended to be exhaustive, it's a baseline for the 90% case.
|
||||
public static readonly IEnumerable<string> MimeTypes = new[]
|
||||
{
|
||||
// General
|
||||
"text/plain",
|
||||
// Static files
|
||||
"text/css",
|
||||
"application/javascript",
|
||||
// MVC
|
||||
"text/html",
|
||||
"application/xml",
|
||||
"text/xml",
|
||||
"application/json",
|
||||
"text/json",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCompression
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable HTTP response compression.
|
||||
/// </summary>
|
||||
public class ResponseCompressionMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
private readonly IResponseCompressionProvider _provider;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the Response Compression middleware.
|
||||
/// </summary>
|
||||
/// <param name="next"></param>
|
||||
/// <param name="provider"></param>
|
||||
public ResponseCompressionMiddleware(RequestDelegate next, IResponseCompressionProvider provider)
|
||||
{
|
||||
if (next == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(next));
|
||||
}
|
||||
if (provider == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(provider));
|
||||
}
|
||||
|
||||
_next = next;
|
||||
_provider = provider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the middleware.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
if (!_provider.CheckRequestAcceptsCompression(context))
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var bodyStream = context.Response.Body;
|
||||
var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>();
|
||||
var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>();
|
||||
|
||||
var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider,
|
||||
originalBufferFeature, originalSendFileFeature);
|
||||
context.Response.Body = bodyWrapperStream;
|
||||
context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream);
|
||||
if (originalSendFileFeature != null)
|
||||
{
|
||||
context.Features.Set<IHttpSendFileFeature>(bodyWrapperStream);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
// This is not disposed via a using statement because we don't want to flush the compression buffer for unhandled exceptions,
|
||||
// that may cause secondary exceptions.
|
||||
bodyWrapperStream.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Response.Body = bodyStream;
|
||||
context.Features.Set(originalBufferFeature);
|
||||
if (originalSendFileFeature != null)
|
||||
{
|
||||
context.Features.Set(originalSendFileFeature);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// 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.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCompression
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the HTTP response compression middleware.
|
||||
/// </summary>
|
||||
public class ResponseCompressionOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Response Content-Type MIME types to compress.
|
||||
/// </summary>
|
||||
public IEnumerable<string> MimeTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if responses over HTTPS connections should be compressed. The default is 'false'.
|
||||
/// Enable compression on HTTPS connections may expose security problems.
|
||||
/// </summary>
|
||||
public bool EnableForHttps { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The ICompressionProviders to use for responses.
|
||||
/// </summary>
|
||||
public CompressionProviderCollection Providers { get; } = new CompressionProviderCollection();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCompression
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class ResponseCompressionProvider : IResponseCompressionProvider
|
||||
{
|
||||
private readonly ICompressionProvider[] _providers;
|
||||
private readonly HashSet<string> _mimeTypes;
|
||||
private readonly bool _enableForHttps;
|
||||
|
||||
/// <summary>
|
||||
/// If no compression providers are specified then GZip is used by default.
|
||||
/// </summary>
|
||||
/// <param name="services">Services to use when instantiating compression providers.</param>
|
||||
/// <param name="options"></param>
|
||||
public ResponseCompressionProvider(IServiceProvider services, IOptions<ResponseCompressionOptions> options)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
_providers = options.Value.Providers.ToArray();
|
||||
if (_providers.Length == 0)
|
||||
{
|
||||
// Use the factory so it can resolve IOptions<GzipCompressionProviderOptions> from DI.
|
||||
_providers = new ICompressionProvider[] { new CompressionProviderFactory(typeof(GzipCompressionProvider)) };
|
||||
}
|
||||
for (var i = 0; i < _providers.Length; i++)
|
||||
{
|
||||
var factory = _providers[i] as CompressionProviderFactory;
|
||||
if (factory != null)
|
||||
{
|
||||
_providers[i] = factory.CreateInstance(services);
|
||||
}
|
||||
}
|
||||
|
||||
var mimeTypes = options.Value.MimeTypes;
|
||||
if (mimeTypes == null || !mimeTypes.Any())
|
||||
{
|
||||
mimeTypes = ResponseCompressionDefaults.MimeTypes;
|
||||
}
|
||||
_mimeTypes = new HashSet<string>(mimeTypes, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
_enableForHttps = options.Value.EnableForHttps;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual ICompressionProvider GetCompressionProvider(HttpContext context)
|
||||
{
|
||||
IList<StringWithQualityHeaderValue> unsorted;
|
||||
|
||||
// e.g. Accept-Encoding: gzip, deflate, sdch
|
||||
var accept = context.Request.Headers[HeaderNames.AcceptEncoding];
|
||||
if (!StringValues.IsNullOrEmpty(accept)
|
||||
&& StringWithQualityHeaderValue.TryParseList(accept, out unsorted)
|
||||
&& unsorted != null && unsorted.Count > 0)
|
||||
{
|
||||
// TODO PERF: clients don't usually include quality values so this sort will not have any effect. Fast-path?
|
||||
var sorted = unsorted
|
||||
.Where(s => s.Quality.GetValueOrDefault(1) > 0)
|
||||
.OrderByDescending(s => s.Quality.GetValueOrDefault(1));
|
||||
|
||||
foreach (var encoding in sorted)
|
||||
{
|
||||
// There will rarely be more than three providers, and there's only one by default
|
||||
foreach (var provider in _providers)
|
||||
{
|
||||
if (StringSegment.Equals(provider.EncodingName, encoding.Value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
// Uncommon but valid options
|
||||
if (StringSegment.Equals("*", encoding.Value, StringComparison.Ordinal))
|
||||
{
|
||||
// Any
|
||||
return _providers[0];
|
||||
}
|
||||
if (StringSegment.Equals("identity", encoding.Value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// No compression
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool ShouldCompressResponse(HttpContext context)
|
||||
{
|
||||
if (context.Response.Headers.ContainsKey(HeaderNames.ContentRange))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var mimeType = context.Response.ContentType;
|
||||
|
||||
if (string.IsNullOrEmpty(mimeType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var separator = mimeType.IndexOf(';');
|
||||
if (separator >= 0)
|
||||
{
|
||||
// Remove the content-type optional parameters
|
||||
mimeType = mimeType.Substring(0, separator);
|
||||
mimeType = mimeType.Trim();
|
||||
}
|
||||
|
||||
// TODO PERF: StringSegments?
|
||||
return _mimeTypes.Contains(mimeType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CheckRequestAcceptsCompression(HttpContext context)
|
||||
{
|
||||
if (context.Request.IsHttps && !_enableForHttps)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return !string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// 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.ResponseCompression;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the ResponseCompression middleware.
|
||||
/// </summary>
|
||||
public static class ResponseCompressionServicesExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add response compression services.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddResponseCompression(this IServiceCollection services)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
services.TryAddSingleton<IResponseCompressionProvider, ResponseCompressionProvider>();
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add response compression services and configure the related options.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
|
||||
/// <param name="configureOptions">A delegate to configure the <see cref="ResponseCompressionOptions"/>.</param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddResponseCompression(this IServiceCollection services, Action<ResponseCompressionOptions> configureOptions)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
if (configureOptions == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configureOptions));
|
||||
}
|
||||
|
||||
services.Configure(configureOptions);
|
||||
services.TryAddSingleton<IResponseCompressionProvider, ResponseCompressionProvider>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,509 @@
|
|||
{
|
||||
"AssemblyIdentity": "Microsoft.AspNetCore.ResponseCompression, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||
"Types": [
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.ResponseCompressionBuilderExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseResponseCompression",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "builder",
|
||||
"Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.ResponseCompressionServicesExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "AddResponseCompression",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "services",
|
||||
"Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "AddResponseCompression",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "services",
|
||||
"Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
|
||||
},
|
||||
{
|
||||
"Name": "configureOptions",
|
||||
"Type": "System.Action<Microsoft.AspNetCore.ResponseCompression.ResponseCompressionOptions>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.CompressionProviderCollection",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"BaseType": "System.Collections.ObjectModel.Collection<Microsoft.AspNetCore.ResponseCompression.ICompressionProvider>",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Add<T0>",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": [
|
||||
{
|
||||
"ParameterName": "TCompressionProvider",
|
||||
"ParameterPosition": 0,
|
||||
"BaseTypeOrInterfaces": [
|
||||
"Microsoft.AspNetCore.ResponseCompression.ICompressionProvider"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Add",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "providerType",
|
||||
"Type": "System.Type"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.GzipCompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [
|
||||
"Microsoft.AspNetCore.ResponseCompression.ICompressionProvider"
|
||||
],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_EncodingName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Sealed": true,
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "Microsoft.AspNetCore.ResponseCompression.ICompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_SupportsFlush",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Sealed": true,
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "Microsoft.AspNetCore.ResponseCompression.ICompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "CreateStream",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "outputStream",
|
||||
"Type": "System.IO.Stream"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.IO.Stream",
|
||||
"Sealed": true,
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "Microsoft.AspNetCore.ResponseCompression.ICompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.ResponseCompression.GzipCompressionProviderOptions>"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.GzipCompressionProviderOptions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [
|
||||
"Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.ResponseCompression.GzipCompressionProviderOptions>"
|
||||
],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_Level",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.IO.Compression.CompressionLevel",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_Level",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.IO.Compression.CompressionLevel"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.ICompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Interface",
|
||||
"Abstract": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_EncodingName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_SupportsFlush",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "CreateStream",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "outputStream",
|
||||
"Type": "System.IO.Stream"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.IO.Stream",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.IResponseCompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Interface",
|
||||
"Abstract": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "GetCompressionProvider",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.ResponseCompression.ICompressionProvider",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "ShouldCompressResponse",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Boolean",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "CheckRequestAcceptsCompression",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Boolean",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.ResponseCompressionDefaults",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Field",
|
||||
"Name": "MimeTypes",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Collections.Generic.IEnumerable<System.String>",
|
||||
"Static": true,
|
||||
"ReadOnly": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.ResponseCompressionMiddleware",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Invoke",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "next",
|
||||
"Type": "Microsoft.AspNetCore.Http.RequestDelegate"
|
||||
},
|
||||
{
|
||||
"Name": "provider",
|
||||
"Type": "Microsoft.AspNetCore.ResponseCompression.IResponseCompressionProvider"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.ResponseCompressionOptions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_MimeTypes",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Collections.Generic.IEnumerable<System.String>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_MimeTypes",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Collections.Generic.IEnumerable<System.String>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_EnableForHttps",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_EnableForHttps",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Boolean"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_Providers",
|
||||
"Parameters": [],
|
||||
"ReturnType": "Microsoft.AspNetCore.ResponseCompression.CompressionProviderCollection",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.ResponseCompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [
|
||||
"Microsoft.AspNetCore.ResponseCompression.IResponseCompressionProvider"
|
||||
],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "GetCompressionProvider",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.ResponseCompression.ICompressionProvider",
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "Microsoft.AspNetCore.ResponseCompression.IResponseCompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "ShouldCompressResponse",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "Microsoft.AspNetCore.ResponseCompression.IResponseCompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "CheckRequestAcceptsCompression",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Sealed": true,
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "Microsoft.AspNetCore.ResponseCompression.IResponseCompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "services",
|
||||
"Type": "System.IServiceProvider"
|
||||
},
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.ResponseCompression.ResponseCompressionOptions>"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,509 @@
|
|||
{
|
||||
"AssemblyIdentity": "Microsoft.AspNetCore.ResponseCompression, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||
"Types": [
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.ResponseCompressionBuilderExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseResponseCompression",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "builder",
|
||||
"Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.ResponseCompressionServicesExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "AddResponseCompression",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "services",
|
||||
"Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "AddResponseCompression",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "services",
|
||||
"Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
|
||||
},
|
||||
{
|
||||
"Name": "configureOptions",
|
||||
"Type": "System.Action<Microsoft.AspNetCore.ResponseCompression.ResponseCompressionOptions>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.CompressionProviderCollection",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"BaseType": "System.Collections.ObjectModel.Collection<Microsoft.AspNetCore.ResponseCompression.ICompressionProvider>",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Add<T0>",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": [
|
||||
{
|
||||
"ParameterName": "TCompressionProvider",
|
||||
"ParameterPosition": 0,
|
||||
"BaseTypeOrInterfaces": [
|
||||
"Microsoft.AspNetCore.ResponseCompression.ICompressionProvider"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Add",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "providerType",
|
||||
"Type": "System.Type"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.GzipCompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [
|
||||
"Microsoft.AspNetCore.ResponseCompression.ICompressionProvider"
|
||||
],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_EncodingName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Sealed": true,
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "Microsoft.AspNetCore.ResponseCompression.ICompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_SupportsFlush",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Sealed": true,
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "Microsoft.AspNetCore.ResponseCompression.ICompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "CreateStream",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "outputStream",
|
||||
"Type": "System.IO.Stream"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.IO.Stream",
|
||||
"Sealed": true,
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "Microsoft.AspNetCore.ResponseCompression.ICompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.ResponseCompression.GzipCompressionProviderOptions>"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.GzipCompressionProviderOptions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [
|
||||
"Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.ResponseCompression.GzipCompressionProviderOptions>"
|
||||
],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_Level",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.IO.Compression.CompressionLevel",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_Level",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.IO.Compression.CompressionLevel"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.ICompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Interface",
|
||||
"Abstract": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_EncodingName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_SupportsFlush",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "CreateStream",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "outputStream",
|
||||
"Type": "System.IO.Stream"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.IO.Stream",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.IResponseCompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Interface",
|
||||
"Abstract": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "GetCompressionProvider",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.ResponseCompression.ICompressionProvider",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "ShouldCompressResponse",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Boolean",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "CheckRequestAcceptsCompression",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Boolean",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.ResponseCompressionDefaults",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Field",
|
||||
"Name": "MimeTypes",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Collections.Generic.IEnumerable<System.String>",
|
||||
"Static": true,
|
||||
"ReadOnly": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.ResponseCompressionMiddleware",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Invoke",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "next",
|
||||
"Type": "Microsoft.AspNetCore.Http.RequestDelegate"
|
||||
},
|
||||
{
|
||||
"Name": "provider",
|
||||
"Type": "Microsoft.AspNetCore.ResponseCompression.IResponseCompressionProvider"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.ResponseCompressionOptions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_MimeTypes",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Collections.Generic.IEnumerable<System.String>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_MimeTypes",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Collections.Generic.IEnumerable<System.String>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_EnableForHttps",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_EnableForHttps",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Boolean"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_Providers",
|
||||
"Parameters": [],
|
||||
"ReturnType": "Microsoft.AspNetCore.ResponseCompression.CompressionProviderCollection",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.ResponseCompression.ResponseCompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [
|
||||
"Microsoft.AspNetCore.ResponseCompression.IResponseCompressionProvider"
|
||||
],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "GetCompressionProvider",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.ResponseCompression.ICompressionProvider",
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "Microsoft.AspNetCore.ResponseCompression.IResponseCompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "ShouldCompressResponse",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "Microsoft.AspNetCore.ResponseCompression.IResponseCompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "CheckRequestAcceptsCompression",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Http.HttpContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Sealed": true,
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "Microsoft.AspNetCore.ResponseCompression.IResponseCompressionProvider",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "services",
|
||||
"Type": "System.IServiceProvider"
|
||||
},
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.ResponseCompression.ResponseCompressionOptions>"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||
{
|
||||
public class BodyWrapperStreamTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null, "Accept-Encoding")]
|
||||
[InlineData("", "Accept-Encoding")]
|
||||
[InlineData("AnotherHeader", "AnotherHeader,Accept-Encoding")]
|
||||
[InlineData("Accept-Encoding", "Accept-Encoding")]
|
||||
[InlineData("accepT-encodinG", "accepT-encodinG")]
|
||||
[InlineData("accept-encoding,AnotherHeader", "accept-encoding,AnotherHeader")]
|
||||
public void OnWrite_AppendsAcceptEncodingToVaryHeader_IfNotPresent(string providedVaryHeader, string expectedVaryHeader)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Response.Headers[HeaderNames.Vary] = providedVaryHeader;
|
||||
var stream = new BodyWrapperStream(httpContext, new MemoryStream(), new MockResponseCompressionProvider(flushable: true), null, null);
|
||||
|
||||
stream.Write(new byte[] { }, 0, 0);
|
||||
|
||||
|
||||
Assert.Equal(expectedVaryHeader, httpContext.Response.Headers[HeaderNames.Vary]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void Write_IsPassedToUnderlyingStream_WhenDisableResponseBuffering(bool flushable)
|
||||
{
|
||||
|
||||
var buffer = new byte[] { 1 };
|
||||
byte[] written = null;
|
||||
|
||||
var mock = new Mock<Stream>();
|
||||
mock.SetupGet(s => s.CanWrite).Returns(true);
|
||||
mock.Setup(s => s.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()))
|
||||
.Callback<byte[], int, int>((b, o, c) =>
|
||||
{
|
||||
written = new ArraySegment<byte>(b, 0, c).ToArray();
|
||||
});
|
||||
|
||||
var stream = new BodyWrapperStream(new DefaultHttpContext(), mock.Object, new MockResponseCompressionProvider(flushable), null, null);
|
||||
|
||||
stream.DisableResponseBuffering();
|
||||
stream.Write(buffer, 0, buffer.Length);
|
||||
|
||||
Assert.Equal(buffer, written);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task WriteAsync_IsPassedToUnderlyingStream_WhenDisableResponseBuffering(bool flushable)
|
||||
{
|
||||
var buffer = new byte[] { 1 };
|
||||
|
||||
var memoryStream = new MemoryStream();
|
||||
var stream = new BodyWrapperStream(new DefaultHttpContext(), memoryStream, new MockResponseCompressionProvider(flushable), null, null);
|
||||
|
||||
stream.DisableResponseBuffering();
|
||||
await stream.WriteAsync(buffer, 0, buffer.Length);
|
||||
|
||||
Assert.Equal(buffer, memoryStream.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendFileAsync_IsPassedToUnderlyingStream_WhenDisableResponseBuffering()
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
var stream = new BodyWrapperStream(new DefaultHttpContext(), memoryStream, new MockResponseCompressionProvider(true), null, null);
|
||||
|
||||
stream.DisableResponseBuffering();
|
||||
|
||||
var path = "testfile1kb.txt";
|
||||
await stream.SendFileAsync(path, 0, null, CancellationToken.None);
|
||||
|
||||
Assert.Equal(File.ReadAllBytes(path), memoryStream.ToArray());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void BeginWrite_IsPassedToUnderlyingStream_WhenDisableResponseBuffering(bool flushable)
|
||||
{
|
||||
var buffer = new byte[] { 1 };
|
||||
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
var stream = new BodyWrapperStream(new DefaultHttpContext(), memoryStream, new MockResponseCompressionProvider(flushable), null, null);
|
||||
|
||||
stream.DisableResponseBuffering();
|
||||
stream.BeginWrite(buffer, 0, buffer.Length, (o) => {}, null);
|
||||
|
||||
Assert.Equal(buffer, memoryStream.ToArray());
|
||||
}
|
||||
|
||||
private class MockResponseCompressionProvider: IResponseCompressionProvider
|
||||
{
|
||||
private readonly bool _flushable;
|
||||
|
||||
public MockResponseCompressionProvider(bool flushable)
|
||||
{
|
||||
_flushable = flushable;
|
||||
}
|
||||
|
||||
public ICompressionProvider GetCompressionProvider(HttpContext context)
|
||||
{
|
||||
return new MockCompressionProvider(_flushable);
|
||||
}
|
||||
|
||||
public bool ShouldCompressResponse(HttpContext context)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CheckRequestAcceptsCompression(HttpContext context)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class MockCompressionProvider : ICompressionProvider
|
||||
{
|
||||
public MockCompressionProvider(bool flushable)
|
||||
{
|
||||
SupportsFlush = flushable;
|
||||
}
|
||||
|
||||
public string EncodingName { get; }
|
||||
|
||||
public bool SupportsFlush { get; }
|
||||
|
||||
public Stream CreateStream(Stream outputStream)
|
||||
{
|
||||
if (SupportsFlush)
|
||||
{
|
||||
return new BufferedStream(outputStream);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new NoFlushBufferedStream(outputStream);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private class NoFlushBufferedStream : Stream
|
||||
{
|
||||
private readonly BufferedStream _bufferedStream;
|
||||
|
||||
public NoFlushBufferedStream(Stream outputStream)
|
||||
{
|
||||
_bufferedStream = new BufferedStream(outputStream);
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) => _bufferedStream.Read(buffer, offset, count);
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => _bufferedStream.Seek(offset, origin);
|
||||
|
||||
public override void SetLength(long value) => _bufferedStream.SetLength(value);
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) => _bufferedStream.Write(buffer, offset, count);
|
||||
|
||||
public override bool CanRead => _bufferedStream.CanRead;
|
||||
|
||||
public override bool CanSeek => _bufferedStream.CanSeek;
|
||||
|
||||
public override bool CanWrite => _bufferedStream.CanWrite;
|
||||
|
||||
public override long Length => _bufferedStream.Length;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get { return _bufferedStream.Position; }
|
||||
set { _bufferedStream.Position = value; }
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
_bufferedStream.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="testfile1kb.txt" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.ResponseCompression" />
|
||||
<Reference Include="Microsoft.AspNetCore.Http" />
|
||||
<Reference Include="Microsoft.Net.Http.Headers" />
|
||||
<Reference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,862 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||
{
|
||||
public class ResponseCompressionMiddlewareTest
|
||||
{
|
||||
private const string TextPlain = "text/plain";
|
||||
|
||||
[Fact]
|
||||
public void Options_HttpsDisabledByDefault()
|
||||
{
|
||||
var options = new ResponseCompressionOptions();
|
||||
|
||||
Assert.False(options.EnableForHttps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Request_NoAcceptEncoding_Uncompressed()
|
||||
{
|
||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: null, responseType: TextPlain);
|
||||
|
||||
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Request_AcceptGzipDeflate_CompressedGzip()
|
||||
{
|
||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "gzip", "deflate" }, responseType: TextPlain);
|
||||
|
||||
CheckResponseCompressed(response, expectedBodyLength: 24);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Request_AcceptUnknown_NotCompressed()
|
||||
{
|
||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "unknown" }, responseType: TextPlain);
|
||||
|
||||
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("text/plain")]
|
||||
[InlineData("text/PLAIN")]
|
||||
[InlineData("text/plain; charset=ISO-8859-4")]
|
||||
[InlineData("text/plain ; charset=ISO-8859-4")]
|
||||
public async Task ContentType_WithCharset_Compress(string contentType)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
app.Run(context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = contentType;
|
||||
return context.Response.WriteAsync(new string('a', 100));
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
CheckResponseCompressed(response, expectedBodyLength: 24);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GZipCompressionProvider_OptionsSetInDI_Compress()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.Configure<GzipCompressionProviderOptions>(options => options.Level = CompressionLevel.NoCompression);
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
app.Run(context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = TextPlain;
|
||||
return context.Response.WriteAsync(new string('a', 100));
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
CheckResponseCompressed(response, expectedBodyLength: 123);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("text/plain2")]
|
||||
public async Task MimeTypes_OtherContentTypes_NoMatch(string contentType)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
app.Run(context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = contentType;
|
||||
return context.Response.WriteAsync(new string('a', 100));
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("text/plain")]
|
||||
[InlineData("text/PLAIN")]
|
||||
[InlineData("text/plain; charset=ISO-8859-4")]
|
||||
[InlineData("text/plain ; charset=ISO-8859-4")]
|
||||
[InlineData("text/plain2")]
|
||||
public async Task NoBody_NotCompressed(string contentType)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
app.Run(context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = contentType;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
CheckResponseNotCompressed(response, expectedBodyLength: 0, sendVaryHeader: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Request_AcceptStar_Compressed()
|
||||
{
|
||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "*" }, responseType: TextPlain);
|
||||
|
||||
CheckResponseCompressed(response, expectedBodyLength: 24);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Request_AcceptIdentity_NotCompressed()
|
||||
{
|
||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "identity" }, responseType: TextPlain);
|
||||
|
||||
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new string[] { "identity;q=0.5", "gzip;q=1" }, 24)]
|
||||
[InlineData(new string[] { "identity;q=0", "gzip;q=0.8" }, 24)]
|
||||
[InlineData(new string[] { "identity;q=0.5", "gzip" }, 24)]
|
||||
public async Task Request_AcceptWithHigherCompressionQuality_Compressed(string[] acceptEncodings, int expectedBodyLength)
|
||||
{
|
||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: acceptEncodings, responseType: TextPlain);
|
||||
|
||||
CheckResponseCompressed(response, expectedBodyLength: expectedBodyLength);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new string[] { "gzip;q=0.5", "identity;q=0.8" }, 100)]
|
||||
public async Task Request_AcceptWithhigherIdentityQuality_NotCompressed(string[] acceptEncodings, int expectedBodyLength)
|
||||
{
|
||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: acceptEncodings, responseType: TextPlain);
|
||||
|
||||
CheckResponseNotCompressed(response, expectedBodyLength: expectedBodyLength, sendVaryHeader: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Response_UnknownMimeType_NotCompressed()
|
||||
{
|
||||
var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "gzip" }, responseType: "text/custom");
|
||||
|
||||
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Response_WithContentRange_NotCompressed()
|
||||
{
|
||||
var response = await InvokeMiddleware(50, requestAcceptEncodings: new string[] { "gzip" }, responseType: TextPlain, addResponseAction: (r) =>
|
||||
{
|
||||
r.Headers[HeaderNames.ContentRange] = "1-2/*";
|
||||
});
|
||||
|
||||
CheckResponseNotCompressed(response, expectedBodyLength: 50, sendVaryHeader: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Response_WithContentEncodingAlreadySet_Stacked()
|
||||
{
|
||||
var otherContentEncoding = "something";
|
||||
|
||||
var response = await InvokeMiddleware(50, requestAcceptEncodings: new string[] { "gzip" }, responseType: TextPlain, addResponseAction: (r) =>
|
||||
{
|
||||
r.Headers[HeaderNames.ContentEncoding] = otherContentEncoding;
|
||||
});
|
||||
|
||||
Assert.True(response.Content.Headers.ContentEncoding.Contains(otherContentEncoding));
|
||||
Assert.True(response.Content.Headers.ContentEncoding.Contains("gzip"));
|
||||
Assert.Equal(24, response.Content.Headers.ContentLength);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, 100)]
|
||||
[InlineData(true, 24)]
|
||||
public async Task Request_Https_CompressedIfEnabled(bool enableHttps, int expectedLength)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression(options =>
|
||||
{
|
||||
options.EnableForHttps = enableHttps;
|
||||
options.MimeTypes = new[] { TextPlain };
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
app.Run(context =>
|
||||
{
|
||||
context.Response.ContentType = TextPlain;
|
||||
return context.Response.WriteAsync(new string('a', 100));
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
server.BaseAddress = new Uri("https://localhost/");
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
Assert.Equal(expectedLength, response.Content.ReadAsByteArrayAsync().Result.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FlushHeaders_SendsHeaders_Compresses()
|
||||
{
|
||||
var responseReceived = new ManualResetEvent(false);
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
app.Run(context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = TextPlain;
|
||||
context.Response.Body.Flush();
|
||||
Assert.True(responseReceived.WaitOne(TimeSpan.FromSeconds(3)));
|
||||
return context.Response.WriteAsync(new string('a', 100));
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
responseReceived.Set();
|
||||
|
||||
await response.Content.LoadIntoBufferAsync();
|
||||
|
||||
CheckResponseCompressed(response, expectedBodyLength: 24);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FlushAsyncHeaders_SendsHeaders_Compresses()
|
||||
{
|
||||
var responseReceived = new ManualResetEvent(false);
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
app.Run(async context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = TextPlain;
|
||||
await context.Response.Body.FlushAsync();
|
||||
Assert.True(responseReceived.WaitOne(TimeSpan.FromSeconds(3)));
|
||||
await context.Response.WriteAsync(new string('a', 100));
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
responseReceived.Set();
|
||||
|
||||
await response.Content.LoadIntoBufferAsync();
|
||||
|
||||
CheckResponseCompressed(response, expectedBodyLength: 24);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FlushBody_CompressesAndFlushes()
|
||||
{
|
||||
var responseReceived = new ManualResetEvent(false);
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
app.Run(context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = TextPlain;
|
||||
context.Response.Body.Write(new byte[10], 0, 10);
|
||||
context.Response.Body.Flush();
|
||||
Assert.True(responseReceived.WaitOne(TimeSpan.FromSeconds(3)));
|
||||
context.Response.Body.Write(new byte[90], 0, 90);
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
IEnumerable<string> contentMD5 = null;
|
||||
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out contentMD5));
|
||||
Assert.Single(response.Content.Headers.ContentEncoding, "gzip");
|
||||
|
||||
var body = await response.Content.ReadAsStreamAsync();
|
||||
var read = await body.ReadAsync(new byte[100], 0, 100);
|
||||
Assert.True(read > 0);
|
||||
|
||||
responseReceived.Set();
|
||||
|
||||
read = await body.ReadAsync(new byte[100], 0, 100);
|
||||
Assert.True(read > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FlushAsyncBody_CompressesAndFlushes()
|
||||
{
|
||||
var responseReceived = new ManualResetEvent(false);
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
app.Run(async context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = TextPlain;
|
||||
await context.Response.WriteAsync(new string('a', 10));
|
||||
await context.Response.Body.FlushAsync();
|
||||
Assert.True(responseReceived.WaitOne(TimeSpan.FromSeconds(3)));
|
||||
await context.Response.WriteAsync(new string('a', 90));
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
IEnumerable<string> contentMD5 = null;
|
||||
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out contentMD5));
|
||||
Assert.Single(response.Content.Headers.ContentEncoding, "gzip");
|
||||
|
||||
var body = await response.Content.ReadAsStreamAsync();
|
||||
var read = await body.ReadAsync(new byte[100], 0, 100);
|
||||
Assert.True(read > 0);
|
||||
|
||||
responseReceived.Set();
|
||||
|
||||
read = await body.ReadAsync(new byte[100], 0, 100);
|
||||
Assert.True(read > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TrickleWriteAndFlush_FlushesEachWrite()
|
||||
{
|
||||
var responseReceived = new[]
|
||||
{
|
||||
new ManualResetEvent(false),
|
||||
new ManualResetEvent(false),
|
||||
new ManualResetEvent(false),
|
||||
new ManualResetEvent(false),
|
||||
new ManualResetEvent(false),
|
||||
};
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
app.Run(context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = TextPlain;
|
||||
context.Features.Get<IHttpBufferingFeature>()?.DisableResponseBuffering();
|
||||
|
||||
foreach (var signal in responseReceived)
|
||||
{
|
||||
context.Response.Body.Write(new byte[1], 0, 1);
|
||||
context.Response.Body.Flush();
|
||||
Assert.True(signal.WaitOne(TimeSpan.FromSeconds(3)));
|
||||
}
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
#if NET461 // Flush not supported, compression disabled
|
||||
Assert.NotNull(response.Content.Headers.GetValues(HeaderNames.ContentMD5));
|
||||
Assert.Empty(response.Content.Headers.ContentEncoding);
|
||||
#elif NETCOREAPP2_0 || NETCOREAPP2_1 // Flush supported, compression enabled
|
||||
IEnumerable<string> contentMD5 = null;
|
||||
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out contentMD5));
|
||||
Assert.Single(response.Content.Headers.ContentEncoding, "gzip");
|
||||
#else
|
||||
#error Target frameworks need to be updated.
|
||||
#endif
|
||||
|
||||
var body = await response.Content.ReadAsStreamAsync();
|
||||
|
||||
foreach (var signal in responseReceived)
|
||||
{
|
||||
var read = await body.ReadAsync(new byte[100], 0, 100);
|
||||
Assert.True(read > 0);
|
||||
|
||||
signal.Set();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TrickleWriteAndFlushAsync_FlushesEachWrite()
|
||||
{
|
||||
var responseReceived = new[]
|
||||
{
|
||||
new ManualResetEvent(false),
|
||||
new ManualResetEvent(false),
|
||||
new ManualResetEvent(false),
|
||||
new ManualResetEvent(false),
|
||||
new ManualResetEvent(false),
|
||||
};
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
app.Run(async context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = TextPlain;
|
||||
context.Features.Get<IHttpBufferingFeature>()?.DisableResponseBuffering();
|
||||
|
||||
foreach (var signal in responseReceived)
|
||||
{
|
||||
await context.Response.WriteAsync("a");
|
||||
await context.Response.Body.FlushAsync();
|
||||
Assert.True(signal.WaitOne(TimeSpan.FromSeconds(3)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
#if NET461 // Flush not supported, compression disabled
|
||||
Assert.NotNull(response.Content.Headers.GetValues(HeaderNames.ContentMD5));
|
||||
Assert.Empty(response.Content.Headers.ContentEncoding);
|
||||
#elif NETCOREAPP2_0 || NETCOREAPP2_1 // Flush supported, compression enabled
|
||||
IEnumerable<string> contentMD5 = null;
|
||||
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out contentMD5));
|
||||
Assert.Single(response.Content.Headers.ContentEncoding, "gzip");
|
||||
#else
|
||||
#error Target framework needs to be updated
|
||||
#endif
|
||||
|
||||
var body = await response.Content.ReadAsStreamAsync();
|
||||
|
||||
foreach (var signal in responseReceived)
|
||||
{
|
||||
var read = await body.ReadAsync(new byte[100], 0, 100);
|
||||
Assert.True(read > 0);
|
||||
|
||||
signal.Set();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendFileAsync_OnlySetIfFeatureAlreadyExists()
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
app.Run(context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = TextPlain;
|
||||
context.Response.ContentLength = 1024;
|
||||
var sendFile = context.Features.Get<IHttpSendFileFeature>();
|
||||
Assert.Null(sendFile);
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendFileAsync_DifferentContentType_NotBypassed()
|
||||
{
|
||||
FakeSendFileFeature fakeSendFile = null;
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Use((context, next) =>
|
||||
{
|
||||
fakeSendFile = new FakeSendFileFeature(context.Response.Body);
|
||||
context.Features.Set<IHttpSendFileFeature>(fakeSendFile);
|
||||
return next();
|
||||
});
|
||||
app.UseResponseCompression();
|
||||
app.Run(context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = "custom/type";
|
||||
context.Response.ContentLength = 1024;
|
||||
var sendFile = context.Features.Get<IHttpSendFileFeature>();
|
||||
Assert.NotNull(sendFile);
|
||||
return sendFile.SendFileAsync("testfile1kb.txt", 0, null, CancellationToken.None);
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
CheckResponseNotCompressed(response, expectedBodyLength: 1024, sendVaryHeader: false);
|
||||
|
||||
Assert.True(fakeSendFile.Invoked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendFileAsync_FirstWrite_CompressesAndFlushes()
|
||||
{
|
||||
FakeSendFileFeature fakeSendFile = null;
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Use((context, next) =>
|
||||
{
|
||||
fakeSendFile = new FakeSendFileFeature(context.Response.Body);
|
||||
context.Features.Set<IHttpSendFileFeature>(fakeSendFile);
|
||||
return next();
|
||||
});
|
||||
app.UseResponseCompression();
|
||||
app.Run(context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = TextPlain;
|
||||
context.Response.ContentLength = 1024;
|
||||
var sendFile = context.Features.Get<IHttpSendFileFeature>();
|
||||
Assert.NotNull(sendFile);
|
||||
return sendFile.SendFileAsync("testfile1kb.txt", 0, null, CancellationToken.None);
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
CheckResponseCompressed(response, expectedBodyLength: 34);
|
||||
|
||||
Assert.False(fakeSendFile.Invoked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendFileAsync_AfterFirstWrite_CompressesAndFlushes()
|
||||
{
|
||||
FakeSendFileFeature fakeSendFile = null;
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Use((context, next) =>
|
||||
{
|
||||
fakeSendFile = new FakeSendFileFeature(context.Response.Body);
|
||||
context.Features.Set<IHttpSendFileFeature>(fakeSendFile);
|
||||
return next();
|
||||
});
|
||||
app.UseResponseCompression();
|
||||
app.Run(async context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = TextPlain;
|
||||
var sendFile = context.Features.Get<IHttpSendFileFeature>();
|
||||
Assert.NotNull(sendFile);
|
||||
|
||||
await context.Response.WriteAsync(new string('a', 100));
|
||||
await sendFile.SendFileAsync("testfile1kb.txt", 0, null, CancellationToken.None);
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
request.Headers.AcceptEncoding.ParseAdd("gzip");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
CheckResponseCompressed(response, expectedBodyLength: 40);
|
||||
|
||||
Assert.False(fakeSendFile.Invoked);
|
||||
}
|
||||
|
||||
private Task<HttpResponseMessage> InvokeMiddleware(int uncompressedBodyLength, string[] requestAcceptEncodings, string responseType, Action<HttpResponse> addResponseAction = null)
|
||||
{
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseResponseCompression();
|
||||
app.Run(context =>
|
||||
{
|
||||
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
|
||||
context.Response.ContentType = responseType;
|
||||
Assert.Null(context.Features.Get<IHttpSendFileFeature>());
|
||||
if (addResponseAction != null)
|
||||
{
|
||||
addResponseAction(context.Response);
|
||||
}
|
||||
return context.Response.WriteAsync(new string('a', uncompressedBodyLength));
|
||||
});
|
||||
});
|
||||
|
||||
var server = new TestServer(builder);
|
||||
var client = server.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
for (var i = 0; i < requestAcceptEncodings?.Length; i++)
|
||||
{
|
||||
request.Headers.AcceptEncoding.Add(System.Net.Http.Headers.StringWithQualityHeaderValue.Parse(requestAcceptEncodings[i]));
|
||||
}
|
||||
|
||||
return client.SendAsync(request);
|
||||
}
|
||||
|
||||
private void CheckResponseCompressed(HttpResponseMessage response, int expectedBodyLength)
|
||||
{
|
||||
IEnumerable<string> contentMD5 = null;
|
||||
|
||||
var containsVaryAcceptEncoding = false;
|
||||
foreach (var value in response.Headers.GetValues(HeaderNames.Vary))
|
||||
{
|
||||
if (value.Contains(HeaderNames.AcceptEncoding))
|
||||
{
|
||||
containsVaryAcceptEncoding = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert.True(containsVaryAcceptEncoding);
|
||||
Assert.False(response.Content.Headers.TryGetValues(HeaderNames.ContentMD5, out contentMD5));
|
||||
Assert.Single(response.Content.Headers.ContentEncoding, "gzip");
|
||||
Assert.Equal(expectedBodyLength, response.Content.Headers.ContentLength);
|
||||
}
|
||||
|
||||
private void CheckResponseNotCompressed(HttpResponseMessage response, int expectedBodyLength, bool sendVaryHeader)
|
||||
{
|
||||
if (sendVaryHeader)
|
||||
{
|
||||
var containsVaryAcceptEncoding = false;
|
||||
foreach (var value in response.Headers.GetValues(HeaderNames.Vary))
|
||||
{
|
||||
if (value.Contains(HeaderNames.AcceptEncoding))
|
||||
{
|
||||
containsVaryAcceptEncoding = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert.True(containsVaryAcceptEncoding);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.False(response.Headers.Contains(HeaderNames.Vary));
|
||||
}
|
||||
Assert.NotNull(response.Content.Headers.GetValues(HeaderNames.ContentMD5));
|
||||
Assert.Empty(response.Content.Headers.ContentEncoding);
|
||||
Assert.Equal(expectedBodyLength, response.Content.Headers.ContentLength);
|
||||
}
|
||||
|
||||
private class FakeSendFileFeature : IHttpSendFileFeature
|
||||
{
|
||||
private readonly Stream _innerBody;
|
||||
|
||||
public FakeSendFileFeature(Stream innerBody)
|
||||
{
|
||||
_innerBody = innerBody;
|
||||
}
|
||||
|
||||
public bool Invoked { get; set; }
|
||||
|
||||
public async Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
|
||||
{
|
||||
// This implementation should only be delegated to if compression is disabled.
|
||||
Invoked = true;
|
||||
using (var file = new FileStream(path, FileMode.Open))
|
||||
{
|
||||
file.Seek(offset, SeekOrigin.Begin);
|
||||
if (count.HasValue)
|
||||
{
|
||||
throw new NotImplementedException("Not implemented for testing");
|
||||
}
|
||||
await file.CopyToAsync(_innerBody, 81920, cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:6156/",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"RewriteSample": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# Rewrite path with additional sub directory
|
||||
RewriteCond %{HTTP_HOST} !^www\.example\.com [NC,OR]
|
||||
RewriteCond %{SERVER_PORT} !^5000$
|
||||
RewriteRule ^/(.*) http://www.example.com/$1 [L,R=302]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Rewrite" />
|
||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Https" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// 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.IO;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace RewriteSample
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IHostingEnvironment environment)
|
||||
{
|
||||
Environment = environment;
|
||||
}
|
||||
|
||||
public IHostingEnvironment Environment { get; private set; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.Configure<RewriteOptions>(options =>
|
||||
{
|
||||
options.AddRedirect("(.*)/$", "$1")
|
||||
.AddRewrite(@"app/(\d+)", "app?id=$1", skipRemainingRules: false)
|
||||
.AddRedirectToHttps(302, 5001)
|
||||
.AddIISUrlRewrite(Environment.ContentRootFileProvider, "UrlRewrite.xml")
|
||||
.AddApacheModRewrite(Environment.ContentRootFileProvider, "Rewrite.txt");
|
||||
});
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
||||
{
|
||||
app.UseRewriter();
|
||||
|
||||
app.Run(context =>
|
||||
{
|
||||
return context.Response.WriteAsync($"Rewritten Url: {context.Request.Path + context.Request.QueryString}");
|
||||
});
|
||||
}
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var host = new WebHostBuilder()
|
||||
.UseKestrel(options =>
|
||||
{
|
||||
options.Listen(IPAddress.Loopback, 5000);
|
||||
options.Listen(IPAddress.Loopback, 5001, listenOptions =>
|
||||
{
|
||||
// Configure SSL
|
||||
listenOptions.UseHttps("testCert.pfx", "testPassword");
|
||||
});
|
||||
})
|
||||
.UseStartup<Startup>()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.Build();
|
||||
|
||||
host.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<rewrite>
|
||||
<rules>
|
||||
<rule name="Rewrite 20 to 10" stopProcessing="true">
|
||||
<match url="^app$" />
|
||||
<conditions>
|
||||
<add input="{QUERY_STRING}" pattern="id=20" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="app?id=10" appendQueryString="false"/>
|
||||
</rule>
|
||||
</rules>
|
||||
</rewrite>
|
||||
Binary file not shown.
|
|
@ -0,0 +1,67 @@
|
|||
// 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.IO;
|
||||
using Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace Microsoft.AspNetCore.Rewrite
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for adding Apache mod_rewrite rules to <see cref="RewriteOptions"/>
|
||||
/// </summary>
|
||||
public static class ApacheModRewriteOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add rules from an Apache mod_rewrite file
|
||||
/// </summary>
|
||||
/// <param name="options">The <see cref="RewriteOptions"/></param>
|
||||
/// <param name="fileProvider">The <see cref="IFileProvider"/> </param>
|
||||
/// <param name="filePath">The path to the file containing mod_rewrite rules.</param>
|
||||
public static RewriteOptions AddApacheModRewrite(this RewriteOptions options, IFileProvider fileProvider, string filePath)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (fileProvider == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(fileProvider));
|
||||
}
|
||||
|
||||
var fileInfo = fileProvider.GetFileInfo(filePath);
|
||||
using (var stream = fileInfo.CreateReadStream())
|
||||
{
|
||||
return options.AddApacheModRewrite(new StreamReader(stream));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add rules from an Apache mod_rewrite file
|
||||
/// </summary>
|
||||
/// <param name="options">The <see cref="RewriteOptions"/></param>
|
||||
/// <param name="reader">A stream of mod_rewrite rules.</param>
|
||||
public static RewriteOptions AddApacheModRewrite(this RewriteOptions options, TextReader reader)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (reader == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(reader));
|
||||
}
|
||||
var rules = new FileParser().Parse(reader);
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
options.Rules.Add(rule);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
// 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.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Rewrite.Logging
|
||||
{
|
||||
internal static class RewriteMiddlewareLoggingExtensions
|
||||
{
|
||||
private static readonly Action<ILogger, string, Exception> _requestContinueResults;
|
||||
private static readonly Action<ILogger, string, int, Exception> _requestResponseComplete;
|
||||
private static readonly Action<ILogger, string, Exception> _requestStopRules;
|
||||
private static readonly Action<ILogger, string, Exception> _urlRewriteDidNotMatchRule;
|
||||
private static readonly Action<ILogger, string, Exception> _urlRewriteMatchedRule;
|
||||
private static readonly Action<ILogger, Exception> _modRewriteDidNotMatchRule;
|
||||
private static readonly Action<ILogger, Exception> _modRewriteMatchedRule;
|
||||
private static readonly Action<ILogger, Exception> _redirectedToHttps;
|
||||
private static readonly Action<ILogger, Exception> _redirectedToWww;
|
||||
private static readonly Action<ILogger, string, Exception> _redirectSummary;
|
||||
private static readonly Action<ILogger, string, Exception> _rewriteSummary;
|
||||
private static readonly Action<ILogger, string, Exception> _abortedRequest;
|
||||
private static readonly Action<ILogger, string, Exception> _customResponse;
|
||||
|
||||
static RewriteMiddlewareLoggingExtensions()
|
||||
{
|
||||
_requestContinueResults = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
1,
|
||||
"Request is continuing in applying rules. Current url is {currentUrl}");
|
||||
|
||||
_requestResponseComplete = LoggerMessage.Define<string, int>(
|
||||
LogLevel.Debug,
|
||||
2,
|
||||
"Request is done processing. Location header '{Location}' with status code '{StatusCode}'.");
|
||||
|
||||
_requestStopRules = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
3,
|
||||
"Request is done applying rules. Url was rewritten to {rewrittenUrl}");
|
||||
|
||||
_urlRewriteDidNotMatchRule = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
4,
|
||||
"Request did not match current rule '{Name}'.");
|
||||
|
||||
_urlRewriteMatchedRule = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
5,
|
||||
"Request matched current UrlRewriteRule '{Name}'.");
|
||||
|
||||
_modRewriteDidNotMatchRule = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
6,
|
||||
"Request matched current ModRewriteRule.");
|
||||
|
||||
_modRewriteMatchedRule = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
7,
|
||||
"Request matched current ModRewriteRule.");
|
||||
|
||||
_redirectedToHttps = LoggerMessage.Define(
|
||||
LogLevel.Information,
|
||||
8,
|
||||
"Request redirected to HTTPS");
|
||||
|
||||
_redirectSummary = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
9,
|
||||
"Request was redirected to {redirectedUrl}");
|
||||
|
||||
_rewriteSummary = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
10,
|
||||
"Request was rewritten to {rewrittenUrl}");
|
||||
|
||||
_abortedRequest = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
11,
|
||||
"Request to {requestedUrl} was aborted");
|
||||
|
||||
_customResponse = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
12,
|
||||
"Request to {requestedUrl} was ended");
|
||||
|
||||
_redirectedToWww = LoggerMessage.Define(
|
||||
LogLevel.Information,
|
||||
13,
|
||||
"Request redirected to www");
|
||||
}
|
||||
|
||||
public static void RewriteMiddlewareRequestContinueResults(this ILogger logger, string currentUrl)
|
||||
{
|
||||
_requestContinueResults(logger, currentUrl, null);
|
||||
}
|
||||
|
||||
public static void RewriteMiddlewareRequestResponseComplete(this ILogger logger, string location, int statusCode)
|
||||
{
|
||||
_requestResponseComplete(logger, location, statusCode, null);
|
||||
}
|
||||
|
||||
public static void RewriteMiddlewareRequestStopRules(this ILogger logger, string rewrittenUrl)
|
||||
{
|
||||
_requestStopRules(logger, rewrittenUrl, null);
|
||||
}
|
||||
|
||||
public static void UrlRewriteDidNotMatchRule(this ILogger logger, string name)
|
||||
{
|
||||
_urlRewriteDidNotMatchRule(logger, name, null);
|
||||
}
|
||||
|
||||
public static void UrlRewriteMatchedRule(this ILogger logger, string name)
|
||||
{
|
||||
_urlRewriteMatchedRule(logger, name, null);
|
||||
}
|
||||
|
||||
public static void ModRewriteDidNotMatchRule(this ILogger logger)
|
||||
{
|
||||
_modRewriteDidNotMatchRule(logger, null);
|
||||
}
|
||||
|
||||
public static void ModRewriteMatchedRule(this ILogger logger)
|
||||
{
|
||||
_modRewriteMatchedRule(logger, null);
|
||||
}
|
||||
|
||||
public static void RedirectedToHttps(this ILogger logger)
|
||||
{
|
||||
_redirectedToHttps(logger, null);
|
||||
}
|
||||
|
||||
public static void RedirectedToWww(this ILogger logger)
|
||||
{
|
||||
_redirectedToWww(logger, null);
|
||||
}
|
||||
|
||||
public static void RedirectedSummary(this ILogger logger, string redirectedUrl)
|
||||
{
|
||||
_redirectSummary(logger, redirectedUrl, null);
|
||||
}
|
||||
|
||||
public static void RewriteSummary(this ILogger logger, string rewrittenUrl)
|
||||
{
|
||||
_rewriteSummary(logger, rewrittenUrl, null);
|
||||
}
|
||||
|
||||
public static void AbortedRequest(this ILogger logger, string requestedUrl)
|
||||
{
|
||||
_abortedRequest(logger, requestedUrl, null);
|
||||
}
|
||||
|
||||
public static void CustomResponse(this ILogger logger, string requestedUrl)
|
||||
{
|
||||
_customResponse(logger, requestedUrl, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// 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.IO;
|
||||
using Microsoft.AspNetCore.Rewrite.Internal.IISUrlRewrite;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace Microsoft.AspNetCore.Rewrite
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for adding IIS Url Rewrite rules to <see cref="RewriteOptions"/>
|
||||
/// </summary>
|
||||
public static class IISUrlRewriteOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add rules from a IIS config file containing Url Rewrite rules
|
||||
/// </summary>
|
||||
/// <param name="options">The <see cref="RewriteOptions"/></param>
|
||||
/// <param name="fileProvider">The <see cref="IFileProvider"/> </param>
|
||||
/// <param name="filePath">The path to the file containing UrlRewrite rules.</param>
|
||||
public static RewriteOptions AddIISUrlRewrite(this RewriteOptions options, IFileProvider fileProvider, string filePath)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (fileProvider == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(fileProvider));
|
||||
}
|
||||
|
||||
var file = fileProvider.GetFileInfo(filePath);
|
||||
|
||||
using (var stream = file.CreateReadStream())
|
||||
{
|
||||
return AddIISUrlRewrite(options, new StreamReader(stream));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add rules from a IIS config file containing Url Rewrite rules
|
||||
/// </summary>
|
||||
/// <param name="options">The <see cref="RewriteOptions"/></param>
|
||||
/// <param name="reader">The text reader stream.</param>
|
||||
public static RewriteOptions AddIISUrlRewrite(this RewriteOptions options, TextReader reader)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (reader == null)
|
||||
{
|
||||
throw new ArgumentException(nameof(reader));
|
||||
}
|
||||
|
||||
var rules = new UrlRewriteFileParser().Parse(reader);
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
options.Rules.Add(rule);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Rewrite
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a rule.
|
||||
/// </summary>
|
||||
public interface IRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the rule.
|
||||
/// Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result"/>
|
||||
/// (defaults to RuleResult.ContinueRules)
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
void ApplyRule(RewriteContext context);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// 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.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Rewrite.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite
|
||||
{
|
||||
public class ApacheModRewriteRule : IRule
|
||||
{
|
||||
public UrlMatch InitialMatch { get; }
|
||||
public IList<Condition> Conditions { get; }
|
||||
public IList<UrlAction> Actions { get; }
|
||||
|
||||
public ApacheModRewriteRule(UrlMatch initialMatch, IList<Condition> conditions, IList<UrlAction> urlActions)
|
||||
{
|
||||
Conditions = conditions;
|
||||
InitialMatch = initialMatch;
|
||||
Actions = urlActions;
|
||||
}
|
||||
|
||||
public virtual void ApplyRule(RewriteContext context)
|
||||
{
|
||||
// 1. Figure out which section of the string to match for the initial rule.
|
||||
var initMatchRes = InitialMatch.Evaluate(context.HttpContext.Request.Path, context);
|
||||
|
||||
if (!initMatchRes.Success)
|
||||
{
|
||||
context.Logger?.ModRewriteDidNotMatchRule();
|
||||
return;
|
||||
}
|
||||
|
||||
BackReferenceCollection condBackReferences = null;
|
||||
if (Conditions != null)
|
||||
{
|
||||
var condResult = ConditionEvaluator.Evaluate(Conditions, context, initMatchRes.BackReferences);
|
||||
if (!condResult.Success)
|
||||
{
|
||||
context.Logger?.ModRewriteDidNotMatchRule();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, we know our rule passed, first apply pre conditions,
|
||||
// which can modify things like the cookie or env, and then apply the action
|
||||
context.Logger?.ModRewriteMatchedRule();
|
||||
|
||||
foreach (var action in Actions)
|
||||
{
|
||||
action.ApplyAction(context, initMatchRes?.BackReferences, condBackReferences);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite
|
||||
{
|
||||
public class Condition
|
||||
{
|
||||
public Pattern Input { get; set; }
|
||||
public UrlMatch Match { get; set; }
|
||||
public bool OrNext { get; set; }
|
||||
|
||||
public MatchResults Evaluate(RewriteContext context, BackReferenceCollection ruleBackReferences, BackReferenceCollection conditionBackReferences)
|
||||
{
|
||||
var pattern = Input.Evaluate(context, ruleBackReferences, conditionBackReferences);
|
||||
return Match.Evaluate(pattern, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// 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.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite
|
||||
{
|
||||
public static class ConditionEvaluator
|
||||
{
|
||||
public static MatchResults Evaluate(IEnumerable<Condition> conditions, RewriteContext context, BackReferenceCollection backReferences)
|
||||
{
|
||||
return Evaluate(conditions, context, backReferences, trackAllCaptures: false);
|
||||
}
|
||||
|
||||
public static MatchResults Evaluate(IEnumerable<Condition> conditions, RewriteContext context, BackReferenceCollection backReferences, bool trackAllCaptures)
|
||||
{
|
||||
BackReferenceCollection prevBackReferences = null;
|
||||
MatchResults condResult = null;
|
||||
var orSucceeded = false;
|
||||
foreach (var condition in conditions)
|
||||
{
|
||||
if (orSucceeded && condition.OrNext)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (orSucceeded)
|
||||
{
|
||||
orSucceeded = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
condResult = condition.Evaluate(context, backReferences, prevBackReferences);
|
||||
var currentBackReferences = condResult.BackReferences;
|
||||
if (condition.OrNext)
|
||||
{
|
||||
orSucceeded = condResult.Success;
|
||||
}
|
||||
else if (!condResult.Success)
|
||||
{
|
||||
return condResult;
|
||||
}
|
||||
|
||||
if (condResult.Success && trackAllCaptures && prevBackReferences != null)
|
||||
{
|
||||
prevBackReferences.Add(currentBackReferences);
|
||||
currentBackReferences = prevBackReferences;
|
||||
}
|
||||
|
||||
prevBackReferences = currentBackReferences;
|
||||
}
|
||||
|
||||
return new MatchResults { BackReferences = prevBackReferences, Success = condResult.Success };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses the "CondPattern" portion of the RewriteCond.
|
||||
/// RewriteCond TestString CondPattern
|
||||
/// </summary>
|
||||
public class ConditionPatternParser
|
||||
{
|
||||
private const char Not = '!';
|
||||
private const char Dash = '-';
|
||||
private const char Less = '<';
|
||||
private const char Greater = '>';
|
||||
private const char EqualSign = '=';
|
||||
|
||||
/// <summary>
|
||||
/// Given a CondPattern, create a ParsedConditionExpression, containing the type of operation
|
||||
/// and value.
|
||||
/// ParsedConditionExpression is an intermediary object, which will be made into a ConditionExpression
|
||||
/// once the flags are parsed.
|
||||
/// </summary>
|
||||
/// <param name="condition">The CondPattern portion of a mod_rewrite RewriteCond.</param>
|
||||
/// <returns>A new parsed condition.</returns>
|
||||
public ParsedModRewriteInput ParseActionCondition(string condition)
|
||||
{
|
||||
if (condition == null)
|
||||
{
|
||||
condition = string.Empty;
|
||||
}
|
||||
var context = new ParserContext(condition);
|
||||
var results = new ParsedModRewriteInput();
|
||||
if (!context.Next())
|
||||
{
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
|
||||
}
|
||||
|
||||
// If we hit a !, invert the condition
|
||||
if (context.Current == Not)
|
||||
{
|
||||
results.Invert = true;
|
||||
if (!context.Next())
|
||||
{
|
||||
// Dangling !
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
|
||||
}
|
||||
}
|
||||
|
||||
// Control Block for strings. Set the operation and type fields based on the sign
|
||||
// Switch on current character
|
||||
switch (context.Current)
|
||||
{
|
||||
case Greater:
|
||||
if (!context.Next())
|
||||
{
|
||||
// Dangling ">"
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
|
||||
}
|
||||
if (context.Current == EqualSign)
|
||||
{
|
||||
if (!context.Next())
|
||||
{
|
||||
// Dangling ">="
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
|
||||
}
|
||||
results.OperationType = OperationType.GreaterEqual;
|
||||
results.ConditionType = ConditionType.StringComp;
|
||||
}
|
||||
else
|
||||
{
|
||||
results.OperationType = OperationType.Greater;
|
||||
results.ConditionType = ConditionType.StringComp;
|
||||
}
|
||||
break;
|
||||
case Less:
|
||||
if (!context.Next())
|
||||
{
|
||||
// Dangling "<"
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
|
||||
}
|
||||
if (context.Current == EqualSign)
|
||||
{
|
||||
if (!context.Next())
|
||||
{
|
||||
// Dangling "<="
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
|
||||
}
|
||||
results.OperationType = OperationType.LessEqual;
|
||||
results.ConditionType = ConditionType.StringComp;
|
||||
}
|
||||
else
|
||||
{
|
||||
results.OperationType = OperationType.Less;
|
||||
results.ConditionType = ConditionType.StringComp;
|
||||
}
|
||||
break;
|
||||
case EqualSign:
|
||||
if (!context.Next())
|
||||
{
|
||||
// Dangling "="
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
|
||||
}
|
||||
results.OperationType = OperationType.Equal;
|
||||
results.ConditionType = ConditionType.StringComp;
|
||||
break;
|
||||
case Dash:
|
||||
results = ParseProperty(context, results.Invert);
|
||||
if (results.ConditionType == ConditionType.PropertyTest)
|
||||
{
|
||||
return results;
|
||||
}
|
||||
context.Next();
|
||||
break;
|
||||
default:
|
||||
results.ConditionType = ConditionType.Regex;
|
||||
break;
|
||||
}
|
||||
|
||||
// Capture the rest of the string guarantee validity.
|
||||
results.Operand = condition.Substring(context.GetIndex());
|
||||
if (IsValidActionCondition(results))
|
||||
{
|
||||
return results;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given that the current index is a property (ex checks for directory or regular files), create a
|
||||
/// new ParsedConditionExpression with the appropriate property operation.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="invert"></param>
|
||||
/// <returns></returns>
|
||||
private static ParsedModRewriteInput ParseProperty(ParserContext context, bool invert)
|
||||
{
|
||||
if (!context.Next())
|
||||
{
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index));
|
||||
}
|
||||
|
||||
switch (context.Current)
|
||||
{
|
||||
case 'd':
|
||||
return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.Directory, operand: null);
|
||||
case 'f':
|
||||
return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.RegularFile, operand: null);
|
||||
case 'F':
|
||||
return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.ExistingFile, operand: null);
|
||||
case 'h':
|
||||
case 'L':
|
||||
return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.SymbolicLink, operand: null);
|
||||
case 's':
|
||||
return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.Size, operand: null);
|
||||
case 'U':
|
||||
return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.ExistingUrl, operand: null);
|
||||
case 'x':
|
||||
return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.Executable, operand: null);
|
||||
case 'e':
|
||||
if (!context.Next() || context.Current != 'q')
|
||||
{
|
||||
// Illegal statement.
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index));
|
||||
}
|
||||
return new ParsedModRewriteInput(invert, ConditionType.IntComp, OperationType.Equal, operand: null);
|
||||
case 'g':
|
||||
if (!context.Next())
|
||||
{
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index));
|
||||
}
|
||||
switch (context.Current)
|
||||
{
|
||||
case 't':
|
||||
return new ParsedModRewriteInput(invert, ConditionType.IntComp, OperationType.Greater, operand: null);
|
||||
case 'e':
|
||||
return new ParsedModRewriteInput(invert, ConditionType.IntComp, OperationType.GreaterEqual, operand: null);
|
||||
default:
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index));
|
||||
}
|
||||
case 'l':
|
||||
// name conflict with -l and -lt/-le, so the assumption is if there is no
|
||||
// charcters after -l, we assume it a symbolic link
|
||||
if (!context.Next())
|
||||
{
|
||||
return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.SymbolicLink, operand: null);
|
||||
}
|
||||
switch (context.Current)
|
||||
{
|
||||
case 't':
|
||||
return new ParsedModRewriteInput(invert, ConditionType.IntComp, OperationType.Less, operand: null);
|
||||
case 'e':
|
||||
return new ParsedModRewriteInput(invert, ConditionType.IntComp, OperationType.LessEqual, operand: null);
|
||||
default:
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index));
|
||||
}
|
||||
case 'n':
|
||||
if (!context.Next() || context.Current != 'e')
|
||||
{
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index));
|
||||
}
|
||||
return new ParsedModRewriteInput(invert, ConditionType.IntComp, OperationType.NotEqual, operand: null);
|
||||
default:
|
||||
throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsValidActionCondition(ParsedModRewriteInput results)
|
||||
{
|
||||
if (results.ConditionType == ConditionType.IntComp)
|
||||
{
|
||||
// If the type is an integer, verify operand is actually an int
|
||||
int res;
|
||||
if (!int.TryParse(results.Operand, out res))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite
|
||||
{
|
||||
public enum ConditionType
|
||||
{
|
||||
Regex,
|
||||
PropertyTest,
|
||||
StringComp,
|
||||
IntComp
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
// 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.Globalization;
|
||||
using Microsoft.AspNetCore.Rewrite.Internal.UrlActions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite
|
||||
{
|
||||
public class CookieActionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ChangeCookieAction" /> <see href="https://httpd.apache.org/docs/current/rewrite/flags.html#flag_co" /> for details.
|
||||
/// </summary>
|
||||
/// <param name="flagValue">The flag</param>
|
||||
/// <returns>The action</returns>
|
||||
public ChangeCookieAction Create(string flagValue)
|
||||
{
|
||||
if (string.IsNullOrEmpty(flagValue))
|
||||
{
|
||||
throw new ArgumentException(nameof(flagValue));
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
var separator = ':';
|
||||
if (flagValue[0] == ';')
|
||||
{
|
||||
separator = ';';
|
||||
i++;
|
||||
}
|
||||
|
||||
ChangeCookieAction action = null;
|
||||
var currentField = Fields.Name;
|
||||
var start = i;
|
||||
for (; i < flagValue.Length; i++)
|
||||
{
|
||||
if (flagValue[i] == separator)
|
||||
{
|
||||
var length = i - start;
|
||||
SetActionOption(flagValue.Substring(start, length).Trim(), currentField, ref action);
|
||||
|
||||
currentField++;
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (i != start)
|
||||
{
|
||||
SetActionOption(flagValue.Substring(start).Trim(new[] { ' ', separator }), currentField, ref action);
|
||||
}
|
||||
|
||||
if (currentField < Fields.Domain)
|
||||
{
|
||||
throw new FormatException(Resources.FormatError_InvalidChangeCookieFlag(flagValue));
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private static void SetActionOption(string value, Fields tokenType, ref ChangeCookieAction action)
|
||||
{
|
||||
switch (tokenType)
|
||||
{
|
||||
case Fields.Name:
|
||||
action = new ChangeCookieAction(value);
|
||||
break;
|
||||
case Fields.Value:
|
||||
action.Value = value;
|
||||
break;
|
||||
case Fields.Domain:
|
||||
// despite what spec says, an empty domain field is allowed in mod_rewrite
|
||||
// by specifying NAME:VALUE:;
|
||||
action.Domain = string.IsNullOrEmpty(value) || value == ";"
|
||||
? null
|
||||
: value;
|
||||
break;
|
||||
case Fields.Lifetime:
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
uint minutes;
|
||||
if (!uint.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out minutes))
|
||||
{
|
||||
throw new FormatException(Resources.FormatError_CouldNotParseInteger(value));
|
||||
}
|
||||
|
||||
action.Lifetime = TimeSpan.FromMinutes(minutes);
|
||||
break;
|
||||
case Fields.Path:
|
||||
action.Path = value;
|
||||
break;
|
||||
case Fields.Secure:
|
||||
action.Secure = "secure".Equals(value, StringComparison.OrdinalIgnoreCase)
|
||||
|| "true".Equals(value, StringComparison.OrdinalIgnoreCase)
|
||||
|| value == "1";
|
||||
break;
|
||||
case Fields.HttpOnly:
|
||||
action.HttpOnly = "httponly".Equals(value, StringComparison.OrdinalIgnoreCase)
|
||||
|| "true".Equals(value, StringComparison.OrdinalIgnoreCase)
|
||||
|| value == "1";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// order matters
|
||||
// see https://httpd.apache.org/docs/current/rewrite/flags.html#flag_co
|
||||
private enum Fields
|
||||
{
|
||||
Name,
|
||||
Value,
|
||||
Domain,
|
||||
Lifetime,
|
||||
Path,
|
||||
Secure,
|
||||
HttpOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite
|
||||
{
|
||||
public class FileParser
|
||||
{
|
||||
public IList<IRule> Parse(TextReader input)
|
||||
{
|
||||
string line;
|
||||
var rules = new List<IRule>();
|
||||
var builder = new RuleBuilder();
|
||||
var lineNum = 0;
|
||||
|
||||
// parsers
|
||||
var testStringParser = new TestStringParser();
|
||||
var conditionParser = new ConditionPatternParser();
|
||||
var regexParser = new RuleRegexParser();
|
||||
var flagsParser = new FlagParser();
|
||||
var tokenizer = new Tokenizer();
|
||||
|
||||
while ((line = input.ReadLine()) != null)
|
||||
{
|
||||
lineNum++;
|
||||
if (string.IsNullOrEmpty(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (line.StartsWith("#"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var tokens = tokenizer.Tokenize(line);
|
||||
if (tokens.Count > 4)
|
||||
{
|
||||
// This means the line didn't have an appropriate format, throw format exception
|
||||
throw new FormatException(Resources.FormatError_ModRewriteParseError("Too many tokens on line", lineNum));
|
||||
}
|
||||
|
||||
switch (tokens[0])
|
||||
{
|
||||
case "RewriteBase":
|
||||
// the notion of the path base spans across all rules, not just mod_rewrite
|
||||
// So not implemented for now
|
||||
throw new NotImplementedException("RewriteBase is not implemented");
|
||||
case "RewriteCond":
|
||||
try
|
||||
{
|
||||
var pattern = testStringParser.Parse(tokens[1]);
|
||||
var condActionParsed = conditionParser.ParseActionCondition(tokens[2]);
|
||||
|
||||
var flags = new Flags();
|
||||
if (tokens.Count == 4)
|
||||
{
|
||||
flags = flagsParser.Parse(tokens[3]);
|
||||
}
|
||||
|
||||
builder.AddConditionFromParts(pattern, condActionParsed, flags);
|
||||
}
|
||||
catch (FormatException formatException)
|
||||
{
|
||||
throw new FormatException(Resources.FormatError_ModRewriteGeneralParseError(lineNum), formatException);
|
||||
}
|
||||
break;
|
||||
case "RewriteRule":
|
||||
try
|
||||
{
|
||||
var regex = regexParser.ParseRuleRegex(tokens[1]);
|
||||
var pattern = testStringParser.Parse(tokens[2]);
|
||||
|
||||
Flags flags;
|
||||
if (tokens.Count == 4)
|
||||
{
|
||||
flags = flagsParser.Parse(tokens[3]);
|
||||
}
|
||||
else
|
||||
{
|
||||
flags = new Flags();
|
||||
}
|
||||
|
||||
builder.AddMatch(regex, flags);
|
||||
builder.AddAction(pattern, flags);
|
||||
rules.Add(builder.Build());
|
||||
builder = new RuleBuilder();
|
||||
}
|
||||
catch (FormatException formatException)
|
||||
{
|
||||
throw new FormatException(Resources.FormatError_ModRewriteGeneralParseError(lineNum), formatException);
|
||||
}
|
||||
break;
|
||||
case "RewriteMap":
|
||||
// Lack of use
|
||||
throw new NotImplementedException("RewriteMap are not implemented");
|
||||
case "RewriteEngine":
|
||||
// Explicitly do nothing here, no notion of turning on regex engine.
|
||||
break;
|
||||
default:
|
||||
throw new FormatException(Resources.FormatError_ModRewriteParseError("Unrecognized keyword: " + tokens[0], lineNum));
|
||||
}
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
// 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.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Rewrite.Internal.ApacheModRewrite
|
||||
{
|
||||
public class FlagParser
|
||||
{
|
||||
private readonly IDictionary<string, FlagType> _ruleFlagLookup = new Dictionary<string, FlagType>(StringComparer.OrdinalIgnoreCase) {
|
||||
{ "b", FlagType.EscapeBackreference},
|
||||
{ "c", FlagType.Chain },
|
||||
{ "chain", FlagType.Chain},
|
||||
{ "co", FlagType.Cookie },
|
||||
{ "cookie", FlagType.Cookie },
|
||||
{ "dpi", FlagType.DiscardPath },
|
||||
{ "discardpath", FlagType.DiscardPath },
|
||||
{ "e", FlagType.Env},
|
||||
{ "env", FlagType.Env},
|
||||
{ "end", FlagType.End },
|
||||
{ "f", FlagType.Forbidden },
|
||||
{ "forbidden", FlagType.Forbidden },
|
||||
{ "g", FlagType.Gone },
|
||||
{ "gone", FlagType.Gone },
|
||||
{ "h", FlagType.Handler },
|
||||
{ "handler", FlagType.Handler },
|
||||
{ "l", FlagType.Last },
|
||||
{ "last", FlagType.Last },
|
||||
{ "n", FlagType.Next },
|
||||
{ "next", FlagType.Next },
|
||||
{ "nc", FlagType.NoCase },
|
||||
{ "nocase", FlagType.NoCase },
|
||||
{ "ne", FlagType.NoEscape },
|
||||
{ "noescape", FlagType.NoEscape },
|
||||
{ "ns", FlagType.NoSubReq },
|
||||
{ "nosubreq", FlagType.NoSubReq },
|
||||
{ "or", FlagType.Or },
|
||||
{ "ornext", FlagType.Or },
|
||||
{ "p", FlagType.Proxy },
|
||||
{ "proxy", FlagType.Proxy },
|
||||
{ "pt", FlagType.PassThrough },
|
||||
{ "passthrough", FlagType.PassThrough },
|
||||
{ "qsa", FlagType.QSAppend },
|
||||
{ "qsappend", FlagType.QSAppend },
|
||||
{ "qsd", FlagType.QSDiscard },
|
||||
{ "qsdiscard", FlagType.QSDiscard },
|
||||
{ "qsl", FlagType.QSLast },
|
||||
{ "qslast", FlagType.QSLast },
|
||||
{ "r", FlagType.Redirect },
|
||||
{ "redirect", FlagType.Redirect },
|
||||
{ "s", FlagType.Skip },
|
||||
{ "skip", FlagType.Skip },
|
||||
{ "t", FlagType.Type },
|
||||
{ "type", FlagType.Type },
|
||||
};
|
||||
|
||||
public Flags Parse(string flagString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(flagString))
|
||||
{
|
||||
throw new ArgumentException(nameof(flagString));
|
||||
}
|
||||
|
||||
// Check that flags are contained within []
|
||||
// Guaranteed to have a length of at least 1 here, so this will never throw for indexing.
|
||||
if (!(flagString[0] == '[' && flagString[flagString.Length - 1] == ']'))
|
||||
{
|
||||
throw new FormatException("Flags should start and end with square brackets: [flags]");
|
||||
}
|
||||
|
||||
// Lexing esque step to split all flags.
|
||||
// Invalid syntax to have any spaces.
|
||||
var tokens = flagString.Substring(1, flagString.Length - 2).Split(',');
|
||||
var flags = new Flags();
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
var hasPayload = token.Split('=');
|
||||
|
||||
FlagType flag;
|
||||
if (!_ruleFlagLookup.TryGetValue(hasPayload[0], out flag))
|
||||
{
|
||||
throw new FormatException($"Unrecognized flag: '{hasPayload[0]}'");
|
||||
}
|
||||
|
||||
if (hasPayload.Length == 2)
|
||||
{
|
||||
flags.SetFlag(flag, hasPayload[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
flags.SetFlag(flag, string.Empty);
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue