From 4de03b6dffda2769d5d0cdd3850229fe3d254c91 Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Tue, 15 Nov 2016 10:28:06 -0800 Subject: [PATCH 01/14] Added logging for preflight requests and origin headers --- .../Infrastructure/CorsService.cs | 17 ++++++ .../Internal/CORSLoggerExtensions.cs | 58 +++++++++++++++++++ .../CorsServiceTests.cs | 2 +- 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs diff --git a/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs b/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs index 6af9dafc7d..87343b2f9f 100644 --- a/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs +++ b/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs @@ -5,9 +5,11 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Microsoft.AspNetCore.Cors.Internal; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Cors.Infrastructure { @@ -17,12 +19,23 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure public class CorsService : ICorsService { private readonly CorsOptions _options; + private readonly ILogger _logger; /// /// Creates a new instance of the . /// /// The option model representing . public CorsService(IOptions options) + :this(options, loggerFactory: null) + { + } + + /// + /// Creates a new instance of the . + /// + /// The option model representing . + /// The . + public CorsService(IOptions options, ILoggerFactory loggerFactory) { if (options == null) { @@ -30,6 +43,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure } _options = options.Value; + _logger = loggerFactory?.CreateLogger(); } /// @@ -70,6 +84,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure !StringValues.IsNullOrEmpty(accessControlRequestMethod)) { EvaluatePreflightRequest(context, policy, corsResult); + _logger?.IsPreflightRequest(); } else { @@ -87,6 +102,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure return; } + _logger?.RequestHasOriginHeader(); AddOriginToResult(origin, policy, result); result.SupportsCredentials = policy.SupportsCredentials; AddHeaderValues(result.AllowedExposedHeaders, policy.ExposedHeaders); @@ -100,6 +116,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure return; } + _logger?.RequestHasOriginHeader(); var accessControlRequestMethod = context.Request.Headers[CorsConstants.AccessControlRequestMethod]; if (StringValues.IsNullOrEmpty(accessControlRequestMethod)) { diff --git a/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs b/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs new file mode 100644 index 0000000000..7672797458 --- /dev/null +++ b/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs @@ -0,0 +1,58 @@ +// 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.Cors.Internal +{ + internal static class CORSLoggerExtensions + { + private static readonly Action _isPreflightRequest; + private static readonly Action _requestHasOriginHeader; + private static readonly Action _policySuccess; + private static readonly Action _policyFailure; + + static CORSLoggerExtensions() + { + _isPreflightRequest = LoggerMessage.Define( + LogLevel.Debug, + 1, + "The request is preflight."); + + _requestHasOriginHeader = LoggerMessage.Define( + LogLevel.Debug, + 2, + "The request has an origin header."); + + _policySuccess = LoggerMessage.Define( + LogLevel.Information, + 3, + "Policy execution successful."); + + _policyFailure = LoggerMessage.Define( + LogLevel.Information, + 3, + "Policy execution failed. '{FailureReason}'"); + } + + public static void IsPreflightRequest(this ILogger logger) + { + _isPreflightRequest(logger, null); + } + + public static void RequestHasOriginHeader(this ILogger logger) + { + _requestHasOriginHeader(logger, null); + } + + public static void PolicySuccess(this ILogger logger, string failureReason) + { + _policySuccess(logger, null); + } + public static void PolicyFailure(this ILogger logger, string failureReason) + { + _policyFailure(logger, failureReason, null); + } + } +} diff --git a/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs b/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs index c88eccd6fa..b3a92a8d68 100644 --- a/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs +++ b/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs @@ -446,7 +446,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure } [Fact] - public void EaluatePolicy_DoesCaseSensitiveComparison() + public void EvaluatePolicy_DoesCaseSensitiveComparison() { // Arrange var corsService = new CorsService(new TestCorsOptions()); From 98bb5d67fdf11e7dc29b34cb562c26979f095484 Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Wed, 16 Nov 2016 15:46:47 -0800 Subject: [PATCH 02/14] Design PR for CORS sample and logging --- CORS.sln | 19 ++++++- SampleDestination/Program.cs | 31 ++++++++++++ SampleDestination/SampleDestination.xproj | 25 ++++++++++ SampleDestination/Startup.cs | 36 +++++++++++++ SampleDestination/StatusMiddleware.cs | 35 +++++++++++++ SampleDestination/hosting.json | 3 ++ SampleDestination/project.json | 50 +++++++++++++++++++ SampleDestination/web.config | 14 ++++++ SampleOrigin/Program.cs | 32 ++++++++++++ SampleOrigin/SampleOrigin.xproj | 25 ++++++++++ SampleOrigin/Startup.cs | 35 +++++++++++++ SampleOrigin/hosting.json | 3 ++ SampleOrigin/project.json | 50 +++++++++++++++++++ SampleOrigin/web.config | 14 ++++++ SampleOrigin/wwwroot/Index.html | 49 ++++++++++++++++++ .../Infrastructure/CorsService.cs | 10 ++-- .../Internal/CORSLoggerExtensions.cs | 5 +- src/Microsoft.AspNetCore.Cors/project.json | 1 + 18 files changed, 430 insertions(+), 7 deletions(-) create mode 100644 SampleDestination/Program.cs create mode 100644 SampleDestination/SampleDestination.xproj create mode 100644 SampleDestination/Startup.cs create mode 100644 SampleDestination/StatusMiddleware.cs create mode 100644 SampleDestination/hosting.json create mode 100644 SampleDestination/project.json create mode 100644 SampleDestination/web.config create mode 100644 SampleOrigin/Program.cs create mode 100644 SampleOrigin/SampleOrigin.xproj create mode 100644 SampleOrigin/Startup.cs create mode 100644 SampleOrigin/hosting.json create mode 100644 SampleOrigin/project.json create mode 100644 SampleOrigin/web.config create mode 100644 SampleOrigin/wwwroot/Index.html diff --git a/CORS.sln b/CORS.sln index 9f3b2ba560..87e766660f 100644 --- a/CORS.sln +++ b/CORS.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{84FE6872-A610-4CEC-855F-A84CBF1F40FC}" EndProject @@ -20,6 +19,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebSites", "WebSites", "{53 EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CorsMiddlewareWebSite", "test\WebSites\CorsMiddlewareWebSite\CorsMiddlewareWebSite.xproj", "{B42D4844-FFF8-4EC2-88D1-3AE95234D9EB}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{960E0703-A8A5-44DF-AA87-B7C614683B3C}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SampleDestination", "SampleDestination\SampleDestination.xproj", "{F6675DC1-AA21-453B-89B6-DA425FB9C3A5}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SampleOrigin", "SampleOrigin\SampleOrigin.xproj", "{99460370-AE5D-4DC9-8DBF-04DF66D6B21D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +43,14 @@ Global {B42D4844-FFF8-4EC2-88D1-3AE95234D9EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {B42D4844-FFF8-4EC2-88D1-3AE95234D9EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {B42D4844-FFF8-4EC2-88D1-3AE95234D9EB}.Release|Any CPU.Build.0 = Release|Any CPU + {F6675DC1-AA21-453B-89B6-DA425FB9C3A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6675DC1-AA21-453B-89B6-DA425FB9C3A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6675DC1-AA21-453B-89B6-DA425FB9C3A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6675DC1-AA21-453B-89B6-DA425FB9C3A5}.Release|Any CPU.Build.0 = Release|Any CPU + {99460370-AE5D-4DC9-8DBF-04DF66D6B21D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99460370-AE5D-4DC9-8DBF-04DF66D6B21D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99460370-AE5D-4DC9-8DBF-04DF66D6B21D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99460370-AE5D-4DC9-8DBF-04DF66D6B21D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -47,5 +60,7 @@ Global {F05BE96F-F869-4408-A480-96935B4835EE} = {F32074C7-087C-46CC-A913-422BFD2D6E0A} {538380BF-0D4C-4E30-8F41-E75C4B1C01FA} = {F32074C7-087C-46CC-A913-422BFD2D6E0A} {B42D4844-FFF8-4EC2-88D1-3AE95234D9EB} = {538380BF-0D4C-4E30-8F41-E75C4B1C01FA} + {F6675DC1-AA21-453B-89B6-DA425FB9C3A5} = {960E0703-A8A5-44DF-AA87-B7C614683B3C} + {99460370-AE5D-4DC9-8DBF-04DF66D6B21D} = {960E0703-A8A5-44DF-AA87-B7C614683B3C} EndGlobalSection EndGlobal diff --git a/SampleDestination/Program.cs b/SampleDestination/Program.cs new file mode 100644 index 0000000000..972cde8b8b --- /dev/null +++ b/SampleDestination/Program.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; + +namespace SampleDestination +{ + public class Program + { + public static void Main(string[] args) + { + var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("hosting.json", optional: true) + .Build(); + + var host = new WebHostBuilder() + .UseKestrel() + .UseConfiguration(config) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/SampleDestination/SampleDestination.xproj b/SampleDestination/SampleDestination.xproj new file mode 100644 index 0000000000..ac33a6f88b --- /dev/null +++ b/SampleDestination/SampleDestination.xproj @@ -0,0 +1,25 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + f6675dc1-aa21-453b-89b6-da425fb9c3a5 + SampleDestination + .\obj + .\bin\ + v4.5.2 + + + + 2.0 + + + + + + + diff --git a/SampleDestination/Startup.cs b/SampleDestination/Startup.cs new file mode 100644 index 0000000000..5ea6b6d284 --- /dev/null +++ b/SampleDestination/Startup.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace SampleDestination +{ + 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 http://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddCors(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseCors(policy => policy.WithOrigins("http://origin.sample.com:8080").WithMethods("GET")); + app.UseMiddleware(); + } +} +} diff --git a/SampleDestination/StatusMiddleware.cs b/SampleDestination/StatusMiddleware.cs new file mode 100644 index 0000000000..1f47c634ad --- /dev/null +++ b/SampleDestination/StatusMiddleware.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace SampleDestination +{ + public class StatusMiddleware + { + /// + /// Instantiates a new . + /// + /// The next middleware in the pipeline. + public StatusMiddleware(RequestDelegate next) + { + } + + /// + /// Writes the status of the request sent in response. Does not invoke later middleware in the pipeline. + /// + /// The of the current request. + /// A that completes when writing to the response is done. + public Task Invoke(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return context.Response.WriteAsync(context.Response.StatusCode.ToString()); + } + + } +} diff --git a/SampleDestination/hosting.json b/SampleDestination/hosting.json new file mode 100644 index 0000000000..804f92d481 --- /dev/null +++ b/SampleDestination/hosting.json @@ -0,0 +1,3 @@ +{ + "server.urls": "http://destination.sample.com:80" +} \ No newline at end of file diff --git a/SampleDestination/project.json b/SampleDestination/project.json new file mode 100644 index 0000000000..6cb1686ef0 --- /dev/null +++ b/SampleDestination/project.json @@ -0,0 +1,50 @@ +{ + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.1.0-*", + "type": "platform" + }, + "Microsoft.AspNetCore.Diagnostics": "1.2.0-*", + "Microsoft.AspNetCore.Server.IISIntegration": "1.2.0-*", + "Microsoft.AspNetCore.Server.Kestrel": "1.2.0-*", + "Microsoft.Extensions.Logging.Console": "1.2.0-*", + "Microsoft.AspNetCore.Cors": "1.2.0-*", + "Microsoft.Extensions.Configuration.FileExtensions": "1.2.0-*", + "Microsoft.Extensions.Configuration.Json": "1.2.0-preview1-*" + }, + + "tools": { + "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-*" + }, + + "frameworks": { + "netcoreapp1.0": { + "imports": [ + "dotnet5.6", + "portable-net45+win8" + ] + } + }, + + "buildOptions": { + "emitEntryPoint": true, + "preserveCompilationContext": true + }, + + "runtimeOptions": { + "configProperties": { + "System.GC.Server": true + } + }, + + "publishOptions": { + "include": [ + "wwwroot", + "web.config" + ] + }, + + "scripts": { + "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] + } +} diff --git a/SampleDestination/web.config b/SampleDestination/web.config new file mode 100644 index 0000000000..dc0514fca5 --- /dev/null +++ b/SampleDestination/web.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/SampleOrigin/Program.cs b/SampleOrigin/Program.cs new file mode 100644 index 0000000000..68ec70a0b2 --- /dev/null +++ b/SampleOrigin/Program.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; + +namespace SampleOrigin +{ + public class Program + { + public static void Main(string[] args) + { + var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("hosting.json", optional: true) + .Build(); + + var host = new WebHostBuilder() + .UseKestrel() + .UseConfiguration(config) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } + +} diff --git a/SampleOrigin/SampleOrigin.xproj b/SampleOrigin/SampleOrigin.xproj new file mode 100644 index 0000000000..2241431ad0 --- /dev/null +++ b/SampleOrigin/SampleOrigin.xproj @@ -0,0 +1,25 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 99460370-ae5d-4dc9-8dbf-04df66d6b21d + SampleOrigin + .\obj + .\bin\ + v4.5.2 + + + + 2.0 + + + + + + + diff --git a/SampleOrigin/Startup.cs b/SampleOrigin/Startup.cs new file mode 100644 index 0000000000..68a679aec2 --- /dev/null +++ b/SampleOrigin/Startup.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.IO; + +namespace SampleOrigin +{ + 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 http://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseDefaultFiles(); + app.UseStaticFiles(); + } + } +} diff --git a/SampleOrigin/hosting.json b/SampleOrigin/hosting.json new file mode 100644 index 0000000000..7d79e9d594 --- /dev/null +++ b/SampleOrigin/hosting.json @@ -0,0 +1,3 @@ +{ + "server.urls": "http://origin.sample.com:8080" +} \ No newline at end of file diff --git a/SampleOrigin/project.json b/SampleOrigin/project.json new file mode 100644 index 0000000000..130536ee70 --- /dev/null +++ b/SampleOrigin/project.json @@ -0,0 +1,50 @@ +{ + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.1.0-*", + "type": "platform" + }, + "Microsoft.AspNetCore.Diagnostics": "1.2.0-*", + "Microsoft.AspNetCore.Server.IISIntegration": "1.2.0-*", + "Microsoft.AspNetCore.Server.Kestrel": "1.2.0-*", + "Microsoft.Extensions.Logging.Console": "1.2.0-*", + "Microsoft.AspNetCore.StaticFiles": "1.2.0-*", + "Microsoft.Extensions.Configuration.FileExtensions": "1.2.0-*", + "Microsoft.Extensions.Configuration.Json": "1.2.0-*" + }, + + "tools": { + "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-*" + }, + + "frameworks": { + "netcoreapp1.0": { + "imports": [ + "dotnet5.6", + "portable-net45+win8" + ] + } + }, + + "buildOptions": { + "emitEntryPoint": true, + "preserveCompilationContext": true + }, + + "runtimeOptions": { + "configProperties": { + "System.GC.Server": true + } + }, + + "publishOptions": { + "include": [ + "wwwroot", + "web.config" + ] + }, + + "scripts": { + "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] + } +} diff --git a/SampleOrigin/web.config b/SampleOrigin/web.config new file mode 100644 index 0000000000..dc0514fca5 --- /dev/null +++ b/SampleOrigin/web.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/SampleOrigin/wwwroot/Index.html b/SampleOrigin/wwwroot/Index.html new file mode 100644 index 0000000000..3bc55efdbe --- /dev/null +++ b/SampleOrigin/wwwroot/Index.html @@ -0,0 +1,49 @@ + + + + + + + + +
+ +
+
+ +
+ + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs b/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs index 87343b2f9f..b52e000fa8 100644 --- a/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs +++ b/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs @@ -83,8 +83,8 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure if (string.Equals(context.Request.Method, CorsConstants.PreflightHttpMethod, StringComparison.OrdinalIgnoreCase) && !StringValues.IsNullOrEmpty(accessControlRequestMethod)) { - EvaluatePreflightRequest(context, policy, corsResult); _logger?.IsPreflightRequest(); + EvaluatePreflightRequest(context, policy, corsResult); } else { @@ -105,6 +105,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure _logger?.RequestHasOriginHeader(); AddOriginToResult(origin, policy, result); result.SupportsCredentials = policy.SupportsCredentials; + _logger?.PolicySuccess(); AddHeaderValues(result.AllowedExposedHeaders, policy.ExposedHeaders); } @@ -141,7 +142,8 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure if (!found) { - return; + _logger?.PolicyFailure($"Request method {accessControlRequestMethod} not allowed in CORS policy."); + return; } } @@ -150,6 +152,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure !requestHeaders.All(header => CorsConstants.SimpleRequestHeaders.Contains(header, StringComparer.OrdinalIgnoreCase) || policy.Headers.Contains(header, StringComparer.OrdinalIgnoreCase))) { + _logger?.PolicyFailure($"One or more request header(s) not allowed in CORS policy."); return; } @@ -157,7 +160,8 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure result.SupportsCredentials = policy.SupportsCredentials; result.PreflightMaxAge = policy.PreflightMaxAge; result.AllowedMethods.Add(accessControlRequestMethod); - AddHeaderValues(result.AllowedHeaders, requestHeaders); + _logger?.PolicySuccess(); + AddHeaderValues(result.AllowedHeaders, requestHeaders); } /// diff --git a/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs b/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs index 7672797458..dc63aaf2ae 100644 --- a/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs +++ b/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs @@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Cors.Internal _policyFailure = LoggerMessage.Define( LogLevel.Information, 3, - "Policy execution failed. '{FailureReason}'"); + "Policy execution failed. {FailureReason}"); } public static void IsPreflightRequest(this ILogger logger) @@ -46,10 +46,11 @@ namespace Microsoft.AspNetCore.Cors.Internal _requestHasOriginHeader(logger, null); } - public static void PolicySuccess(this ILogger logger, string failureReason) + public static void PolicySuccess(this ILogger logger) { _policySuccess(logger, null); } + public static void PolicyFailure(this ILogger logger, string failureReason) { _policyFailure(logger, failureReason, null); diff --git a/src/Microsoft.AspNetCore.Cors/project.json b/src/Microsoft.AspNetCore.Cors/project.json index fae16fb6a1..c707dd94a2 100644 --- a/src/Microsoft.AspNetCore.Cors/project.json +++ b/src/Microsoft.AspNetCore.Cors/project.json @@ -23,6 +23,7 @@ "Microsoft.AspNetCore.Http.Extensions": "1.2.0-*", "Microsoft.Extensions.Configuration.Abstractions": "1.2.0-*", "Microsoft.Extensions.DependencyInjection.Abstractions": "1.2.0-*", + "Microsoft.Extensions.Logging.Abstractions": "1.2.0-*", "Microsoft.Extensions.Options": "1.2.0-*", "NETStandard.Library": "1.6.1-*" }, From 517dc3c86015415c9efe64395c49b90fa2ec909b Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Wed, 16 Nov 2016 18:25:15 -0800 Subject: [PATCH 03/14] Added tests for logging --- .../Infrastructure/CorsService.cs | 22 +++++- .../CorsServiceTests.cs | 76 +++++++++++++++++++ 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs b/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs index b52e000fa8..72e17d5365 100644 --- a/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs +++ b/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs @@ -97,26 +97,40 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure public virtual void EvaluateRequest(HttpContext context, CorsPolicy policy, CorsResult result) { var origin = context.Request.Headers[CorsConstants.Origin]; - if (StringValues.IsNullOrEmpty(origin) || !policy.AllowAnyOrigin && !policy.Origins.Contains(origin)) + if (StringValues.IsNullOrEmpty(origin)) { return; } + if (!policy.AllowAnyOrigin && !policy.Origins.Contains(origin)) + { + _logger?.RequestHasOriginHeader(); + _logger.PolicyFailure($"Request origin {origin} does not have permission to access the resource."); + return; + } + _logger?.RequestHasOriginHeader(); AddOriginToResult(origin, policy, result); result.SupportsCredentials = policy.SupportsCredentials; - _logger?.PolicySuccess(); AddHeaderValues(result.AllowedExposedHeaders, policy.ExposedHeaders); + _logger?.PolicySuccess(); } public virtual void EvaluatePreflightRequest(HttpContext context, CorsPolicy policy, CorsResult result) { var origin = context.Request.Headers[CorsConstants.Origin]; - if (StringValues.IsNullOrEmpty(origin) || !policy.AllowAnyOrigin && !policy.Origins.Contains(origin)) + if (StringValues.IsNullOrEmpty(origin)) { return; } + if (!policy.AllowAnyOrigin && !policy.Origins.Contains(origin)) + { + _logger?.RequestHasOriginHeader(); + _logger.PolicyFailure($"Request origin {origin} does not have permission to access the resource."); + return; + } + _logger?.RequestHasOriginHeader(); var accessControlRequestMethod = context.Request.Headers[CorsConstants.AccessControlRequestMethod]; if (StringValues.IsNullOrEmpty(accessControlRequestMethod)) @@ -160,8 +174,8 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure result.SupportsCredentials = policy.SupportsCredentials; result.PreflightMaxAge = policy.PreflightMaxAge; result.AllowedMethods.Add(accessControlRequestMethod); + AddHeaderValues(result.AllowedHeaders, requestHeaders); _logger?.PolicySuccess(); - AddHeaderValues(result.AllowedHeaders, requestHeaders); } /// diff --git a/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs b/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs index b3a92a8d68..617155ea43 100644 --- a/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs +++ b/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs @@ -4,6 +4,7 @@ using System; using Microsoft.AspNetCore.Http; using Xunit; +using Microsoft.Extensions.Logging.Testing; namespace Microsoft.AspNetCore.Cors.Infrastructure { @@ -227,6 +228,81 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure Assert.Contains("PUT", result.AllowedMethods); } + public static TheoryData LoggingData + { + get + { + return new TheoryData + { + { + "http://example.com", + "PUT", + null, + "Policy execution failed. Request origin http://example.com does not have permission to access the resource." + }, + { + "http://allowedexample.com", + "DELETE", + null, + "Policy execution failed. Request method DELETE not allowed in CORS policy." + }, + { + "http://allowedexample.com", + "PUT", + new[] { "test" }, + "Policy execution failed. One or more request header(s) not allowed in CORS policy." + }, + { + "http://allowedexample.com", + "PUT", + null, + "Policy execution successful." + }, + }; + } + } + + [Theory] + [MemberData(nameof(LoggingData))] + public void EvaluatePolicy_LoggingForPreflightRequests(string origin, string method, string[] headers, string logMessage) + { + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var corsService = new CorsService(new TestCorsOptions(), loggerFactory); + var requestContext = GetHttpContext(method: "OPTIONS", origin: origin, accessControlRequestMethod: method, accessControlRequestHeaders: headers); + var policy = new CorsPolicy(); + policy.Origins.Add("http://allowedexample.com"); + policy.Methods.Add("PUT"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + Assert.Equal("The request is preflight.", sink.Writes[0].State.ToString()); + Assert.Equal("The request has an origin header.", sink.Writes[1].State.ToString()); + Assert.Equal(logMessage, sink.Writes[2].State.ToString()); + } + + [Theory] + [InlineData("http://allowedexample.com", "Policy execution successful.")] + [InlineData("http://example.com", "Policy execution failed. Request origin http://example.com does not have permission to access the resource.")] + public void EvaluatePolicy_LoggingForRequests(string origin, string logMessage) + { + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var corsService = new CorsService(new TestCorsOptions(), loggerFactory); + var requestContext = GetHttpContext(origin: origin); + var policy = new CorsPolicy(); + policy.Origins.Add("http://allowedexample.com"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + Assert.Equal("The request has an origin header.", sink.Writes[0].State.ToString()); + Assert.Equal(logMessage, sink.Writes[1].State.ToString()); + } + [Theory] [InlineData("OpTions")] [InlineData("OPTIONS")] From 5dcf47ef121a0a779d4805459ffc0aa6ee26a0f6 Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Thu, 17 Nov 2016 17:36:18 -0800 Subject: [PATCH 04/14] PR comments incorporated --- SampleDestination/Startup.cs | 36 ------- SampleDestination/StatusMiddleware.cs | 35 ------- SampleDestination/hosting.json | 3 - SampleOrigin/Startup.cs | 35 ------- SampleOrigin/hosting.json | 3 - samples/README.md | 28 ++++++ .../SampleDestination}/Program.cs | 15 +-- .../SampleDestination.xproj | 0 samples/SampleDestination/Startup.cs | 29 ++++++ .../SampleDestination}/project.json | 5 +- .../SampleDestination}/web.config | 0 .../SampleOrigin}/Program.cs | 15 +-- .../SampleOrigin}/SampleOrigin.xproj | 0 samples/SampleOrigin/Startup.cs | 36 +++++++ .../SampleOrigin}/project.json | 6 +- .../SampleOrigin}/web.config | 0 .../SampleOrigin}/wwwroot/Index.html | 2 +- .../Infrastructure/CorsService.cs | 6 +- .../Internal/CORSLoggerExtensions.cs | 17 +++- .../CorsServiceTests.cs | 94 +++++++++++++++---- 20 files changed, 199 insertions(+), 166 deletions(-) delete mode 100644 SampleDestination/Startup.cs delete mode 100644 SampleDestination/StatusMiddleware.cs delete mode 100644 SampleDestination/hosting.json delete mode 100644 SampleOrigin/Startup.cs delete mode 100644 SampleOrigin/hosting.json create mode 100644 samples/README.md rename {SampleDestination => samples/SampleDestination}/Program.cs (55%) rename {SampleDestination => samples/SampleDestination}/SampleDestination.xproj (100%) create mode 100644 samples/SampleDestination/Startup.cs rename {SampleOrigin => samples/SampleDestination}/project.json (80%) rename {SampleDestination => samples/SampleDestination}/web.config (100%) rename {SampleOrigin => samples/SampleOrigin}/Program.cs (55%) rename {SampleOrigin => samples/SampleOrigin}/SampleOrigin.xproj (100%) create mode 100644 samples/SampleOrigin/Startup.cs rename {SampleDestination => samples/SampleOrigin}/project.json (75%) rename {SampleOrigin => samples/SampleOrigin}/web.config (100%) rename {SampleOrigin => samples/SampleOrigin}/wwwroot/Index.html (95%) diff --git a/SampleDestination/Startup.cs b/SampleDestination/Startup.cs deleted file mode 100644 index 5ea6b6d284..0000000000 --- a/SampleDestination/Startup.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace SampleDestination -{ - 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 http://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - services.AddCors(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) - { - loggerFactory.AddConsole(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseCors(policy => policy.WithOrigins("http://origin.sample.com:8080").WithMethods("GET")); - app.UseMiddleware(); - } -} -} diff --git a/SampleDestination/StatusMiddleware.cs b/SampleDestination/StatusMiddleware.cs deleted file mode 100644 index 1f47c634ad..0000000000 --- a/SampleDestination/StatusMiddleware.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace SampleDestination -{ - public class StatusMiddleware - { - /// - /// Instantiates a new . - /// - /// The next middleware in the pipeline. - public StatusMiddleware(RequestDelegate next) - { - } - - /// - /// Writes the status of the request sent in response. Does not invoke later middleware in the pipeline. - /// - /// The of the current request. - /// A that completes when writing to the response is done. - public Task Invoke(HttpContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - return context.Response.WriteAsync(context.Response.StatusCode.ToString()); - } - - } -} diff --git a/SampleDestination/hosting.json b/SampleDestination/hosting.json deleted file mode 100644 index 804f92d481..0000000000 --- a/SampleDestination/hosting.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "server.urls": "http://destination.sample.com:80" -} \ No newline at end of file diff --git a/SampleOrigin/Startup.cs b/SampleOrigin/Startup.cs deleted file mode 100644 index 68a679aec2..0000000000 --- a/SampleOrigin/Startup.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System.IO; - -namespace SampleOrigin -{ - 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 http://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) - { - loggerFactory.AddConsole(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseDefaultFiles(); - app.UseStaticFiles(); - } - } -} diff --git a/SampleOrigin/hosting.json b/SampleOrigin/hosting.json deleted file mode 100644 index 7d79e9d594..0000000000 --- a/SampleOrigin/hosting.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "server.urls": "http://origin.sample.com:8080" -} \ No newline at end of file diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 0000000000..c0b4a4414e --- /dev/null +++ b/samples/README.md @@ -0,0 +1,28 @@ +CORS Sample +=== +This sample consists of a request origin (SampleOrigin) and a request destination (SampleDestination). +Both have different domain names, to simulate a CORS request. + +Modify Hosts File +Windows: +Run a text editor (e.g. Notepad) as an Administrator. Open the hosts file on the path: "C:\Windows\System32\drivers\etc\hosts". + +Linux: +On a Terminal window, type "sudo nano /etc/hosts" and enter your admin password when prompted. + +In the hosts file, add the following to the bottom of the file: +127.0.0.1 destination.example.com +127.0.0.1 origin.example.com + +Save the file and close it. Then clear your browser history. + +Run the sample +*In a command prompt window, open the directory where you cloned the repository, and open the SampleDestination directory. Run the command: dotnet run +*Repeat the above step in the SampleOrigin directory. +*Open a browser window and go to http://origin.example.com +*Click the button to see CORS in action. + +If using Visual Studio to launch the request origin: +Open Visual Studio and in the launchSettings.json file for the SampleOrigin project, change the launchUrl under SampleOrigin to +http://origin.example.com:8080. Using the dropdown near the Start button, choose SampleOrigin before pressing Start to ensure that it uses Kestrel +and not IIS Express. diff --git a/SampleDestination/Program.cs b/samples/SampleDestination/Program.cs similarity index 55% rename from SampleDestination/Program.cs rename to samples/SampleDestination/Program.cs index 972cde8b8b..d964f308f5 100644 --- a/SampleDestination/Program.cs +++ b/samples/SampleDestination/Program.cs @@ -1,10 +1,8 @@ -using System; -using System.Collections.Generic; +// 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.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; namespace SampleDestination { @@ -12,14 +10,9 @@ namespace SampleDestination { public static void Main(string[] args) { - var config = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("hosting.json", optional: true) - .Build(); - var host = new WebHostBuilder() .UseKestrel() - .UseConfiguration(config) + .UseUrls("http://*:5000") .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup() diff --git a/SampleDestination/SampleDestination.xproj b/samples/SampleDestination/SampleDestination.xproj similarity index 100% rename from SampleDestination/SampleDestination.xproj rename to samples/SampleDestination/SampleDestination.xproj diff --git a/samples/SampleDestination/Startup.cs b/samples/SampleDestination/Startup.cs new file mode 100644 index 0000000000..1055b9f224 --- /dev/null +++ b/samples/SampleDestination/Startup.cs @@ -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.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace SampleDestination +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddCors(); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(); + app.UseCors(policy => policy.WithOrigins("http://origin.example.com:8080")); + app.Run(async context => + { + await context.Response.WriteAsync("Status code of your request: " + context.Response.StatusCode.ToString()); + }); + } + } +} diff --git a/SampleOrigin/project.json b/samples/SampleDestination/project.json similarity index 80% rename from SampleOrigin/project.json rename to samples/SampleDestination/project.json index 130536ee70..4e96e2c773 100644 --- a/SampleOrigin/project.json +++ b/samples/SampleDestination/project.json @@ -4,13 +4,10 @@ "version": "1.1.0-*", "type": "platform" }, - "Microsoft.AspNetCore.Diagnostics": "1.2.0-*", "Microsoft.AspNetCore.Server.IISIntegration": "1.2.0-*", "Microsoft.AspNetCore.Server.Kestrel": "1.2.0-*", "Microsoft.Extensions.Logging.Console": "1.2.0-*", - "Microsoft.AspNetCore.StaticFiles": "1.2.0-*", - "Microsoft.Extensions.Configuration.FileExtensions": "1.2.0-*", - "Microsoft.Extensions.Configuration.Json": "1.2.0-*" + "Microsoft.AspNetCore.Cors": "1.2.0-*" }, "tools": { diff --git a/SampleDestination/web.config b/samples/SampleDestination/web.config similarity index 100% rename from SampleDestination/web.config rename to samples/SampleDestination/web.config diff --git a/SampleOrigin/Program.cs b/samples/SampleOrigin/Program.cs similarity index 55% rename from SampleOrigin/Program.cs rename to samples/SampleOrigin/Program.cs index 68ec70a0b2..3a51bfdf8b 100644 --- a/SampleOrigin/Program.cs +++ b/samples/SampleOrigin/Program.cs @@ -1,10 +1,8 @@ -using System; -using System.Collections.Generic; +// 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.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; namespace SampleOrigin { @@ -12,14 +10,9 @@ namespace SampleOrigin { public static void Main(string[] args) { - var config = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("hosting.json", optional: true) - .Build(); - var host = new WebHostBuilder() .UseKestrel() - .UseConfiguration(config) + .UseUrls("http://*:8080") .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup() diff --git a/SampleOrigin/SampleOrigin.xproj b/samples/SampleOrigin/SampleOrigin.xproj similarity index 100% rename from SampleOrigin/SampleOrigin.xproj rename to samples/SampleOrigin/SampleOrigin.xproj diff --git a/samples/SampleOrigin/Startup.cs b/samples/SampleOrigin/Startup.cs new file mode 100644 index 0000000000..01e05c1b4d --- /dev/null +++ b/samples/SampleOrigin/Startup.cs @@ -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 Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace SampleOrigin +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(); + app.Run( context => + { + var fileInfoProvider = env.WebRootFileProvider; + var fileInfo = fileInfoProvider.GetFileInfo("/Index.html"); + context.Response.Headers.Add("Content-Type", "text/html; charset=utf-8"); + return context.Response.SendFileAsync(fileInfo); + }); + + app.Run(async context => + { + await context.Response.WriteAsync("Status code of your request: " + context.Response.StatusCode.ToString()); + }); + + } + } +} diff --git a/SampleDestination/project.json b/samples/SampleOrigin/project.json similarity index 75% rename from SampleDestination/project.json rename to samples/SampleOrigin/project.json index 6cb1686ef0..b4165727fd 100644 --- a/SampleDestination/project.json +++ b/samples/SampleOrigin/project.json @@ -4,13 +4,9 @@ "version": "1.1.0-*", "type": "platform" }, - "Microsoft.AspNetCore.Diagnostics": "1.2.0-*", "Microsoft.AspNetCore.Server.IISIntegration": "1.2.0-*", "Microsoft.AspNetCore.Server.Kestrel": "1.2.0-*", - "Microsoft.Extensions.Logging.Console": "1.2.0-*", - "Microsoft.AspNetCore.Cors": "1.2.0-*", - "Microsoft.Extensions.Configuration.FileExtensions": "1.2.0-*", - "Microsoft.Extensions.Configuration.Json": "1.2.0-preview1-*" + "Microsoft.Extensions.Logging.Console": "1.2.0-*" }, "tools": { diff --git a/SampleOrigin/web.config b/samples/SampleOrigin/web.config similarity index 100% rename from SampleOrigin/web.config rename to samples/SampleOrigin/web.config diff --git a/SampleOrigin/wwwroot/Index.html b/samples/SampleOrigin/wwwroot/Index.html similarity index 95% rename from SampleOrigin/wwwroot/Index.html rename to samples/SampleOrigin/wwwroot/Index.html index 3bc55efdbe..d9d8b74672 100644 --- a/SampleOrigin/wwwroot/Index.html +++ b/samples/SampleOrigin/wwwroot/Index.html @@ -14,7 +14,7 @@ function makeCORSRequest(method) { // Destination server with CORS enabled. - var url = 'http://destination.sample.com/api'; + var url = 'http://destination.example.com:5000/api'; var request = createCORSRequest(method , url); if (!request) { diff --git a/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs b/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs index 72e17d5365..c8a8e54d4e 100644 --- a/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs +++ b/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs @@ -99,13 +99,14 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure var origin = context.Request.Headers[CorsConstants.Origin]; if (StringValues.IsNullOrEmpty(origin)) { + _logger?.RequestDoesNotHaveOriginHeader(); return; } if (!policy.AllowAnyOrigin && !policy.Origins.Contains(origin)) { _logger?.RequestHasOriginHeader(); - _logger.PolicyFailure($"Request origin {origin} does not have permission to access the resource."); + _logger?.PolicyFailure($"Request origin {origin} does not have permission to access the resource."); return; } @@ -121,13 +122,14 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure var origin = context.Request.Headers[CorsConstants.Origin]; if (StringValues.IsNullOrEmpty(origin)) { + _logger?.RequestDoesNotHaveOriginHeader(); return; } if (!policy.AllowAnyOrigin && !policy.Origins.Contains(origin)) { _logger?.RequestHasOriginHeader(); - _logger.PolicyFailure($"Request origin {origin} does not have permission to access the resource."); + _logger?.PolicyFailure($"Request origin {origin} does not have permission to access the resource."); return; } diff --git a/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs b/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs index dc63aaf2ae..e744e95565 100644 --- a/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs +++ b/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs @@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Cors.Internal { private static readonly Action _isPreflightRequest; private static readonly Action _requestHasOriginHeader; + private static readonly Action _requestDoesNotHaveOriginHeader; private static readonly Action _policySuccess; private static readonly Action _policyFailure; @@ -18,21 +19,26 @@ namespace Microsoft.AspNetCore.Cors.Internal _isPreflightRequest = LoggerMessage.Define( LogLevel.Debug, 1, - "The request is preflight."); + "This is a preflight request."); _requestHasOriginHeader = LoggerMessage.Define( LogLevel.Debug, 2, "The request has an origin header."); + _requestDoesNotHaveOriginHeader = LoggerMessage.Define( + LogLevel.Debug, + 3, + "The request does not have an origin header."); + _policySuccess = LoggerMessage.Define( LogLevel.Information, - 3, + 4, "Policy execution successful."); _policyFailure = LoggerMessage.Define( LogLevel.Information, - 3, + 5, "Policy execution failed. {FailureReason}"); } @@ -46,6 +52,11 @@ namespace Microsoft.AspNetCore.Cors.Internal _requestHasOriginHeader(logger, null); } + public static void RequestDoesNotHaveOriginHeader(this ILogger logger) + { + _requestDoesNotHaveOriginHeader(logger, null); + } + public static void PolicySuccess(this ILogger logger) { _policySuccess(logger, null); diff --git a/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs b/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs index 617155ea43..d20adfaa4b 100644 --- a/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs +++ b/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs @@ -228,34 +228,38 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure Assert.Contains("PUT", result.AllowedMethods); } - public static TheoryData LoggingData + public static TheoryData PreflightRequests_LoggingData { get { - return new TheoryData + return new TheoryData { { "http://example.com", "PUT", null, + "The request has an origin header.", "Policy execution failed. Request origin http://example.com does not have permission to access the resource." }, { - "http://allowedexample.com", + "http://allowed.example.com", "DELETE", null, + "The request has an origin header.", "Policy execution failed. Request method DELETE not allowed in CORS policy." }, { - "http://allowedexample.com", + "http://allowed.example.com", "PUT", new[] { "test" }, + "The request has an origin header.", "Policy execution failed. One or more request header(s) not allowed in CORS policy." }, { - "http://allowedexample.com", + "http://allowed.example.com", "PUT", null, + "The request has an origin header.", "Policy execution successful." }, }; @@ -263,8 +267,8 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure } [Theory] - [MemberData(nameof(LoggingData))] - public void EvaluatePolicy_LoggingForPreflightRequests(string origin, string method, string[] headers, string logMessage) + [MemberData(nameof(PreflightRequests_LoggingData))] + public void EvaluatePolicy_LoggingForPreflightRequests_HasOriginHeader(string origin, string method, string[] headers, string originLogMessage, string policyLogMessage) { var sink = new TestSink(); var loggerFactory = new TestLoggerFactory(sink, enabled: true); @@ -272,21 +276,59 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure var corsService = new CorsService(new TestCorsOptions(), loggerFactory); var requestContext = GetHttpContext(method: "OPTIONS", origin: origin, accessControlRequestMethod: method, accessControlRequestHeaders: headers); var policy = new CorsPolicy(); - policy.Origins.Add("http://allowedexample.com"); + policy.Origins.Add("http://allowed.example.com"); policy.Methods.Add("PUT"); // Act var result = corsService.EvaluatePolicy(requestContext, policy); - Assert.Equal("The request is preflight.", sink.Writes[0].State.ToString()); - Assert.Equal("The request has an origin header.", sink.Writes[1].State.ToString()); - Assert.Equal(logMessage, sink.Writes[2].State.ToString()); + Assert.Equal("This is a preflight request.", sink.Writes[0].State.ToString()); + Assert.Equal(originLogMessage, sink.Writes[1].State.ToString()); + Assert.Equal(policyLogMessage, sink.Writes[2].State.ToString()); + } + + [Fact] + public void EvaluatePolicy_LoggingForPreflightRequests_DoesNotHaveOriginHeader() + { + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var corsService = new CorsService(new TestCorsOptions(), loggerFactory); + var requestContext = GetHttpContext(method: "OPTIONS", origin: null, accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add("http://allowed.example.com"); + policy.Methods.Add("PUT"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + Assert.Equal("This is a preflight request.", sink.Writes[0].State.ToString()); + Assert.Equal("The request does not have an origin header.", sink.Writes[1].State.ToString()); + } + + public static TheoryData NonPreflightRequests_LoggingData + { + get + { + return new TheoryData + { + { + "http://example.com", + "The request has an origin header.", + "Policy execution failed. Request origin http://example.com does not have permission to access the resource." + }, + { + "http://allowed.example.com", + "The request has an origin header.", + "Policy execution successful." + } + }; + } } [Theory] - [InlineData("http://allowedexample.com", "Policy execution successful.")] - [InlineData("http://example.com", "Policy execution failed. Request origin http://example.com does not have permission to access the resource.")] - public void EvaluatePolicy_LoggingForRequests(string origin, string logMessage) + [MemberData(nameof(NonPreflightRequests_LoggingData))] + public void EvaluatePolicy_LoggingForNonPreflightRequests_HasOriginHeader(string origin, string originlogMessage, string policyLogMessage) { var sink = new TestSink(); var loggerFactory = new TestLoggerFactory(sink, enabled: true); @@ -294,13 +336,31 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure var corsService = new CorsService(new TestCorsOptions(), loggerFactory); var requestContext = GetHttpContext(origin: origin); var policy = new CorsPolicy(); - policy.Origins.Add("http://allowedexample.com"); + policy.Origins.Add("http://allowed.example.com"); // Act var result = corsService.EvaluatePolicy(requestContext, policy); - Assert.Equal("The request has an origin header.", sink.Writes[0].State.ToString()); - Assert.Equal(logMessage, sink.Writes[1].State.ToString()); + Assert.Equal(originlogMessage, sink.Writes[0].State.ToString()); + Assert.Equal(policyLogMessage, sink.Writes[1].State.ToString()); + } + + [Fact] + public void EvaluatePolicy_LoggingForNonPreflightRequests_DoesNotHaveOriginHeader() + { + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var corsService = new CorsService(new TestCorsOptions(), loggerFactory); + var requestContext = GetHttpContext(origin: null); + var policy = new CorsPolicy(); + policy.Origins.Add("http://allowed.example.com"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + var logMessage = Assert.Single(sink.Writes); + Assert.Equal("The request does not have an origin header.", logMessage.State.ToString()); } [Theory] From b3bdba15591145856925a19a7f2d9c6c57891acf Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Mon, 21 Nov 2016 15:31:14 -0800 Subject: [PATCH 05/14] PR comments incorporated. Working on making SampleDestination dynamic- define the policy within the sample through a UI. --- CORS.sln | 4 +- samples/README.md | 8 +- samples/SampleDestination/Program.cs | 1 - samples/SampleDestination/Startup.cs | 16 +- samples/SampleDestination/project.json | 12 +- samples/SampleDestination/web.config | 14 -- samples/SampleOrigin/Program.cs | 3 +- samples/SampleOrigin/project.json | 12 +- samples/SampleOrigin/web.config | 11 +- samples/SampleOrigin/wwwroot/Index.html | 94 +++++++++--- .../Infrastructure/CorsService.cs | 35 +++-- .../Internal/CORSLoggerExtensions.cs | 55 +++++-- .../CorsServiceTests.cs | 143 +++++++++++------- 13 files changed, 253 insertions(+), 155 deletions(-) delete mode 100644 samples/SampleDestination/web.config diff --git a/CORS.sln b/CORS.sln index 87e766660f..c471720431 100644 --- a/CORS.sln +++ b/CORS.sln @@ -21,9 +21,9 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CorsMiddlewareWebSite", "te EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{960E0703-A8A5-44DF-AA87-B7C614683B3C}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SampleDestination", "SampleDestination\SampleDestination.xproj", "{F6675DC1-AA21-453B-89B6-DA425FB9C3A5}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SampleDestination", "samples\SampleDestination\SampleDestination.xproj", "{F6675DC1-AA21-453B-89B6-DA425FB9C3A5}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SampleOrigin", "SampleOrigin\SampleOrigin.xproj", "{99460370-AE5D-4DC9-8DBF-04DF66D6B21D}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SampleOrigin", "samples\SampleOrigin\SampleOrigin.xproj", "{99460370-AE5D-4DC9-8DBF-04DF66D6B21D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/samples/README.md b/samples/README.md index c0b4a4414e..ce871c4940 100644 --- a/samples/README.md +++ b/samples/README.md @@ -19,9 +19,15 @@ Save the file and close it. Then clear your browser history. Run the sample *In a command prompt window, open the directory where you cloned the repository, and open the SampleDestination directory. Run the command: dotnet run *Repeat the above step in the SampleOrigin directory. -*Open a browser window and go to http://origin.example.com +*Open a browser window and go to http://origin.example.com:5001 *Click the button to see CORS in action. +The SampleOrigin application will use port 5001, and SampleDestination will use 5000. Please ensure there are no other processes using those ports before running the CORS sample. + +As an example, apart from GET, HEAD and POST requests, PUT requests are allowed in the CORS policy on SampleDestination. Any others, like DELETE, OPTIONS etc. are not allowed and throw an error. +Content-Length has been added as an allowed header to the sample. Any other headers are not allowed and throw an error. +To edit the policy, please see app.UseCors() method in the Startup.cs file of SampleDestination. + If using Visual Studio to launch the request origin: Open Visual Studio and in the launchSettings.json file for the SampleOrigin project, change the launchUrl under SampleOrigin to http://origin.example.com:8080. Using the dropdown near the Start button, choose SampleOrigin before pressing Start to ensure that it uses Kestrel diff --git a/samples/SampleDestination/Program.cs b/samples/SampleDestination/Program.cs index d964f308f5..c196d5b838 100644 --- a/samples/SampleDestination/Program.cs +++ b/samples/SampleDestination/Program.cs @@ -14,7 +14,6 @@ namespace SampleDestination .UseKestrel() .UseUrls("http://*:5000") .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() .UseStartup() .Build(); diff --git a/samples/SampleDestination/Startup.cs b/samples/SampleDestination/Startup.cs index 1055b9f224..199bb16795 100644 --- a/samples/SampleDestination/Startup.cs +++ b/samples/SampleDestination/Startup.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -19,10 +20,21 @@ namespace SampleDestination public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(); - app.UseCors(policy => policy.WithOrigins("http://origin.example.com:8080")); + + app.UseCors(policy => policy + .WithOrigins("http://origin.example.com:5001") + .WithMethods("PUT") + .WithHeaders("Content-Length")); + app.Run(async context => { - await context.Response.WriteAsync("Status code of your request: " + context.Response.StatusCode.ToString()); + var responseHeaders = context.Response.Headers; + foreach (var responseHeader in responseHeaders) + { + await context.Response.WriteAsync("\n"+responseHeader.Key+": "+responseHeader.Value); + } + + await context.Response.WriteAsync("\nStatus code of your request: " + context.Response.StatusCode.ToString()); }); } } diff --git a/samples/SampleDestination/project.json b/samples/SampleDestination/project.json index 4e96e2c773..3c86825eb6 100644 --- a/samples/SampleDestination/project.json +++ b/samples/SampleDestination/project.json @@ -4,16 +4,11 @@ "version": "1.1.0-*", "type": "platform" }, - "Microsoft.AspNetCore.Server.IISIntegration": "1.2.0-*", "Microsoft.AspNetCore.Server.Kestrel": "1.2.0-*", "Microsoft.Extensions.Logging.Console": "1.2.0-*", "Microsoft.AspNetCore.Cors": "1.2.0-*" }, - "tools": { - "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-*" - }, - "frameworks": { "netcoreapp1.0": { "imports": [ @@ -36,12 +31,7 @@ "publishOptions": { "include": [ - "wwwroot", - "web.config" + "wwwroot" ] - }, - - "scripts": { - "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] } } diff --git a/samples/SampleDestination/web.config b/samples/SampleDestination/web.config deleted file mode 100644 index dc0514fca5..0000000000 --- a/samples/SampleDestination/web.config +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - diff --git a/samples/SampleOrigin/Program.cs b/samples/SampleOrigin/Program.cs index 3a51bfdf8b..1a6960a91f 100644 --- a/samples/SampleOrigin/Program.cs +++ b/samples/SampleOrigin/Program.cs @@ -12,9 +12,8 @@ namespace SampleOrigin { var host = new WebHostBuilder() .UseKestrel() - .UseUrls("http://*:8080") + .UseUrls("http://*:5001") .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() .UseStartup() .Build(); diff --git a/samples/SampleOrigin/project.json b/samples/SampleOrigin/project.json index b4165727fd..259fb617c3 100644 --- a/samples/SampleOrigin/project.json +++ b/samples/SampleOrigin/project.json @@ -4,15 +4,10 @@ "version": "1.1.0-*", "type": "platform" }, - "Microsoft.AspNetCore.Server.IISIntegration": "1.2.0-*", "Microsoft.AspNetCore.Server.Kestrel": "1.2.0-*", "Microsoft.Extensions.Logging.Console": "1.2.0-*" }, - "tools": { - "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-*" - }, - "frameworks": { "netcoreapp1.0": { "imports": [ @@ -35,12 +30,7 @@ "publishOptions": { "include": [ - "wwwroot", - "web.config" + "wwwroot" ] - }, - - "scripts": { - "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] } } diff --git a/samples/SampleOrigin/web.config b/samples/SampleOrigin/web.config index dc0514fca5..e04a0397bf 100644 --- a/samples/SampleOrigin/web.config +++ b/samples/SampleOrigin/web.config @@ -1,14 +1,9 @@  - - - - + - + - + \ No newline at end of file diff --git a/samples/SampleOrigin/wwwroot/Index.html b/samples/SampleOrigin/wwwroot/Index.html index d9d8b74672..dd2c4f0ae9 100644 --- a/samples/SampleOrigin/wwwroot/Index.html +++ b/samples/SampleOrigin/wwwroot/Index.html @@ -2,30 +2,56 @@ - - - - -
- -
-
- -
+

