diff --git a/CORS.sln b/CORS.sln index 9f3b2ba560..c471720431 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", "samples\SampleDestination\SampleDestination.xproj", "{F6675DC1-AA21-453B-89B6-DA425FB9C3A5}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SampleOrigin", "samples\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/samples/README.md b/samples/README.md new file mode 100644 index 0000000000..0224188d71 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,38 @@ +# 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 +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". + +### 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 +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 +* 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 + +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. + +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:5001`. +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/samples/SampleDestination/Program.cs b/samples/SampleDestination/Program.cs new file mode 100644 index 0000000000..c196d5b838 --- /dev/null +++ b/samples/SampleDestination/Program.cs @@ -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.IO; +using Microsoft.AspNetCore.Hosting; + +namespace SampleDestination +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseUrls("http://*:5000") + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/samples/SampleDestination/SampleDestination.xproj b/samples/SampleDestination/SampleDestination.xproj new file mode 100644 index 0000000000..ac33a6f88b --- /dev/null +++ b/samples/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/samples/SampleDestination/Startup.cs b/samples/SampleDestination/Startup.cs new file mode 100644 index 0000000000..e874697e18 --- /dev/null +++ b/samples/SampleDestination/Startup.cs @@ -0,0 +1,41 @@ +// 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:5001") + .WithMethods("PUT") + .WithHeaders("Cache-Control")); + + app.Run(async context => + { + var responseHeaders = context.Response.Headers; + context.Response.ContentType = "text/plain"; + 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 new file mode 100644 index 0000000000..3c86825eb6 --- /dev/null +++ b/samples/SampleDestination/project.json @@ -0,0 +1,37 @@ +{ + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.1.0-*", + "type": "platform" + }, + "Microsoft.AspNetCore.Server.Kestrel": "1.2.0-*", + "Microsoft.Extensions.Logging.Console": "1.2.0-*", + "Microsoft.AspNetCore.Cors": "1.2.0-*" + }, + + "frameworks": { + "netcoreapp1.0": { + "imports": [ + "dotnet5.6", + "portable-net45+win8" + ] + } + }, + + "buildOptions": { + "emitEntryPoint": true, + "preserveCompilationContext": true + }, + + "runtimeOptions": { + "configProperties": { + "System.GC.Server": true + } + }, + + "publishOptions": { + "include": [ + "wwwroot" + ] + } +} diff --git a/samples/SampleOrigin/Program.cs b/samples/SampleOrigin/Program.cs new file mode 100644 index 0000000000..34a19eccb5 --- /dev/null +++ b/samples/SampleOrigin/Program.cs @@ -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.IO; +using Microsoft.AspNetCore.Hosting; + +namespace SampleOrigin +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseUrls("http://*:5001") + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/samples/SampleOrigin/SampleOrigin.xproj b/samples/SampleOrigin/SampleOrigin.xproj new file mode 100644 index 0000000000..2241431ad0 --- /dev/null +++ b/samples/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/samples/SampleOrigin/Startup.cs b/samples/SampleOrigin/Startup.cs new file mode 100644 index 0000000000..d8a54d74d5 --- /dev/null +++ b/samples/SampleOrigin/Startup.cs @@ -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 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.ContentType = "text/html"; + return context.Response.SendFileAsync(fileInfo); + }); + } + } +} diff --git a/samples/SampleOrigin/project.json b/samples/SampleOrigin/project.json new file mode 100644 index 0000000000..259fb617c3 --- /dev/null +++ b/samples/SampleOrigin/project.json @@ -0,0 +1,36 @@ +{ + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.1.0-*", + "type": "platform" + }, + "Microsoft.AspNetCore.Server.Kestrel": "1.2.0-*", + "Microsoft.Extensions.Logging.Console": "1.2.0-*" + }, + + "frameworks": { + "netcoreapp1.0": { + "imports": [ + "dotnet5.6", + "portable-net45+win8" + ] + } + }, + + "buildOptions": { + "emitEntryPoint": true, + "preserveCompilationContext": true + }, + + "runtimeOptions": { + "configProperties": { + "System.GC.Server": true + } + }, + + "publishOptions": { + "include": [ + "wwwroot" + ] + } +} diff --git a/samples/SampleOrigin/wwwroot/Index.html b/samples/SampleOrigin/wwwroot/Index.html new file mode 100644 index 0000000000..fad335b83f --- /dev/null +++ b/samples/SampleOrigin/wwwroot/Index.html @@ -0,0 +1,89 @@ + + + + + + CORS Sample + + + + +