CORS Sample

+ Method:

+ Header Name: Header Value:

+ + +



+ + Method DELETE is not allowed: + Method PUT is allowed:

+ + Header 'Max-Forwards' not supported: + Header 'Content-Length' is supported:

\ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs b/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs index c8a8e54d4e..32aea828c5 100644 --- a/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs +++ b/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure ///
/// The option model representing . public CorsService(IOptions options) - :this(options, loggerFactory: null) + : this(options, loggerFactory: null) { } @@ -103,14 +103,14 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure return; } + _logger?.RequestHasOriginHeader(origin); if (!policy.AllowAnyOrigin && !policy.Origins.Contains(origin)) { - _logger?.RequestHasOriginHeader(); - _logger?.PolicyFailure($"Request origin {origin} does not have permission to access the resource."); + _logger?.PolicyFailure(); + _logger?.OriginNotAllowed(origin); return; } - _logger?.RequestHasOriginHeader(); AddOriginToResult(origin, policy, result); result.SupportsCredentials = policy.SupportsCredentials; AddHeaderValues(result.AllowedExposedHeaders, policy.ExposedHeaders); @@ -126,14 +126,14 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure return; } + _logger?.RequestHasOriginHeader(origin); if (!policy.AllowAnyOrigin && !policy.Origins.Contains(origin)) { - _logger?.RequestHasOriginHeader(); - _logger?.PolicyFailure($"Request origin {origin} does not have permission to access the resource."); + _logger?.PolicyFailure(); + _logger?.OriginNotAllowed(origin); return; } - _logger?.RequestHasOriginHeader(); var accessControlRequestMethod = context.Request.Headers[CorsConstants.AccessControlRequestMethod]; if (StringValues.IsNullOrEmpty(accessControlRequestMethod)) { @@ -158,18 +158,25 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure if (!found) { - _logger?.PolicyFailure($"Request method {accessControlRequestMethod} not allowed in CORS policy."); - return; + _logger?.PolicyFailure(); + _logger?.AccessControlMethodNotAllowed(accessControlRequestMethod); + return; } } if (!policy.AllowAnyHeader && - requestHeaders != null && - !requestHeaders.All(header => CorsConstants.SimpleRequestHeaders.Contains(header, StringComparer.OrdinalIgnoreCase) || - policy.Headers.Contains(header, StringComparer.OrdinalIgnoreCase))) + requestHeaders != null) { - _logger?.PolicyFailure($"One or more request header(s) not allowed in CORS policy."); - return; + foreach (var requestHeader in requestHeaders) + { + if (!CorsConstants.SimpleRequestHeaders.Contains(requestHeader, StringComparer.OrdinalIgnoreCase) && + !policy.Headers.Contains(requestHeader, StringComparer.OrdinalIgnoreCase)) + { + _logger?.PolicyFailure(); + _logger?.RequestHeaderNotAllowed(requestHeader); + return; + } + } } AddOriginToResult(origin, policy, result); diff --git a/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs b/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs index e744e95565..727d19a4ea 100644 --- a/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs +++ b/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs @@ -9,22 +9,25 @@ namespace Microsoft.AspNetCore.Cors.Internal internal static class CORSLoggerExtensions { private static readonly Action _isPreflightRequest; - private static readonly Action _requestHasOriginHeader; + private static readonly Action _requestHasOriginHeader; private static readonly Action _requestDoesNotHaveOriginHeader; private static readonly Action _policySuccess; - private static readonly Action _policyFailure; + private static readonly Action _policyFailure; + private static readonly Action _originNotAllowed; + private static readonly Action _accessControlMethodNotAllowed; + private static readonly Action _requestHeaderNotAllowed; static CORSLoggerExtensions() { _isPreflightRequest = LoggerMessage.Define( LogLevel.Debug, 1, - "This is a preflight request."); + "The request is a preflight request."); - _requestHasOriginHeader = LoggerMessage.Define( + _requestHasOriginHeader = LoggerMessage.Define( LogLevel.Debug, 2, - "The request has an origin header."); + "The request has an origin header: '{origin}'."); _requestDoesNotHaveOriginHeader = LoggerMessage.Define( LogLevel.Debug, @@ -36,10 +39,25 @@ namespace Microsoft.AspNetCore.Cors.Internal 4, "Policy execution successful."); - _policyFailure = LoggerMessage.Define( + _policyFailure = LoggerMessage.Define( LogLevel.Information, 5, - "Policy execution failed. {FailureReason}"); + "Policy execution failed."); + + _originNotAllowed = LoggerMessage.Define( + LogLevel.Information, + 6, + "Request origin {origin} does not have permission to access the resource."); + + _accessControlMethodNotAllowed = LoggerMessage.Define( + LogLevel.Information, + 7, + "Request method {accessControlRequestMethod} not allowed in CORS policy."); + + _requestHeaderNotAllowed = LoggerMessage.Define( + LogLevel.Information, + 8, + "Request header '{requestHeader}' not allowed in CORS policy."); } public static void IsPreflightRequest(this ILogger logger) @@ -47,9 +65,9 @@ namespace Microsoft.AspNetCore.Cors.Internal _isPreflightRequest(logger, null); } - public static void RequestHasOriginHeader(this ILogger logger) + public static void RequestHasOriginHeader(this ILogger logger, string origin) { - _requestHasOriginHeader(logger, null); + _requestHasOriginHeader(logger, origin, null); } public static void RequestDoesNotHaveOriginHeader(this ILogger logger) @@ -62,9 +80,24 @@ namespace Microsoft.AspNetCore.Cors.Internal _policySuccess(logger, null); } - public static void PolicyFailure(this ILogger logger, string failureReason) + public static void PolicyFailure(this ILogger logger) { - _policyFailure(logger, failureReason, null); + _policyFailure(logger, null); + } + + public static void OriginNotAllowed(this ILogger logger, string origin) + { + _originNotAllowed(logger, origin, null); + } + + public static void AccessControlMethodNotAllowed(this ILogger logger, string accessControlMethod) + { + _accessControlMethodNotAllowed(logger, accessControlMethod, null); + } + + public static void RequestHeaderNotAllowed(this ILogger logger, string requestHeader) + { + _requestHeaderNotAllowed(logger, requestHeader, null); } } } diff --git a/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs b/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs index d20adfaa4b..b3ce5927d8 100644 --- a/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs +++ b/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs @@ -228,39 +228,41 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure Assert.Contains("PUT", result.AllowedMethods); } - public static TheoryData PreflightRequests_LoggingData + public static TheoryData PreflightRequests_LoggingData { get { - return new TheoryData + return new TheoryData { { - "http://example.com", - "PUT", - null, - "The request has an origin header.", - "Policy execution failed. Request origin http://example.com does not have permission to access the resource." + new LogData { + origin = "http://example.com", + method = "PUT", + headers = null, + originLogMessage = "The request has an origin header: 'http://example.com'.", + policyLogMessage = "Policy execution failed.", + failureReason = "Request origin http://example.com does not have permission to access the resource." + } }, { - "http://allowed.example.com", - "DELETE", - null, - "The request has an origin header.", - "Policy execution failed. Request method DELETE not allowed in CORS policy." + new LogData { + origin = "http://allowed.example.com", + method = "DELETE", + headers = null, + originLogMessage = "The request has an origin header: 'http://allowed.example.com'.", + policyLogMessage = "Policy execution failed.", + failureReason = "Request method DELETE not allowed in CORS policy." + } }, { - "http://allowed.example.com", - "PUT", - new[] { "test" }, - "The request has an origin header.", - "Policy execution failed. One or more request header(s) not allowed in CORS policy." - }, - { - "http://allowed.example.com", - "PUT", - null, - "The request has an origin header.", - "Policy execution successful." + new LogData { + origin = "http://allowed.example.com", + method = "PUT", + headers = new[] { "test" }, + originLogMessage = "The request has an origin header: 'http://allowed.example.com'.", + policyLogMessage = "Policy execution failed.", + failureReason = "Request header 'test' not allowed in CORS policy." + } }, }; } @@ -268,13 +270,13 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure [Theory] [MemberData(nameof(PreflightRequests_LoggingData))] - public void EvaluatePolicy_LoggingForPreflightRequests_HasOriginHeader(string origin, string method, string[] headers, string originLogMessage, string policyLogMessage) + public void EvaluatePolicy_LoggingForPreflightRequests_HasOriginHeader_PolicyFailed(LogData logData) { var sink = new TestSink(); var loggerFactory = new TestLoggerFactory(sink, enabled: true); var corsService = new CorsService(new TestCorsOptions(), loggerFactory); - var requestContext = GetHttpContext(method: "OPTIONS", origin: origin, accessControlRequestMethod: method, accessControlRequestHeaders: headers); + var requestContext = GetHttpContext(method: "OPTIONS", origin: logData.origin, accessControlRequestMethod: logData.method, accessControlRequestHeaders: logData.headers); var policy = new CorsPolicy(); policy.Origins.Add("http://allowed.example.com"); policy.Methods.Add("PUT"); @@ -282,9 +284,30 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure // Act var result = corsService.EvaluatePolicy(requestContext, policy); - Assert.Equal("This is a preflight request.", sink.Writes[0].State.ToString()); - Assert.Equal(originLogMessage, sink.Writes[1].State.ToString()); - Assert.Equal(policyLogMessage, sink.Writes[2].State.ToString()); + Assert.Equal("The request is a preflight request.", sink.Writes[0].State.ToString()); + Assert.Equal(logData.originLogMessage, sink.Writes[1].State.ToString()); + Assert.Equal(logData.policyLogMessage, sink.Writes[2].State.ToString()); + Assert.Equal(logData.failureReason, sink.Writes[3].State.ToString()); + } + + [Fact] + public void EvaluatePolicy_LoggingForPreflightRequests_HasOriginHeader_PolicySucceeded() + { + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var corsService = new CorsService(new TestCorsOptions(), loggerFactory); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://allowed.example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add("http://allowed.example.com"); + policy.Methods.Add("PUT"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + Assert.Equal("The request is a preflight request.", sink.Writes[0].State.ToString()); + Assert.Equal("The request has an origin header: 'http://allowed.example.com'.", sink.Writes[1].State.ToString()); + Assert.Equal("Policy execution successful.", sink.Writes[2].State.ToString()); } [Fact] @@ -302,47 +325,45 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure // Act var result = corsService.EvaluatePolicy(requestContext, policy); - Assert.Equal("This is a preflight request.", sink.Writes[0].State.ToString()); + Assert.Equal("The request is a preflight request.", sink.Writes[0].State.ToString()); Assert.Equal("The request does not have an origin header.", sink.Writes[1].State.ToString()); } - public static TheoryData NonPreflightRequests_LoggingData - { - get - { - return new TheoryData - { - { - "http://example.com", - "The request has an origin header.", - "Policy execution failed. Request origin http://example.com does not have permission to access the resource." - }, - { - "http://allowed.example.com", - "The request has an origin header.", - "Policy execution successful." - } - }; - } - } - - [Theory] - [MemberData(nameof(NonPreflightRequests_LoggingData))] - public void EvaluatePolicy_LoggingForNonPreflightRequests_HasOriginHeader(string origin, string originlogMessage, string policyLogMessage) + [Fact] + public void EvaluatePolicy_LoggingForNonPreflightRequests_HasOriginHeader_PolicyFailed() { var sink = new TestSink(); var loggerFactory = new TestLoggerFactory(sink, enabled: true); var corsService = new CorsService(new TestCorsOptions(), loggerFactory); - var requestContext = GetHttpContext(origin: origin); + var requestContext = GetHttpContext(origin: "http://example.com"); var policy = new CorsPolicy(); policy.Origins.Add("http://allowed.example.com"); // Act var result = corsService.EvaluatePolicy(requestContext, policy); - Assert.Equal(originlogMessage, sink.Writes[0].State.ToString()); - Assert.Equal(policyLogMessage, sink.Writes[1].State.ToString()); + Assert.Equal("The request has an origin header: 'http://example.com'.", sink.Writes[0].State.ToString()); + Assert.Equal("Policy execution failed.", sink.Writes[1].State.ToString()); + Assert.Equal("Request origin http://example.com does not have permission to access the resource.", sink.Writes[2].State.ToString()); + } + + [Fact] + public void EvaluatePolicy_LoggingForNonPreflightRequests_HasOriginHeader_PolicySucceeded() + { + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var corsService = new CorsService(new TestCorsOptions(), loggerFactory); + var requestContext = GetHttpContext(origin: "http://allowed.example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add("http://allowed.example.com"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + Assert.Equal("The request has an origin header: 'http://allowed.example.com'.", sink.Writes[0].State.ToString()); + Assert.Equal("Policy execution successful.", sink.Writes[1].State.ToString()); } [Fact] @@ -1049,5 +1070,15 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure return context; } + + public struct LogData + { + public string origin { get; set; } + public string method { get; set; } + public string[] headers { get; set; } + public string originLogMessage { get; set; } + public string policyLogMessage { get; set; } + public string failureReason { get; set; } + } } } \ No newline at end of file From 34aa1c572216438f07e46e21568416a7c30a377b Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Mon, 21 Nov 2016 15:38:39 -0800 Subject: [PATCH 06/14] README fixed --- samples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/README.md b/samples/README.md index ce871c4940..a0cd02d8fa 100644 --- a/samples/README.md +++ b/samples/README.md @@ -30,5 +30,5 @@ To edit the policy, please see app.UseCors() method in the Startup.cs file of Sa If using Visual Studio to launch the request origin: Open Visual Studio and in the launchSettings.json file for the SampleOrigin project, change the launchUrl under SampleOrigin to -http://origin.example.com:8080. Using the dropdown near the Start button, choose SampleOrigin before pressing Start to ensure that it uses Kestrel +http://origin.example.com:5001. Using the dropdown near the Start button, choose SampleOrigin before pressing Start to ensure that it uses Kestrel and not IIS Express. From 7b190ccf0f4342140928988008e6fa3af2067b84 Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Mon, 21 Nov 2016 16:13:30 -0800 Subject: [PATCH 07/14] Changed allowed header to Cache-control to illustrate Access-Control-Allow-Headers and Access-Control-Request-Headers --- samples/SampleDestination/Startup.cs | 2 +- samples/SampleOrigin/wwwroot/Index.html | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/samples/SampleDestination/Startup.cs b/samples/SampleDestination/Startup.cs index 199bb16795..9859aa3abb 100644 --- a/samples/SampleDestination/Startup.cs +++ b/samples/SampleDestination/Startup.cs @@ -24,7 +24,7 @@ namespace SampleDestination app.UseCors(policy => policy .WithOrigins("http://origin.example.com:5001") .WithMethods("PUT") - .WithHeaders("Content-Length")); + .WithHeaders("Cache-Control")); app.Run(async context => { diff --git a/samples/SampleOrigin/wwwroot/Index.html b/samples/SampleOrigin/wwwroot/Index.html index dd2c4f0ae9..6529ddb6ea 100644 --- a/samples/SampleOrigin/wwwroot/Index.html +++ b/samples/SampleOrigin/wwwroot/Index.html @@ -63,7 +63,7 @@