CORS Sample

+ Method:

+ Header Name: Header Value:

+ + + +



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

+ + Header 'Max-Forwards' not supported: + Header 'Cache-Control' 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 6af9dafc7d..483cb3e59a 100644 --- a/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs +++ b/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs @@ -5,7 +5,9 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Microsoft.AspNetCore.Cors.Internal; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -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(); } /// @@ -69,6 +83,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure if (string.Equals(context.Request.Method, CorsConstants.PreflightHttpMethod, StringComparison.OrdinalIgnoreCase) && !StringValues.IsNullOrEmpty(accessControlRequestMethod)) { + _logger?.IsPreflightRequest(); EvaluatePreflightRequest(context, policy, corsResult); } else @@ -82,21 +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)) { + _logger?.RequestDoesNotHaveOriginHeader(); + return; + } + + _logger?.RequestHasOriginHeader(origin); + if (!policy.AllowAnyOrigin && !policy.Origins.Contains(origin)) + { + _logger?.PolicyFailure(); + _logger?.OriginNotAllowed(origin); return; } AddOriginToResult(origin, policy, result); result.SupportsCredentials = policy.SupportsCredentials; 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)) { + _logger?.RequestDoesNotHaveOriginHeader(); + return; + } + + _logger?.RequestHasOriginHeader(origin); + if (!policy.AllowAnyOrigin && !policy.Origins.Contains(origin)) + { + _logger?.PolicyFailure(); + _logger?.OriginNotAllowed(origin); return; } @@ -124,16 +158,25 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure if (!found) { + _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) { - 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); @@ -141,6 +184,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure result.PreflightMaxAge = policy.PreflightMaxAge; result.AllowedMethods.Add(accessControlRequestMethod); AddHeaderValues(result.AllowedHeaders, requestHeaders); + _logger?.PolicySuccess(); } /// @@ -261,4 +305,4 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs b/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs new file mode 100644 index 0000000000..727d19a4ea --- /dev/null +++ b/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs @@ -0,0 +1,103 @@ +// 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 _requestDoesNotHaveOriginHeader; + private static readonly Action _policySuccess; + 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, + "The request is a preflight request."); + + _requestHasOriginHeader = LoggerMessage.Define( + LogLevel.Debug, + 2, + "The request has an origin header: '{origin}'."); + + _requestDoesNotHaveOriginHeader = LoggerMessage.Define( + LogLevel.Debug, + 3, + "The request does not have an origin header."); + + _policySuccess = LoggerMessage.Define( + LogLevel.Information, + 4, + "Policy execution successful."); + + _policyFailure = LoggerMessage.Define( + LogLevel.Information, + 5, + "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) + { + _isPreflightRequest(logger, null); + } + + public static void RequestHasOriginHeader(this ILogger logger, string origin) + { + _requestHasOriginHeader(logger, origin, null); + } + + public static void RequestDoesNotHaveOriginHeader(this ILogger logger) + { + _requestDoesNotHaveOriginHeader(logger, null); + } + + public static void PolicySuccess(this ILogger logger) + { + _policySuccess(logger, null); + } + + public static void PolicyFailure(this ILogger logger) + { + _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/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-*" }, diff --git a/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs b/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs index c88eccd6fa..7edfc895fd 100644 --- a/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs +++ b/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Testing; using Xunit; namespace Microsoft.AspNetCore.Cors.Infrastructure @@ -227,6 +228,162 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure Assert.Contains("PUT", result.AllowedMethods); } + public static TheoryData PreflightRequests_LoggingData + { + get + { + return new TheoryData + { + { + 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." + } + }, + { + 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." + } + }, + { + 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." + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(PreflightRequests_LoggingData))] + 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: logData.Origin, accessControlRequestMethod: logData.Method, accessControlRequestHeaders: logData.Headers); + 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(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] + 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("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()); + } + + [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: "http://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://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] + 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] [InlineData("OpTions")] [InlineData("OPTIONS")] @@ -446,7 +603,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure } [Fact] - public void EaluatePolicy_DoesCaseSensitiveComparison() + public void EvaluatePolicy_DoesCaseSensitiveComparison() { // Arrange var corsService = new CorsService(new TestCorsOptions()); @@ -913,5 +1070,15 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure return context; } + + public class 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 +}