CORS Sample

Method:

- Header Name: Header Value:

+ Header Name: Header Value:

+

CORS Sample

Method:

- Header Name: Header Value:

+ Header Name: Header Value:





From b351e033f512e25a597336b7422a059654571fe9 Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Wed, 23 Nov 2016 14:17:09 -0800 Subject: [PATCH 14/14] README formatting fixed --- samples/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/README.md b/samples/README.md index e4cb93b915..0224188d71 100644 --- a/samples/README.md +++ b/samples/README.md @@ -3,7 +3,7 @@ This sample consists of a request origin (SampleOrigin) and a request destination (SampleDestination). Both have different domain names, to simulate a CORS request. ## Modify Hosts File -To run this CORS sample, modify the hosts file to register the hostnames `destination.example.com` and `origin.example.com.` +To run this CORS sample, modify the hosts file to register the hostnames `destination.example.com` and `origin.example.com`. ### Windows: Run a text editor (e.g. Notepad) as an Administrator. Open the hosts file on the path: "C:\Windows\System32\drivers\etc\hosts". @@ -23,9 +23,9 @@ Save the file and close it. Then clear your browser history. The SampleOrigin application will use port 5001, and SampleDestination will use 5000. Please ensure there are no other processes using those ports before running the CORS sample. * In a command prompt window, open the directory where you cloned the repository, and open the SampleDestination directory. Run the command: dotnet run -* Repeat the above step in the SampleOrigin directory. +* Repeat the above step in the SampleOrigin directory * Open a browser window and go to `http://origin.example.com:5001` -* Input a method and header to create a CORS request or use one of the example buttons to see CORS in action. +* Input a method and header to create a CORS request or use one of the example buttons to see CORS in action As an example, apart from `GET`, `HEAD` and `POST` requests, `PUT` requests are allowed in the CORS policy on SampleDestination. Any others, like `DELETE`, `OPTIONS` etc. are not allowed and throw an error. `Cache-Control` has been added as an allowed header to the sample. Any other headers are not allowed and throw an error. You may leave the header name and value blank.