diff --git a/src/CORS/.gitignore b/src/CORS/.gitignore new file mode 100644 index 0000000000..a435689c0a --- /dev/null +++ b/src/CORS/.gitignore @@ -0,0 +1,41 @@ +[Oo]bj/ +[Bb]in/ +TestResults/ +.nuget/ +*.sln.ide/ +_ReSharper.*/ +packages/ +artifacts/ +PublishProfiles/ +.vs/ +bower_components/ +node_modules/ +**/wwwroot/lib/ +debugSettings.json +project.lock.json +*.user +*.suo +*.cache +*.docstates +_ReSharper.* +nuget.exe +*net45.csproj +*net451.csproj +*k10.csproj +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution +*.*sdf +*.ipch +.settings +*.sln.ide +node_modules +**/[Cc]ompiler/[Rr]esources/**/*.js +*launchSettings.json +.build/ +.testPublish/ +.vscode +global.json diff --git a/src/CORS/CORS.sln b/src/CORS/CORS.sln new file mode 100644 index 0000000000..afdbf1fdc0 --- /dev/null +++ b/src/CORS/CORS.sln @@ -0,0 +1,89 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26817.0 +MinimumVisualStudioVersion = 15.0.26730.03 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{84FE6872-A610-4CEC-855F-A84CBF1F40FC}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F32074C7-087C-46CC-A913-422BFD2D6E0A}" + ProjectSection(SolutionItems) = preProject + test\Directory.Build.props = test\Directory.Build.props + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Cors", "src\Microsoft.AspNetCore.Cors\Microsoft.AspNetCore.Cors.csproj", "{41349FCD-D1C4-47A6-82D0-D16D00A8D59D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Cors.Test", "test\Microsoft.AspNetCore.Cors.Test\Microsoft.AspNetCore.Cors.Test.csproj", "{F05BE96F-F869-4408-A480-96935B4835EE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebSites", "WebSites", "{538380BF-0D4C-4E30-8F41-E75C4B1C01FA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CorsMiddlewareWebSite", "test\WebSites\CorsMiddlewareWebSite\CorsMiddlewareWebSite.csproj", "{B42D4844-FFF8-4EC2-88D1-3AE95234D9EB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{960E0703-A8A5-44DF-AA87-B7C614683B3C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleDestination", "samples\SampleDestination\SampleDestination.csproj", "{F6675DC1-AA21-453B-89B6-DA425FB9C3A5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleOrigin", "samples\SampleOrigin\SampleOrigin.csproj", "{99460370-AE5D-4DC9-8DBF-04DF66D6B21D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0CCC5C1B-F548-4A17-96F8-14C700093FA0}" + ProjectSection(SolutionItems) = preProject + .appveyor.yml = .appveyor.yml + .gitattributes = .gitattributes + .gitignore = .gitignore + .travis.yml = .travis.yml + build.cmd = build.cmd + build.ps1 = build.ps1 + build.sh = build.sh + CONTRIBUTING.md = CONTRIBUTING.md + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + LICENSE.txt = LICENSE.txt + NuGet.config = NuGet.config + NuGetPackageVerifier.json = NuGetPackageVerifier.json + README.md = README.md + version.xml = version.xml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {41349FCD-D1C4-47A6-82D0-D16D00A8D59D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41349FCD-D1C4-47A6-82D0-D16D00A8D59D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41349FCD-D1C4-47A6-82D0-D16D00A8D59D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41349FCD-D1C4-47A6-82D0-D16D00A8D59D}.Release|Any CPU.Build.0 = Release|Any CPU + {F05BE96F-F869-4408-A480-96935B4835EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F05BE96F-F869-4408-A480-96935B4835EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F05BE96F-F869-4408-A480-96935B4835EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F05BE96F-F869-4408-A480-96935B4835EE}.Release|Any CPU.Build.0 = Release|Any CPU + {B42D4844-FFF8-4EC2-88D1-3AE95234D9EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {41349FCD-D1C4-47A6-82D0-D16D00A8D59D} = {84FE6872-A610-4CEC-855F-A84CBF1F40FC} + {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 + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F9ED9C53-44CD-4853-9621-D028B7B6A431} + EndGlobalSection +EndGlobal diff --git a/src/CORS/Directory.Build.props b/src/CORS/Directory.Build.props new file mode 100644 index 0000000000..c81115d310 --- /dev/null +++ b/src/CORS/Directory.Build.props @@ -0,0 +1,21 @@ + + + + + + + + + Microsoft ASP.NET Core + https://github.com/aspnet/CORS + git + $(MSBuildThisFileDirectory) + $(MSBuildThisFileDirectory)build\Key.snk + true + true + true + + + diff --git a/src/CORS/Directory.Build.targets b/src/CORS/Directory.Build.targets new file mode 100644 index 0000000000..53b3f6e1da --- /dev/null +++ b/src/CORS/Directory.Build.targets @@ -0,0 +1,7 @@ + + + $(MicrosoftNETCoreApp20PackageVersion) + $(MicrosoftNETCoreApp21PackageVersion) + $(NETStandardLibrary20PackageVersion) + + diff --git a/src/CORS/NuGetPackageVerifier.json b/src/CORS/NuGetPackageVerifier.json new file mode 100644 index 0000000000..b153ab1515 --- /dev/null +++ b/src/CORS/NuGetPackageVerifier.json @@ -0,0 +1,7 @@ +{ + "Default": { + "rules": [ + "DefaultCompositeRule" + ] + } +} \ No newline at end of file diff --git a/src/CORS/README.md b/src/CORS/README.md new file mode 100644 index 0000000000..795d311da9 --- /dev/null +++ b/src/CORS/README.md @@ -0,0 +1,9 @@ +CORS +=== +AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/yi0m8evjtg107o12/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/CORS/branch/dev) + +Travis: [![Travis](https://travis-ci.org/aspnet/CORS.svg?branch=dev)](https://travis-ci.org/aspnet/CORS) + +CORS repository includes the core implementation for CORS policy, utilized by the CORS middleware and MVC. + +This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo. diff --git a/src/CORS/build/Key.snk b/src/CORS/build/Key.snk new file mode 100644 index 0000000000..e10e4889c1 Binary files /dev/null and b/src/CORS/build/Key.snk differ diff --git a/src/CORS/build/buildpipeline/linux.groovy b/src/CORS/build/buildpipeline/linux.groovy new file mode 100644 index 0000000000..903f218bb8 --- /dev/null +++ b/src/CORS/build/buildpipeline/linux.groovy @@ -0,0 +1,10 @@ +@Library('dotnet-ci') _ + +simpleNode('Ubuntu16.04', 'latest-or-auto-docker') { + stage ('Checking out source') { + checkout scm + } + stage ('Build') { + sh './build.sh --ci' + } +} diff --git a/src/CORS/build/buildpipeline/osx.groovy b/src/CORS/build/buildpipeline/osx.groovy new file mode 100644 index 0000000000..aaac63686b --- /dev/null +++ b/src/CORS/build/buildpipeline/osx.groovy @@ -0,0 +1,10 @@ +@Library('dotnet-ci') _ + +simpleNode('OSX10.12','latest') { + stage ('Checking out source') { + checkout scm + } + stage ('Build') { + sh './build.sh --ci' + } +} diff --git a/src/CORS/build/buildpipeline/pipeline.groovy b/src/CORS/build/buildpipeline/pipeline.groovy new file mode 100644 index 0000000000..e915cadae1 --- /dev/null +++ b/src/CORS/build/buildpipeline/pipeline.groovy @@ -0,0 +1,18 @@ +import org.dotnet.ci.pipelines.Pipeline + +def windowsPipeline = Pipeline.createPipeline(this, 'build/buildpipeline/windows.groovy') +def linuxPipeline = Pipeline.createPipeline(this, 'build/buildpipeline/linux.groovy') +def osxPipeline = Pipeline.createPipeline(this, 'build/buildpipeline/osx.groovy') +String configuration = 'Release' +def parameters = [ + 'Configuration': configuration +] + +windowsPipeline.triggerPipelineOnEveryGithubPR("Windows ${configuration} x64 Build", parameters) +windowsPipeline.triggerPipelineOnGithubPush(parameters) + +linuxPipeline.triggerPipelineOnEveryGithubPR("Ubuntu 16.04 ${configuration} Build", parameters) +linuxPipeline.triggerPipelineOnGithubPush(parameters) + +osxPipeline.triggerPipelineOnEveryGithubPR("OSX 10.12 ${configuration} Build", parameters) +osxPipeline.triggerPipelineOnGithubPush(parameters) diff --git a/src/CORS/build/buildpipeline/windows.groovy b/src/CORS/build/buildpipeline/windows.groovy new file mode 100644 index 0000000000..8d26f313d4 --- /dev/null +++ b/src/CORS/build/buildpipeline/windows.groovy @@ -0,0 +1,12 @@ +@Library('dotnet-ci') _ + +// 'node' indicates to Jenkins that the enclosed block runs on a node that matches +// the label 'windows-with-vs' +simpleNode('Windows_NT','latest') { + stage ('Checking out source') { + checkout scm + } + stage ('Build') { + bat '.\\run.cmd -CI default-build' + } +} diff --git a/src/CORS/build/dependencies.props b/src/CORS/build/dependencies.props new file mode 100644 index 0000000000..36fe73426a --- /dev/null +++ b/src/CORS/build/dependencies.props @@ -0,0 +1,35 @@ + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + + 2.1.3-rtm-15802 + 2.0.0 + 2.1.2 + 15.6.1 + 4.7.49 + 2.0.3 + 0.8.0 + 2.3.1 + 2.4.0-beta.1.build3945 + + + + + + + + 2.1.1 + 2.1.1 + 2.1.2 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + + \ No newline at end of file diff --git a/src/CORS/build/repo.props b/src/CORS/build/repo.props new file mode 100644 index 0000000000..dab1601c88 --- /dev/null +++ b/src/CORS/build/repo.props @@ -0,0 +1,15 @@ + + + + + + Internal.AspNetCore.Universe.Lineup + 2.1.0-rc1-* + https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json + + + + + + + diff --git a/src/CORS/build/sources.props b/src/CORS/build/sources.props new file mode 100644 index 0000000000..9215df9751 --- /dev/null +++ b/src/CORS/build/sources.props @@ -0,0 +1,17 @@ + + + + + $(DotNetRestoreSources) + + $(RestoreSources); + https://dotnet.myget.org/F/dotnet-core/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; + + + $(RestoreSources); + https://api.nuget.org/v3/index.json; + + + diff --git a/src/CORS/samples/README.md b/src/CORS/samples/README.md new file mode 100644 index 0000000000..0224188d71 --- /dev/null +++ b/src/CORS/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/src/CORS/samples/SampleDestination/Program.cs b/src/CORS/samples/SampleDestination/Program.cs new file mode 100644 index 0000000000..0c2bf0968f --- /dev/null +++ b/src/CORS/samples/SampleDestination/Program.cs @@ -0,0 +1,25 @@ +// 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; +using Microsoft.Extensions.Logging; + +namespace SampleDestination +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseUrls("http://*:5000") + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureLogging(factory => factory.AddConsole()) + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/src/CORS/samples/SampleDestination/SampleDestination.csproj b/src/CORS/samples/SampleDestination/SampleDestination.csproj new file mode 100644 index 0000000000..b8e59ede3a --- /dev/null +++ b/src/CORS/samples/SampleDestination/SampleDestination.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp2.1;net461 + + + + + + + + + + + + diff --git a/src/CORS/samples/SampleDestination/Startup.cs b/src/CORS/samples/SampleDestination/Startup.cs new file mode 100644 index 0000000000..0664d84572 --- /dev/null +++ b/src/CORS/samples/SampleDestination/Startup.cs @@ -0,0 +1,39 @@ +// 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) + { + 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/src/CORS/samples/SampleOrigin/Program.cs b/src/CORS/samples/SampleOrigin/Program.cs new file mode 100644 index 0000000000..278c09f48a --- /dev/null +++ b/src/CORS/samples/SampleOrigin/Program.cs @@ -0,0 +1,25 @@ +// 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; +using Microsoft.Extensions.Logging; + +namespace SampleOrigin +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseUrls("http://*:5001") + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureLogging(factory => factory.AddConsole()) + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/src/CORS/samples/SampleOrigin/SampleOrigin.csproj b/src/CORS/samples/SampleOrigin/SampleOrigin.csproj new file mode 100644 index 0000000000..b43f880f9c --- /dev/null +++ b/src/CORS/samples/SampleOrigin/SampleOrigin.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp2.1;net461 + + + + + + + + diff --git a/src/CORS/samples/SampleOrigin/Startup.cs b/src/CORS/samples/SampleOrigin/Startup.cs new file mode 100644 index 0000000000..a442b98d68 --- /dev/null +++ b/src/CORS/samples/SampleOrigin/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 SampleOrigin +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + 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/src/CORS/samples/SampleOrigin/wwwroot/Index.html b/src/CORS/samples/SampleOrigin/wwwroot/Index.html new file mode 100644 index 0000000000..fad335b83f --- /dev/null +++ b/src/CORS/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/CORS/src/Directory.Build.props b/src/CORS/src/Directory.Build.props new file mode 100644 index 0000000000..4f07cbc45d --- /dev/null +++ b/src/CORS/src/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/CorsServiceCollectionExtensions.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/CorsServiceCollectionExtensions.cs new file mode 100644 index 0000000000..430621d2fd --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/CorsServiceCollectionExtensions.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extension methods for setting up cross-origin resource sharing services in an . + /// + public static class CorsServiceCollectionExtensions + { + /// + /// Adds cross-origin resource sharing services to the specified . + /// + /// The to add services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddCors(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddOptions(); + + services.TryAdd(ServiceDescriptor.Transient()); + services.TryAdd(ServiceDescriptor.Transient()); + + return services; + } + + /// + /// Adds cross-origin resource sharing services to the specified . + /// + /// The to add services to. + /// An to configure the provided . + /// The so that additional calls can be chained. + public static IServiceCollection AddCors(this IServiceCollection services, Action setupAction) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (setupAction == null) + { + throw new ArgumentNullException(nameof(setupAction)); + } + + services.AddCors(); + services.Configure(setupAction); + + return services; + } + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/DisableCorsAttribute.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/DisableCorsAttribute.cs new file mode 100644 index 0000000000..ab4c755066 --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/DisableCorsAttribute.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Cors.Infrastructure; + +namespace Microsoft.AspNetCore.Cors +{ + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public class DisableCorsAttribute : Attribute, IDisableCorsAttribute + { + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/EnableCorsAttribute.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/EnableCorsAttribute.cs new file mode 100644 index 0000000000..b19851e39a --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/EnableCorsAttribute.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Cors.Infrastructure; + +namespace Microsoft.AspNetCore.Cors +{ + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class EnableCorsAttribute : Attribute, IEnableCorsAttribute + { + /// + /// Creates a new instance of the with the default policy + /// name defined by . + /// + public EnableCorsAttribute() + : this(policyName: null) + { + } + + /// + /// Creates a new instance of the with the supplied policy name. + /// + /// The name of the policy to be applied. + public EnableCorsAttribute(string policyName) + { + PolicyName = policyName; + } + + /// + public string PolicyName { get; set; } + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsConstants.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsConstants.cs new file mode 100644 index 0000000000..22110b2e41 --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsConstants.cs @@ -0,0 +1,95 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + /// + /// CORS-related constants. + /// + public static class CorsConstants + { + /// + /// The HTTP method for the CORS preflight request. + /// + public static readonly string PreflightHttpMethod = HttpMethods.Options; + + /// + /// The Origin request header. + /// + public static readonly string Origin = HeaderNames.Origin; + + /// + /// The value for the Access-Control-Allow-Origin response header to allow all origins. + /// + public static readonly string AnyOrigin = "*"; + + /// + /// The Access-Control-Request-Method request header. + /// + public static readonly string AccessControlRequestMethod = HeaderNames.AccessControlRequestMethod; + + /// + /// The Access-Control-Request-Headers request header. + /// + public static readonly string AccessControlRequestHeaders = HeaderNames.AccessControlRequestHeaders; + + /// + /// The Access-Control-Allow-Origin response header. + /// + public static readonly string AccessControlAllowOrigin = HeaderNames.AccessControlAllowOrigin; + + /// + /// The Access-Control-Allow-Headers response header. + /// + public static readonly string AccessControlAllowHeaders = HeaderNames.AccessControlAllowHeaders; + + /// + /// The Access-Control-Expose-Headers response header. + /// + public static readonly string AccessControlExposeHeaders = HeaderNames.AccessControlExposeHeaders; + + /// + /// The Access-Control-Allow-Methods response header. + /// + public static readonly string AccessControlAllowMethods = HeaderNames.AccessControlAllowMethods; + + /// + /// The Access-Control-Allow-Credentials response header. + /// + public static readonly string AccessControlAllowCredentials = HeaderNames.AccessControlAllowCredentials; + + /// + /// The Access-Control-Max-Age response header. + /// + public static readonly string AccessControlMaxAge = HeaderNames.AccessControlMaxAge; + + + internal static readonly string[] SimpleRequestHeaders = + { + HeaderNames.Origin, + HeaderNames.Accept, + HeaderNames.AcceptLanguage, + HeaderNames.ContentLanguage, + }; + + internal static readonly string[] SimpleResponseHeaders = + { + HeaderNames.CacheControl, + HeaderNames.ContentLanguage, + HeaderNames.ContentType, + HeaderNames.Expires, + HeaderNames.LastModified, + HeaderNames.Pragma + }; + + internal static readonly string[] SimpleMethods = + { + HttpMethods.Get, + HttpMethods.Head, + HttpMethods.Post + }; + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddleware.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddleware.cs new file mode 100644 index 0000000000..eb773f5abf --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddleware.cs @@ -0,0 +1,131 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + /// + /// An ASP.NET middleware for handling CORS. + /// + public class CorsMiddleware + { + private readonly RequestDelegate _next; + private readonly ICorsService _corsService; + private readonly ICorsPolicyProvider _corsPolicyProvider; + private readonly CorsPolicy _policy; + private readonly string _corsPolicyName; + + /// + /// Instantiates a new . + /// + /// The next middleware in the pipeline. + /// An instance of . + /// A policy provider which can get an . + public CorsMiddleware( + RequestDelegate next, + ICorsService corsService, + ICorsPolicyProvider policyProvider) + : this(next, corsService, policyProvider, policyName: null) + { + } + + /// + /// Instantiates a new . + /// + /// The next middleware in the pipeline. + /// An instance of . + /// A policy provider which can get an . + /// An optional name of the policy to be fetched. + public CorsMiddleware( + RequestDelegate next, + ICorsService corsService, + ICorsPolicyProvider policyProvider, + string policyName) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (corsService == null) + { + throw new ArgumentNullException(nameof(corsService)); + } + + if (policyProvider == null) + { + throw new ArgumentNullException(nameof(policyProvider)); + } + + _next = next; + _corsService = corsService; + _corsPolicyProvider = policyProvider; + _corsPolicyName = policyName; + } + + /// + /// Instantiates a new . + /// + /// The next middleware in the pipeline. + /// An instance of . + /// An instance of the which can be applied. + public CorsMiddleware( + RequestDelegate next, + ICorsService corsService, + CorsPolicy policy) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (corsService == null) + { + throw new ArgumentNullException(nameof(corsService)); + } + + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + + _next = next; + _corsService = corsService; + _policy = policy; + } + + /// + public async Task Invoke(HttpContext context) + { + if (context.Request.Headers.ContainsKey(CorsConstants.Origin)) + { + var corsPolicy = _policy ?? await _corsPolicyProvider?.GetPolicyAsync(context, _corsPolicyName); + if (corsPolicy != null) + { + var corsResult = _corsService.EvaluatePolicy(context, corsPolicy); + _corsService.ApplyResult(corsResult, context.Response); + + var accessControlRequestMethod = + context.Request.Headers[CorsConstants.AccessControlRequestMethod]; + if (string.Equals( + context.Request.Method, + CorsConstants.PreflightHttpMethod, + StringComparison.OrdinalIgnoreCase) && + !StringValues.IsNullOrEmpty(accessControlRequestMethod)) + { + // Since there is a policy which was identified, + // always respond to preflight requests. + context.Response.StatusCode = StatusCodes.Status204NoContent; + return; + } + } + } + + await _next(context); + } + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddlewareExtensions.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddlewareExtensions.cs new file mode 100644 index 0000000000..dea1ad01d9 --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddlewareExtensions.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Cors.Infrastructure; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// The extensions for adding CORS middleware support. + /// + public static class CorsMiddlewareExtensions + { + /// + /// Adds a CORS middleware to your web application pipeline to allow cross domain requests. + /// + /// The IApplicationBuilder passed to your Configure method + /// The original app parameter + public static IApplicationBuilder UseCors(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + + /// + /// Adds a CORS middleware to your web application pipeline to allow cross domain requests. + /// + /// The IApplicationBuilder passed to your Configure method + /// The policy name of a configured policy. + /// The original app parameter + public static IApplicationBuilder UseCors(this IApplicationBuilder app, string policyName) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(policyName); + } + + /// + /// Adds a CORS middleware to your web application pipeline to allow cross domain requests. + /// + /// The IApplicationBuilder passed to your Configure method. + /// A delegate which can use a policy builder to build a policy. + /// The original app parameter + public static IApplicationBuilder UseCors( + this IApplicationBuilder app, + Action configurePolicy) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (configurePolicy == null) + { + throw new ArgumentNullException(nameof(configurePolicy)); + } + + var policyBuilder = new CorsPolicyBuilder(); + configurePolicy(policyBuilder); + return app.UseMiddleware(policyBuilder.Build()); + } + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsOptions.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsOptions.cs new file mode 100644 index 0000000000..92e1f775ba --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsOptions.cs @@ -0,0 +1,119 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + /// + /// Provides programmatic configuration for Cors. + /// + public class CorsOptions + { + private string _defaultPolicyName = "__DefaultCorsPolicy"; + private IDictionary PolicyMap { get; } = new Dictionary(); + + public string DefaultPolicyName + { + get + { + return _defaultPolicyName; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _defaultPolicyName = value; + } + } + + /// + /// Adds a new policy and sets it as the default. + /// + /// The policy to be added. + public void AddDefaultPolicy(CorsPolicy policy) + { + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + + AddPolicy(DefaultPolicyName, policy); + } + + /// + /// Adds a new policy and sets it as the default. + /// + /// A delegate which can use a policy builder to build a policy. + public void AddDefaultPolicy(Action configurePolicy) + { + if (configurePolicy == null) + { + throw new ArgumentNullException(nameof(configurePolicy)); + } + + AddPolicy(DefaultPolicyName, configurePolicy); + } + + /// + /// Adds a new policy. + /// + /// The name of the policy. + /// The policy to be added. + public void AddPolicy(string name, CorsPolicy policy) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + + PolicyMap[name] = policy; + } + + /// + /// Adds a new policy. + /// + /// The name of the policy. + /// A delegate which can use a policy builder to build a policy. + public void AddPolicy(string name, Action configurePolicy) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (configurePolicy == null) + { + throw new ArgumentNullException(nameof(configurePolicy)); + } + + var policyBuilder = new CorsPolicyBuilder(); + configurePolicy(policyBuilder); + PolicyMap[name] = policyBuilder.Build(); + } + + /// + /// Gets the policy based on the + /// + /// The name of the policy to lookup. + /// The if the policy was added.null otherwise. + public CorsPolicy GetPolicy(string name) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return PolicyMap.ContainsKey(name) ? PolicyMap[name] : null; + } + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicy.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicy.cs new file mode 100644 index 0000000000..f541863c69 --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicy.cs @@ -0,0 +1,164 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + /// + /// Defines the policy for Cross-Origin requests based on the CORS specifications. + /// + public class CorsPolicy + { + private TimeSpan? _preflightMaxAge; + + /// + /// Default constructor for a CorsPolicy. + /// + public CorsPolicy() + { + IsOriginAllowed = DefaultIsOriginAllowed; + } + + /// + /// Gets a value indicating if all headers are allowed. + /// + public bool AllowAnyHeader + { + get + { + if (Headers == null || Headers.Count != 1 || Headers.Count == 1 && Headers[0] != "*") + { + return false; + } + + return true; + } + } + + /// + /// Gets a value indicating if all methods are allowed. + /// + public bool AllowAnyMethod + { + get + { + if (Methods == null || Methods.Count != 1 || Methods.Count == 1 && Methods[0] != "*") + { + return false; + } + + return true; + } + } + + /// + /// Gets a value indicating if all origins are allowed. + /// + public bool AllowAnyOrigin + { + get + { + if (Origins == null || Origins.Count != 1 || Origins.Count == 1 && Origins[0] != "*") + { + return false; + } + + return true; + } + } + + /// + /// Gets or sets a function which evaluates whether an origin is allowed. + /// + public Func IsOriginAllowed { get; set; } + + /// + /// Gets the headers that the resource might use and can be exposed. + /// + public IList ExposedHeaders { get; } = new List(); + + /// + /// Gets the headers that are supported by the resource. + /// + public IList Headers { get; } = new List(); + + /// + /// Gets the methods that are supported by the resource. + /// + public IList Methods { get; } = new List(); + + /// + /// Gets the origins that are allowed to access the resource. + /// + public IList Origins { get; } = new List(); + + /// + /// Gets or sets the for which the results of a preflight request can be cached. + /// + public TimeSpan? PreflightMaxAge + { + get + { + return _preflightMaxAge; + } + set + { + if (value < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(value), Resources.PreflightMaxAgeOutOfRange); + } + + _preflightMaxAge = value; + } + } + + /// + /// Gets or sets a value indicating whether the resource supports user credentials in the request. + /// + public bool SupportsCredentials { get; set; } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + var builder = new StringBuilder(); + builder.Append("AllowAnyHeader: "); + builder.Append(AllowAnyHeader); + builder.Append(", AllowAnyMethod: "); + builder.Append(AllowAnyMethod); + builder.Append(", AllowAnyOrigin: "); + builder.Append(AllowAnyOrigin); + builder.Append(", PreflightMaxAge: "); + builder.Append(PreflightMaxAge.HasValue ? + PreflightMaxAge.Value.TotalSeconds.ToString() : "null"); + builder.Append(", SupportsCredentials: "); + builder.Append(SupportsCredentials); + builder.Append(", Origins: {"); + builder.Append(string.Join(",", Origins)); + builder.Append("}"); + builder.Append(", Methods: {"); + builder.Append(string.Join(",", Methods)); + builder.Append("}"); + builder.Append(", Headers: {"); + builder.Append(string.Join(",", Headers)); + builder.Append("}"); + builder.Append(", ExposedHeaders: {"); + builder.Append(string.Join(",", ExposedHeaders)); + builder.Append("}"); + return builder.ToString(); + } + + private bool DefaultIsOriginAllowed(string origin) + { + return Origins.Contains(origin, StringComparer.Ordinal); + } + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyBuilder.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyBuilder.cs new file mode 100644 index 0000000000..88e16fa0fa --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyBuilder.cs @@ -0,0 +1,225 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + /// + /// Exposes methods to build a policy. + /// + public class CorsPolicyBuilder + { + private readonly CorsPolicy _policy = new CorsPolicy(); + + /// + /// Creates a new instance of the . + /// + /// list of origins which can be added. + public CorsPolicyBuilder(params string[] origins) + { + WithOrigins(origins); + } + + /// + /// Creates a new instance of the . + /// + /// The policy which will be used to intialize the builder. + public CorsPolicyBuilder(CorsPolicy policy) + { + Combine(policy); + } + + /// + /// Adds the specified to the policy. + /// + /// The origins that are allowed. + /// The current policy builder. + public CorsPolicyBuilder WithOrigins(params string[] origins) + { + foreach (var req in origins) + { + _policy.Origins.Add(req); + } + + return this; + } + + /// + /// Adds the specified to the policy. + /// + /// The headers which need to be allowed in the request. + /// The current policy builder. + public CorsPolicyBuilder WithHeaders(params string[] headers) + { + foreach (var req in headers) + { + _policy.Headers.Add(req); + } + return this; + } + + /// + /// Adds the specified to the policy. + /// + /// The headers which need to be exposed to the client. + /// The current policy builder. + public CorsPolicyBuilder WithExposedHeaders(params string[] exposedHeaders) + { + foreach (var req in exposedHeaders) + { + _policy.ExposedHeaders.Add(req); + } + + return this; + } + + /// + /// Adds the specified to the policy. + /// + /// The methods which need to be added to the policy. + /// The current policy builder. + public CorsPolicyBuilder WithMethods(params string[] methods) + { + foreach (var req in methods) + { + _policy.Methods.Add(req); + } + + return this; + } + + /// + /// Sets the policy to allow credentials. + /// + /// The current policy builder. + public CorsPolicyBuilder AllowCredentials() + { + _policy.SupportsCredentials = true; + return this; + } + + /// + /// Sets the policy to not allow credentials. + /// + /// The current policy builder. + public CorsPolicyBuilder DisallowCredentials() + { + _policy.SupportsCredentials = false; + return this; + } + + /// + /// Ensures that the policy allows any origin. + /// + /// The current policy builder. + public CorsPolicyBuilder AllowAnyOrigin() + { + _policy.Origins.Clear(); + _policy.Origins.Add(CorsConstants.AnyOrigin); + return this; + } + + /// + /// Ensures that the policy allows any method. + /// + /// The current policy builder. + public CorsPolicyBuilder AllowAnyMethod() + { + _policy.Methods.Clear(); + _policy.Methods.Add("*"); + return this; + } + + /// + /// Ensures that the policy allows any header. + /// + /// The current policy builder. + public CorsPolicyBuilder AllowAnyHeader() + { + _policy.Headers.Clear(); + _policy.Headers.Add("*"); + return this; + } + + /// + /// Sets the preflightMaxAge for the underlying policy. + /// + /// A positive indicating the time a preflight + /// request can be cached. + /// The current policy builder. + public CorsPolicyBuilder SetPreflightMaxAge(TimeSpan preflightMaxAge) + { + _policy.PreflightMaxAge = preflightMaxAge; + return this; + } + + /// + /// Sets the specified for the underlying policy. + /// + /// The function used by the policy to evaluate if an origin is allowed. + /// The current policy builder. + public CorsPolicyBuilder SetIsOriginAllowed(Func isOriginAllowed) + { + _policy.IsOriginAllowed = isOriginAllowed; + return this; + } + + /// + /// Sets the property of the policy to be a function + /// that allows origins to match a configured wildcarded domain when evaluating if the + /// origin is allowed. + /// + /// The current policy builder. + public CorsPolicyBuilder SetIsOriginAllowedToAllowWildcardSubdomains() + { + _policy.IsOriginAllowed = _policy.IsOriginAnAllowedSubdomain; + return this; + } + + /// + /// Builds a new using the entries added. + /// + /// The constructed . + public CorsPolicy Build() + { + return _policy; + } + + /// + /// Combines the given to the existing properties in the builder. + /// + /// The policy which needs to be combined. + /// The current policy builder. + private CorsPolicyBuilder Combine(CorsPolicy policy) + { + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + + WithOrigins(policy.Origins.ToArray()); + WithHeaders(policy.Headers.ToArray()); + WithExposedHeaders(policy.ExposedHeaders.ToArray()); + WithMethods(policy.Methods.ToArray()); + SetIsOriginAllowed(policy.IsOriginAllowed); + + if (policy.PreflightMaxAge.HasValue) + { + SetPreflightMaxAge(policy.PreflightMaxAge.Value); + } + + if (policy.SupportsCredentials) + { + AllowCredentials(); + } + else + { + DisallowCredentials(); + } + + return this; + } + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyExtensions.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyExtensions.cs new file mode 100644 index 0000000000..312f772994 --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyExtensions.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 System; +using System.Linq; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + internal static class CorsPolicyExtensions + { + private const string _WildcardSubdomain = "*."; + + public static bool IsOriginAnAllowedSubdomain(this CorsPolicy policy, string origin) + { + if (policy.Origins.Contains(origin)) + { + return true; + } + + if (Uri.TryCreate(origin, UriKind.Absolute, out var originUri)) + { + return policy.Origins + .Where(o => o.Contains($"://{_WildcardSubdomain}")) + .Select(CreateDomainUri) + .Any(domain => UriHelpers.IsSubdomainOf(originUri, domain)); + } + + return false; + } + + private static Uri CreateDomainUri(string origin) + { + return new Uri(origin.Replace(_WildcardSubdomain, string.Empty), UriKind.Absolute); + } + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsResult.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsResult.cs new file mode 100644 index 0000000000..99d38b9fd1 --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsResult.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + /// + /// Results returned by . + /// + public class CorsResult + { + private TimeSpan? _preflightMaxAge; + + /// + /// Gets or sets the allowed origin. + /// + public string AllowedOrigin { get; set; } + + /// + /// Gets or sets a value indicating whether the resource supports user credentials. + /// + public bool SupportsCredentials { get; set; } + + /// + /// Gets the allowed methods. + /// + public IList AllowedMethods { get; } = new List(); + + /// + /// Gets the allowed headers. + /// + public IList AllowedHeaders { get; } = new List(); + + /// + /// Gets the allowed headers that can be exposed on the response. + /// + public IList AllowedExposedHeaders { get; } = new List(); + + /// + /// Gets or sets a value indicating if a 'Vary' header with the value 'Origin' is required. + /// + public bool VaryByOrigin { get; set; } + + /// + /// Gets or sets the for which the results of a preflight request can be cached. + /// + public TimeSpan? PreflightMaxAge + { + get + { + return _preflightMaxAge; + } + set + { + if (value < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(value), Resources.PreflightMaxAgeOutOfRange); + } + _preflightMaxAge = value; + } + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + var builder = new StringBuilder(); + builder.Append("AllowCredentials: "); + builder.Append(SupportsCredentials); + builder.Append(", PreflightMaxAge: "); + builder.Append(PreflightMaxAge.HasValue ? + PreflightMaxAge.Value.TotalSeconds.ToString() : "null"); + builder.Append(", AllowOrigin: "); + builder.Append(AllowedOrigin); + builder.Append(", AllowExposedHeaders: {"); + builder.Append(string.Join(",", AllowedExposedHeaders)); + builder.Append("}"); + builder.Append(", AllowHeaders: {"); + builder.Append(string.Join(",", AllowedHeaders)); + builder.Append("}"); + builder.Append(", AllowMethods: {"); + builder.Append(string.Join(",", AllowedMethods)); + builder.Append("}"); + return builder.ToString(); + } + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs new file mode 100644 index 0000000000..5060ddf205 --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs @@ -0,0 +1,313 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.AspNetCore.Cors.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + /// + /// Default implementation of . + /// + 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) + { + throw new ArgumentNullException(nameof(options)); + } + + _options = options.Value; + _logger = loggerFactory?.CreateLogger(); + } + + /// + /// Looks up a policy using the and then evaluates the policy using the passed in + /// . + /// + /// + /// + /// A which contains the result of policy evaluation and can be + /// used by the caller to set appropriate response headers. + public CorsResult EvaluatePolicy(HttpContext context, string policyName) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var policy = _options.GetPolicy(policyName); + return EvaluatePolicy(context, policy); + } + + /// + public CorsResult EvaluatePolicy(HttpContext context, CorsPolicy policy) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + + var corsResult = new CorsResult(); + var accessControlRequestMethod = context.Request.Headers[CorsConstants.AccessControlRequestMethod]; + if (string.Equals(context.Request.Method, CorsConstants.PreflightHttpMethod, StringComparison.OrdinalIgnoreCase) && + !StringValues.IsNullOrEmpty(accessControlRequestMethod)) + { + _logger?.IsPreflightRequest(); + EvaluatePreflightRequest(context, policy, corsResult); + } + else + { + EvaluateRequest(context, policy, corsResult); + } + + return corsResult; + } + + public virtual void EvaluateRequest(HttpContext context, CorsPolicy policy, CorsResult result) + { + var origin = context.Request.Headers[CorsConstants.Origin]; + if (!IsOriginAllowed(policy, 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 (!IsOriginAllowed(policy, origin)) + { + return; + } + + var accessControlRequestMethod = context.Request.Headers[CorsConstants.AccessControlRequestMethod]; + if (StringValues.IsNullOrEmpty(accessControlRequestMethod)) + { + return; + } + + var requestHeaders = + context.Request.Headers.GetCommaSeparatedValues(CorsConstants.AccessControlRequestHeaders); + + if (!policy.AllowAnyMethod) + { + var found = false; + for (var i = 0; i < policy.Methods.Count; i++) + { + var method = policy.Methods[i]; + if (string.Equals(method, accessControlRequestMethod, StringComparison.OrdinalIgnoreCase)) + { + found = true; + break; + } + } + + if (!found) + { + _logger?.PolicyFailure(); + _logger?.AccessControlMethodNotAllowed(accessControlRequestMethod); + return; + } + } + + if (!policy.AllowAnyHeader && + requestHeaders != null) + { + 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); + result.SupportsCredentials = policy.SupportsCredentials; + result.PreflightMaxAge = policy.PreflightMaxAge; + result.AllowedMethods.Add(accessControlRequestMethod); + AddHeaderValues(result.AllowedHeaders, requestHeaders); + _logger?.PolicySuccess(); + } + + /// + public virtual void ApplyResult(CorsResult result, HttpResponse response) + { + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + + var headers = response.Headers; + + if (result.AllowedOrigin != null) + { + headers[CorsConstants.AccessControlAllowOrigin] = result.AllowedOrigin; + } + + if (result.VaryByOrigin) + { + headers["Vary"] = "Origin"; + } + + if (result.SupportsCredentials) + { + headers[CorsConstants.AccessControlAllowCredentials] = "true"; + } + + if (result.AllowedMethods.Count > 0) + { + // Filter out simple methods + var nonSimpleAllowMethods = result.AllowedMethods + .Where(m => + !CorsConstants.SimpleMethods.Contains(m, StringComparer.OrdinalIgnoreCase)) + .ToArray(); + + if (nonSimpleAllowMethods.Length > 0) + { + headers.SetCommaSeparatedValues( + CorsConstants.AccessControlAllowMethods, + nonSimpleAllowMethods); + } + } + + if (result.AllowedHeaders.Count > 0) + { + // Filter out simple request headers + var nonSimpleAllowRequestHeaders = result.AllowedHeaders + .Where(header => + !CorsConstants.SimpleRequestHeaders.Contains(header, StringComparer.OrdinalIgnoreCase)) + .ToArray(); + + if (nonSimpleAllowRequestHeaders.Length > 0) + { + headers.SetCommaSeparatedValues( + CorsConstants.AccessControlAllowHeaders, + nonSimpleAllowRequestHeaders); + } + } + + if (result.AllowedExposedHeaders.Count > 0) + { + // Filter out simple response headers + var nonSimpleAllowResponseHeaders = result.AllowedExposedHeaders + .Where(header => + !CorsConstants.SimpleResponseHeaders.Contains(header, StringComparer.OrdinalIgnoreCase)) + .ToArray(); + + if (nonSimpleAllowResponseHeaders.Length > 0) + { + headers.SetCommaSeparatedValues( + CorsConstants.AccessControlExposeHeaders, + nonSimpleAllowResponseHeaders); + } + } + + if (result.PreflightMaxAge.HasValue) + { + headers[CorsConstants.AccessControlMaxAge] + = result.PreflightMaxAge.Value.TotalSeconds.ToString(CultureInfo.InvariantCulture); + } + } + + private void AddOriginToResult(string origin, CorsPolicy policy, CorsResult result) + { + if (policy.AllowAnyOrigin) + { + if (policy.SupportsCredentials) + { + result.AllowedOrigin = origin; + result.VaryByOrigin = true; + } + else + { + result.AllowedOrigin = CorsConstants.AnyOrigin; + } + } + else if (policy.IsOriginAllowed(origin)) + { + result.AllowedOrigin = origin; + + if(policy.Origins.Count > 1) + { + result.VaryByOrigin = true; + } + } + } + + private static void AddHeaderValues(IList target, IEnumerable headerValues) + { + if (headerValues == null) + { + return; + } + + foreach (var current in headerValues) + { + target.Add(current); + } + } + + private bool IsOriginAllowed(CorsPolicy policy, StringValues origin) + { + if (StringValues.IsNullOrEmpty(origin)) + { + _logger?.RequestDoesNotHaveOriginHeader(); + return false; + } + + _logger?.RequestHasOriginHeader(origin); + if (policy.AllowAnyOrigin || policy.IsOriginAllowed(origin)) + { + return true; + } + _logger?.PolicyFailure(); + _logger?.OriginNotAllowed(origin); + return false; + } + } +} diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/DefaultCorsPolicyProvider.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/DefaultCorsPolicyProvider.cs new file mode 100644 index 0000000000..4841d60f75 --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/DefaultCorsPolicyProvider.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 System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + /// + public class DefaultCorsPolicyProvider : ICorsPolicyProvider + { + private readonly CorsOptions _options; + + /// + /// Creates a new instance of . + /// + /// The options configured for the application. + public DefaultCorsPolicyProvider(IOptions options) + { + _options = options.Value; + } + + /// + public Task GetPolicyAsync(HttpContext context, string policyName) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.GetPolicy(policyName ?? _options.DefaultPolicyName)); + } + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/ICorsPolicyProvider.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/ICorsPolicyProvider.cs new file mode 100644 index 0000000000..7785e3de9c --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/ICorsPolicyProvider.cs @@ -0,0 +1,22 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + /// + /// A type which can provide a for a particular . + /// + public interface ICorsPolicyProvider + { + /// + /// Gets a from the given + /// + /// The associated with this call. + /// An optional policy name to look for. + /// A + Task GetPolicyAsync(HttpContext context, string policyName); + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/ICorsService.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/ICorsService.cs new file mode 100644 index 0000000000..ab8176a961 --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/ICorsService.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + /// + /// A type which can evaluate a policy for a particular . + /// + public interface ICorsService + { + /// + /// Evaluates the given using the passed in . + /// + /// The associated with the call. + /// The which needs to be evaluated. + /// A which contains the result of policy evaluation and can be + /// used by the caller to set appropriate response headers. + CorsResult EvaluatePolicy(HttpContext context, CorsPolicy policy); + + + /// + /// Adds CORS-specific response headers to the given . + /// + /// The used to read the allowed values. + /// The associated with the current call. + void ApplyResult(CorsResult result, HttpResponse response); + + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/IDisableCorsAttribute.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/IDisableCorsAttribute.cs new file mode 100644 index 0000000000..1e69ba3da3 --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/IDisableCorsAttribute.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + /// + /// An interface which can be used to identify a type which provides metdata to disable cors for a resource. + /// + public interface IDisableCorsAttribute + { + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/IEnableCorsAttribute.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/IEnableCorsAttribute.cs new file mode 100644 index 0000000000..c58e2a1d96 --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/IEnableCorsAttribute.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + /// + /// An interface which can be used to identify a type which provides metadata needed for enabling CORS support. + /// + public interface IEnableCorsAttribute + { + /// + /// The name of the policy which needs to be applied. + /// + string PolicyName { get; set; } + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/UriHelpers.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/UriHelpers.cs new file mode 100644 index 0000000000..6c420e9260 --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/UriHelpers.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + internal static class UriHelpers + { + public static bool IsSubdomainOf(Uri subdomain, Uri domain) + { + return subdomain.IsAbsoluteUri + && domain.IsAbsoluteUri + && subdomain.Scheme == domain.Scheme + && subdomain.Port == domain.Port + && subdomain.Host.EndsWith($".{domain.Host}", StringComparison.Ordinal); + } + } +} \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs new file mode 100644 index 0000000000..727d19a4ea --- /dev/null +++ b/src/CORS/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/CORS/src/Microsoft.AspNetCore.Cors/Microsoft.AspNetCore.Cors.csproj b/src/CORS/src/Microsoft.AspNetCore.Cors/Microsoft.AspNetCore.Cors.csproj new file mode 100644 index 0000000000..10d8530a2e --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Microsoft.AspNetCore.Cors.csproj @@ -0,0 +1,22 @@ + + + + CORS middleware and policy for ASP.NET Core to enable cross-origin resource sharing. +Commonly used types: +Microsoft.AspNetCore.Cors.DisableCorsAttribute +Microsoft.AspNetCore.Cors.EnableCorsAttribute + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;cors + + + + + + + + + + + diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Properties/AssemblyInfo.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..b3d086e2e5 --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Cors.Test,PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Resources.Designer.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Resources.Designer.cs new file mode 100644 index 0000000000..0bc258074c --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNetCore.Cors { + using System; + using System.Reflection; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNetCore.Cors.Resources", typeof(Resources).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to PreflightMaxAge must be greater than or equal to 0.. + /// + internal static string PreflightMaxAgeOutOfRange { + get { + return ResourceManager.GetString("PreflightMaxAgeOutOfRange", resourceCulture); + } + } + } +} diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Resources.resx b/src/CORS/src/Microsoft.AspNetCore.Cors/Resources.resx new file mode 100644 index 0000000000..6b9ebaad31 --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Resources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + PreflightMaxAge must be greater than or equal to 0. + + \ No newline at end of file diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/baseline.netcore.json b/src/CORS/src/Microsoft.AspNetCore.Cors/baseline.netcore.json new file mode 100644 index 0000000000..ef96cf2eed --- /dev/null +++ b/src/CORS/src/Microsoft.AspNetCore.Cors/baseline.netcore.json @@ -0,0 +1,1250 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Cors, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Builder.CorsMiddlewareExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseCors", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseCors", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "policyName", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseCors", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "configurePolicy", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Cors.DisableCorsAttribute", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.Attribute", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Cors.Infrastructure.IDisableCorsAttribute" + ], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Cors.EnableCorsAttribute", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.Attribute", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Cors.Infrastructure.IEnableCorsAttribute" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_PolicyName", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Cors.Infrastructure.IEnableCorsAttribute", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PolicyName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Cors.Infrastructure.IEnableCorsAttribute", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "policyName", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Cors.Infrastructure.CorsConstants", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "PreflightHttpMethod", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Origin", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AnyOrigin", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AccessControlRequestMethod", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AccessControlRequestHeaders", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AccessControlAllowOrigin", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AccessControlAllowHeaders", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AccessControlExposeHeaders", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AccessControlAllowMethods", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AccessControlAllowCredentials", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AccessControlMaxAge", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "corsService", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsService" + }, + { + "Name": "policyProvider", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsPolicyProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "corsService", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsService" + }, + { + "Name": "policyProvider", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsPolicyProvider" + }, + { + "Name": "policyName", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "corsService", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsService" + }, + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Cors.Infrastructure.CorsOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_DefaultPolicyName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultPolicyName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddDefaultPolicy", + "Parameters": [ + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddDefaultPolicy", + "Parameters": [ + { + "Name": "configurePolicy", + "Type": "System.Action" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddPolicy", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddPolicy", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "configurePolicy", + "Type": "System.Action" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetPolicy", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AllowAnyHeader", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AllowAnyMethod", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AllowAnyOrigin", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsOriginAllowed", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IsOriginAllowed", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ExposedHeaders", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Headers", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Methods", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Origins", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PreflightMaxAge", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PreflightMaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SupportsCredentials", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SupportsCredentials", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "WithOrigins", + "Parameters": [ + { + "Name": "origins", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WithHeaders", + "Parameters": [ + { + "Name": "headers", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WithExposedHeaders", + "Parameters": [ + { + "Name": "exposedHeaders", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WithMethods", + "Parameters": [ + { + "Name": "methods", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AllowCredentials", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DisallowCredentials", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AllowAnyOrigin", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AllowAnyMethod", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AllowAnyHeader", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetPreflightMaxAge", + "Parameters": [ + { + "Name": "preflightMaxAge", + "Type": "System.TimeSpan" + } + ], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetIsOriginAllowed", + "Parameters": [ + { + "Name": "isOriginAllowed", + "Type": "System.Func" + } + ], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetIsOriginAllowedToAllowWildcardSubdomains", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "origins", + "Type": "System.String[]", + "IsParams": true + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AllowedOrigin", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AllowedOrigin", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SupportsCredentials", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SupportsCredentials", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AllowedMethods", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AllowedHeaders", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AllowedExposedHeaders", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_VaryByOrigin", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_VaryByOrigin", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PreflightMaxAge", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PreflightMaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Cors.Infrastructure.CorsService", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Cors.Infrastructure.ICorsService" + ], + "Members": [ + { + "Kind": "Method", + "Name": "EvaluatePolicy", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "policyName", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EvaluatePolicy", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy" + } + ], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsService", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EvaluateRequest", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy" + }, + { + "Name": "result", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EvaluatePreflightRequest", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy" + }, + { + "Name": "result", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ApplyResult", + "Parameters": [ + { + "Name": "result", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult" + }, + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsService", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + }, + { + "Name": "loggerFactory", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Cors.Infrastructure.DefaultCorsPolicyProvider", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Cors.Infrastructure.ICorsPolicyProvider" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetPolicyAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "policyName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsPolicyProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsPolicyProvider", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetPolicyAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "policyName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsService", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "EvaluatePolicy", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy" + } + ], + "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ApplyResult", + "Parameters": [ + { + "Name": "result", + "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult" + }, + { + "Name": "response", + "Type": "Microsoft.AspNetCore.Http.HttpResponse" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Cors.Infrastructure.IDisableCorsAttribute", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Cors.Infrastructure.IEnableCorsAttribute", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_PolicyName", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_PolicyName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.DependencyInjection.CorsServiceCollectionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddCors", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddCors", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "setupAction", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/CORS/test/Directory.Build.props b/src/CORS/test/Directory.Build.props new file mode 100644 index 0000000000..4244624987 --- /dev/null +++ b/src/CORS/test/Directory.Build.props @@ -0,0 +1,15 @@ + + + + + netcoreapp2.1 + $(DeveloperBuildTestTfms) + $(StandardTestTfms);netcoreapp2.0 + $(StandardTestTfms);net461 + + + + + + + diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsMiddlewareFunctionalTest.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsMiddlewareFunctionalTest.cs new file mode 100644 index 0000000000..008a4926d9 --- /dev/null +++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsMiddlewareFunctionalTest.cs @@ -0,0 +1,98 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsMiddlewareFunctionalTests : IClassFixture> + { + public CorsMiddlewareFunctionalTests(CorsTestFixture fixture) + { + Client = fixture.Client; + } + + public HttpClient Client { get; } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + [InlineData("POST")] + public async Task ResourceWithSimpleRequestPolicy_Allows_SimpleRequests(string method) + { + // Arrange + var path = "/CorsMiddleware/EC6AA70D-BA3E-4B71-A87F-18625ADDB2BD"; + var origin = "http://example.com"; + var request = new HttpRequestMessage(new HttpMethod(method), path); + request.Headers.Add(CorsConstants.Origin, origin); + + // Act + var response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal(path, content); + var responseHeaders = response.Headers; + var header = Assert.Single(response.Headers); + Assert.Equal(CorsConstants.AccessControlAllowOrigin, header.Key); + Assert.Equal(new[] { "http://example.com" }, header.Value.ToArray()); + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + [InlineData("POST")] + [InlineData("PUT")] + public async Task PolicyFailed_Disallows_PreFlightRequest(string method) + { + // Arrange + var path = "/CorsMiddleware/9B8BB9C6-5BF2-4255-A636-DCB450D51AAE"; + var request = new HttpRequestMessage(new HttpMethod(CorsConstants.PreflightHttpMethod), path); + + // Adding a custom header makes it a non-simple request. + request.Headers.Add(CorsConstants.Origin, "http://example.com"); + request.Headers.Add(CorsConstants.AccessControlRequestMethod, method); + request.Headers.Add(CorsConstants.AccessControlRequestHeaders, "Custom"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + // Middleware applied the policy and since that did not pass, there were no access control headers. + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + Assert.Empty(response.Headers); + + // It should short circuit and hence no result. + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal(string.Empty, content); + } + + [Fact] + public async Task PolicyFailed_Allows_ActualRequest_WithMissingResponseHeaders() + { + // Arrange + var path = "/CorsMiddleware/1E6C6F4D-1E1C-450E-8BD0-73DBF089A78F"; + var request = new HttpRequestMessage(HttpMethod.Put, path); + + // Adding a custom header makes it a non simple request. + request.Headers.Add(CorsConstants.Origin, "http://example2.com"); + + // Act + var response = await Client.SendAsync(request); + + // Assert + // Middleware applied the policy and since that did not pass, there were no access control headers. + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers); + + // It still has executed the action. + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal(path, content); + } + } +} \ No newline at end of file diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsMiddlewareTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsMiddlewareTests.cs new file mode 100644 index 0000000000..d74c020eae --- /dev/null +++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsMiddlewareTests.cs @@ -0,0 +1,353 @@ +// 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.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsMiddlewareTests + { + [Theory] + [InlineData("PuT")] + [InlineData("PUT")] + public async Task CorsRequest_MatchesPolicy_OnCaseInsensitiveAccessControlRequestMethod(string accessControlRequestMethod) + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors(builder => + builder.WithOrigins("http://localhost:5001") + .WithMethods("PUT")); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => services.AddCors()); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Actual request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, "http://localhost:5001") + .SendAsync(accessControlRequestMethod); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Single(response.Headers); + Assert.Equal("Cross origin response", await response.Content.ReadAsStringAsync()); + Assert.Equal("http://localhost:5001", response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault()); + } + } + + [Fact] + public async Task CorsRequest_MatchPolicy_SetsResponseHeaders() + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors(builder => + builder.WithOrigins("http://localhost:5001") + .WithMethods("PUT") + .WithHeaders("Header1") + .WithExposedHeaders("AllowedHeader")); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => services.AddCors()); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Actual request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, "http://localhost:5001") + .SendAsync("PUT"); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Equal(2, response.Headers.Count()); + Assert.Equal("Cross origin response", await response.Content.ReadAsStringAsync()); + Assert.Equal("http://localhost:5001", response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault()); + Assert.Equal("AllowedHeader", response.Headers.GetValues(CorsConstants.AccessControlExposeHeaders).FirstOrDefault()); + } + } + + [Theory] + [InlineData("OpTions")] + [InlineData("OPTIONS")] + public async Task PreFlight_MatchesPolicy_OnCaseInsensitiveOptionsMethod(string preflightMethod) + { + // Arrange + var policy = new CorsPolicy(); + policy.Origins.Add("http://localhost:5001"); + policy.Methods.Add("PUT"); + + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors("customPolicy"); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => + { + services.AddCors(options => + { + options.AddPolicy("customPolicy", policy); + }); + }); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Preflight request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, "http://localhost:5001") + .SendAsync(preflightMethod); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Single(response.Headers); + Assert.Equal("http://localhost:5001", response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault()); + } + } + + [Fact] + public async Task PreFlight_MatchesPolicy_SetsResponseHeaders() + { + // Arrange + var policy = new CorsPolicy(); + policy.Origins.Add("http://localhost:5001"); + policy.Methods.Add("PUT"); + policy.Headers.Add("Header1"); + policy.ExposedHeaders.Add("AllowedHeader"); + + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors("customPolicy"); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => + { + services.AddCors(options => + { + options.AddPolicy("customPolicy", policy); + }); + }); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Preflight request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, "http://localhost:5001") + .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT") + .SendAsync(CorsConstants.PreflightHttpMethod); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Equal(2, response.Headers.Count()); + Assert.Equal("http://localhost:5001", response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault()); + Assert.Equal("PUT", response.Headers.GetValues(CorsConstants.AccessControlAllowMethods).FirstOrDefault()); + } + } + + [Fact] + public async Task PreFlightRequest_DoesNotMatchPolicy_DoesNotSetHeaders() + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors(builder => + builder.WithOrigins("http://localhost:5001") + .WithMethods("PUT") + .WithHeaders("Header1") + .WithExposedHeaders("AllowedHeader")); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => services.AddCors()); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Preflight request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, "http://localhost:5002") + .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT") + .SendAsync(CorsConstants.PreflightHttpMethod); + + // Assert + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + Assert.Empty(response.Headers); + } + } + + [Fact] + public async Task CorsRequest_DoesNotMatchPolicy_DoesNotSetHeaders() + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors(builder => + builder.WithOrigins("http://localhost:5001") + .WithMethods("PUT") + .WithHeaders("Header1") + .WithExposedHeaders("AllowedHeader")); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => services.AddCors()); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Actual request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, "http://localhost:5002") + .SendAsync("PUT"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers); + } + } + + [Fact] + public async Task Uses_PolicyProvider_AsFallback() + { + // Arrange + var corsService = Mock.Of(); + var mockProvider = new Mock(); + mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(null)) + .Verifiable(); + + var middleware = new CorsMiddleware( + Mock.Of(), + corsService, + mockProvider.Object, + policyName: null); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + + // Act + await middleware.Invoke(httpContext); + + // Assert + mockProvider.Verify( + o => o.GetPolicyAsync(It.IsAny(), It.IsAny()), + Times.Once); + } + + [Fact] + public async Task DoesNotSetHeaders_ForNoPolicy() + { + // Arrange + var corsService = Mock.Of(); + var mockProvider = new Mock(); + mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(null)) + .Verifiable(); + + var middleware = new CorsMiddleware( + Mock.Of(), + corsService, + mockProvider.Object, + policyName: null); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" }); + + // Act + await middleware.Invoke(httpContext); + + // Assert + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Empty(httpContext.Response.Headers); + mockProvider.Verify( + o => o.GetPolicyAsync(It.IsAny(), It.IsAny()), + Times.Once); + } + + [Fact] + public async Task PreFlight_MatchesDefaultPolicy_SetsResponseHeaders() + { + // Arrange + var hostBuilder = new WebHostBuilder() + .Configure(app => + { + app.UseCors(); + app.Run(async context => + { + await context.Response.WriteAsync("Cross origin response"); + }); + }) + .ConfigureServices(services => + { + services.AddCors(options => + { + options.AddDefaultPolicy(policyBuilder => + { + policyBuilder + .WithOrigins("http://localhost:5001") + .WithMethods("PUT") + .WithHeaders("Header1") + .WithExposedHeaders("AllowedHeader") + .Build(); + }); + options.AddPolicy("policy2", policyBuilder => + { + policyBuilder + .WithOrigins("http://localhost:5002") + .Build(); + }); + }); + }); + + using (var server = new TestServer(hostBuilder)) + { + // Act + // Preflight request. + var response = await server.CreateRequest("/") + .AddHeader(CorsConstants.Origin, "http://localhost:5001") + .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT") + .SendAsync(CorsConstants.PreflightHttpMethod); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Equal(2, response.Headers.Count()); + Assert.Equal("http://localhost:5001", response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault()); + Assert.Equal("PUT", response.Headers.GetValues(CorsConstants.AccessControlAllowMethods).FirstOrDefault()); + } + } + } +} \ No newline at end of file diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsOptionsTest.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsOptionsTest.cs new file mode 100644 index 0000000000..360231d38b --- /dev/null +++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsOptionsTest.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsOptionsTest + { + [Fact] + public void AddDefaultPolicy_SetsDefaultPolicyName() + { + // Arrange + var corsOptions = new CorsOptions(); + var expectedPolicy = new CorsPolicy(); + + // Act + corsOptions.AddPolicy("policy1", new CorsPolicy()); + corsOptions.AddDefaultPolicy(expectedPolicy); + corsOptions.AddPolicy("policy3", new CorsPolicy()); + + // Assert + var actualPolicy = corsOptions.GetPolicy(corsOptions.DefaultPolicyName); + Assert.Same(expectedPolicy, actualPolicy); + } + + [Fact] + public void AddDefaultPolicy_OverridesDefaultPolicyName() + { + // Arrange + var corsOptions = new CorsOptions(); + var expectedPolicy = new CorsPolicy(); + + // Act + corsOptions.AddDefaultPolicy(new CorsPolicy()); + corsOptions.AddDefaultPolicy(expectedPolicy); + + // Assert + var actualPolicy = corsOptions.GetPolicy(corsOptions.DefaultPolicyName); + Assert.Same(expectedPolicy, actualPolicy); + } + + [Fact] + public void AddDefaultPolicy_UsingPolicyBuilder_SetsDefaultPolicyName() + { + // Arrange + var corsOptions = new CorsOptions(); + CorsPolicy expectedPolicy = null; + + // Act + corsOptions.AddPolicy("policy1", policyBuilder => + { + policyBuilder.AllowAnyOrigin().Build(); + }); + corsOptions.AddDefaultPolicy(policyBuilder => + { + expectedPolicy = policyBuilder.AllowAnyOrigin().Build(); + }); + corsOptions.AddPolicy("policy3", new CorsPolicy()); + + // Assert + var actualPolicy = corsOptions.GetPolicy(corsOptions.DefaultPolicyName); + Assert.Same(expectedPolicy, actualPolicy); + } + } +} \ No newline at end of file diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyBuilderTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyBuilderTests.cs new file mode 100644 index 0000000000..8a223ad225 --- /dev/null +++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyBuilderTests.cs @@ -0,0 +1,292 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsPolicyBuilderTests + { + [Fact] + public void Constructor_WithPolicy_AddsTheGivenPolicy() + { + // Arrange + Func isOriginAllowed = origin => true; + var originalPolicy = new CorsPolicy(); + originalPolicy.Origins.Add("http://existing.com"); + originalPolicy.Headers.Add("Existing"); + originalPolicy.Methods.Add("GET"); + originalPolicy.ExposedHeaders.Add("ExistingExposed"); + originalPolicy.SupportsCredentials = true; + originalPolicy.PreflightMaxAge = TimeSpan.FromSeconds(12); + originalPolicy.IsOriginAllowed = isOriginAllowed; + + // Act + var builder = new CorsPolicyBuilder(originalPolicy); + + // Assert + var corsPolicy = builder.Build(); + + Assert.False(corsPolicy.AllowAnyHeader); + Assert.False(corsPolicy.AllowAnyMethod); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.True(corsPolicy.SupportsCredentials); + Assert.NotSame(originalPolicy.Headers, corsPolicy.Headers); + Assert.Equal(originalPolicy.Headers, corsPolicy.Headers); + Assert.NotSame(originalPolicy.Methods, corsPolicy.Methods); + Assert.Equal(originalPolicy.Methods, corsPolicy.Methods); + Assert.NotSame(originalPolicy.Origins, corsPolicy.Origins); + Assert.Equal(originalPolicy.Origins, corsPolicy.Origins); + Assert.NotSame(originalPolicy.ExposedHeaders, corsPolicy.ExposedHeaders); + Assert.Equal(originalPolicy.ExposedHeaders, corsPolicy.ExposedHeaders); + Assert.Equal(TimeSpan.FromSeconds(12), corsPolicy.PreflightMaxAge); + Assert.Same(originalPolicy.IsOriginAllowed, corsPolicy.IsOriginAllowed); + } + + [Fact] + public void ConstructorWithPolicy_HavingNullPreflightMaxAge_AddsTheGivenPolicy() + { + // Arrange + var originalPolicy = new CorsPolicy(); + originalPolicy.Origins.Add("http://existing.com"); + + // Act + var builder = new CorsPolicyBuilder(originalPolicy); + + // Assert + var corsPolicy = builder.Build(); + + Assert.Null(corsPolicy.PreflightMaxAge); + Assert.False(corsPolicy.AllowAnyHeader); + Assert.False(corsPolicy.AllowAnyMethod); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.NotSame(originalPolicy.Origins, corsPolicy.Origins); + Assert.Equal(originalPolicy.Origins, corsPolicy.Origins); + Assert.Empty(corsPolicy.Headers); + Assert.Empty(corsPolicy.Methods); + Assert.Empty(corsPolicy.ExposedHeaders); + } + + [Fact] + public void Constructor_WithNoOrigin() + { + // Arrange & Act + var builder = new CorsPolicyBuilder(); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyHeader); + Assert.False(corsPolicy.AllowAnyMethod); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.False(corsPolicy.SupportsCredentials); + Assert.Empty(corsPolicy.ExposedHeaders); + Assert.Empty(corsPolicy.Headers); + Assert.Empty(corsPolicy.Methods); + Assert.Empty(corsPolicy.Origins); + Assert.Null(corsPolicy.PreflightMaxAge); + } + + [Theory] + [InlineData("")] + [InlineData("http://example.com,http://example2.com")] + public void Constructor_WithParamsOrigin_InitializesOrigin(string origin) + { + // Arrange + var origins = origin.Split(','); + + // Act + var builder = new CorsPolicyBuilder(origins); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyHeader); + Assert.False(corsPolicy.AllowAnyMethod); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.False(corsPolicy.SupportsCredentials); + Assert.Empty(corsPolicy.ExposedHeaders); + Assert.Empty(corsPolicy.Headers); + Assert.Empty(corsPolicy.Methods); + Assert.Equal(origins.ToList(), corsPolicy.Origins); + Assert.Null(corsPolicy.PreflightMaxAge); + } + + [Fact] + public void WithOrigins_AddsOrigins() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.WithOrigins("http://example.com", "http://example2.com"); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.Equal(new List() { "http://example.com", "http://example2.com" }, corsPolicy.Origins); + } + + [Fact] + public void AllowAnyOrigin_AllowsAny() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AllowAnyOrigin(); + + // Assert + var corsPolicy = builder.Build(); + Assert.True(corsPolicy.AllowAnyOrigin); + Assert.Equal(new List() { "*" }, corsPolicy.Origins); + } + + [Fact] + public void SetIsOriginAllowed_AddsIsOriginAllowed() + { + // Arrange + var builder = new CorsPolicyBuilder(); + Func isOriginAllowed = origin => true; + + // Act + builder.SetIsOriginAllowed(isOriginAllowed); + + // Assert + var corsPolicy = builder.Build(); + Assert.Same(corsPolicy.IsOriginAllowed, isOriginAllowed); + } + + [Fact] + public void SetIsOriginAllowedToAllowWildcardSubdomains_AllowsWildcardSubdomains() + { + // Arrange + var builder = new CorsPolicyBuilder("http://*.example.com"); + + // Act + builder.SetIsOriginAllowedToAllowWildcardSubdomains(); + + // Assert + var corsPolicy = builder.Build(); + Assert.True(corsPolicy.IsOriginAllowed("http://test.example.com")); + } + + [Fact] + public void WithMethods_AddsMethods() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.WithMethods("PUT", "GET"); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.Equal(new List() { "PUT", "GET" }, corsPolicy.Methods); + } + + [Fact] + public void AllowAnyMethod_AllowsAny() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AllowAnyMethod(); + + // Assert + var corsPolicy = builder.Build(); + Assert.True(corsPolicy.AllowAnyMethod); + Assert.Equal(new List() { "*" }, corsPolicy.Methods); + } + + [Fact] + public void WithHeaders_AddsHeaders() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.WithHeaders("example1", "example2"); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyHeader); + Assert.Equal(new List() { "example1", "example2" }, corsPolicy.Headers); + } + + [Fact] + public void AllowAnyHeaders_AllowsAny() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AllowAnyHeader(); + + // Assert + var corsPolicy = builder.Build(); + Assert.True(corsPolicy.AllowAnyHeader); + Assert.Equal(new List() { "*" }, corsPolicy.Headers); + } + + [Fact] + public void WithExposedHeaders_AddsExposedHeaders() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.WithExposedHeaders("exposed1", "exposed2"); + + // Assert + var corsPolicy = builder.Build(); + Assert.Equal(new List() { "exposed1", "exposed2" }, corsPolicy.ExposedHeaders); + } + + [Fact] + public void SetPreFlightMaxAge_SetsThePreFlightAge() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.SetPreflightMaxAge(TimeSpan.FromSeconds(12)); + + // Assert + var corsPolicy = builder.Build(); + Assert.Equal(TimeSpan.FromSeconds(12), corsPolicy.PreflightMaxAge); + } + + [Fact] + public void AllowCredential_SetsSupportsCredentials_ToTrue() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AllowCredentials(); + + // Assert + var corsPolicy = builder.Build(); + Assert.True(corsPolicy.SupportsCredentials); + } + + + [Fact] + public void DisallowCredential_SetsSupportsCredentials_ToFalse() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.DisallowCredentials(); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.SupportsCredentials); + } + } +} \ No newline at end of file diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyExtensionsTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyExtensionsTests.cs new file mode 100644 index 0000000000..74dd67db0b --- /dev/null +++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyExtensionsTests.cs @@ -0,0 +1,85 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public sealed class CorsPolicyExtensionsTest + { + [Fact] + public void IsOriginAnAllowedSubdomain_ReturnsTrueIfPolicyContainsOrigin() + { + // Arrange + const string origin = "http://sub.domain"; + var policy = new CorsPolicy(); + policy.Origins.Add(origin); + + // Act + var actual = policy.IsOriginAnAllowedSubdomain(origin); + + // Assert + Assert.True(actual); + } + + [Theory] + [InlineData(null)] + [InlineData("null")] + [InlineData("http://")] + [InlineData("http://*")] + [InlineData("http://.domain")] + [InlineData("http://.domain/hello")] + public void IsOriginAnAllowedSubdomain_ReturnsFalseIfOriginIsMalformedUri(string malformedOrigin) + { + // Arrange + var policy = new CorsPolicy(); + policy.Origins.Add("http://*.domain"); + + // Act + var actual = policy.IsOriginAnAllowedSubdomain(malformedOrigin); + + // Assert + Assert.False(actual); + } + + [Theory] + [InlineData("http://sub.domain", "http://*.domain")] + [InlineData("http://sub.sub.domain", "http://*.domain")] + [InlineData("http://sub.sub.domain", "http://*.sub.domain")] + [InlineData("http://sub.domain:4567", "http://*.domain:4567")] + public void IsOriginAnAllowedSubdomain_ReturnsTrue_WhenASubdomain(string origin, string allowedOrigin) + { + // Arrange + var policy = new CorsPolicy(); + policy.Origins.Add(allowedOrigin); + + // Act + var isAllowed = policy.IsOriginAnAllowedSubdomain(origin); + + // Assert + Assert.True(isAllowed); + } + + [Theory] + [InlineData("http://domain", "http://*.domain")] + [InlineData("http://sub.domain", "http://domain")] + [InlineData("http://sub.domain:1234", "http://*.domain:5678")] + [InlineData("http://sub.domain", "http://domain.*")] + [InlineData("http://sub.sub.domain", "http://sub.*.domain")] + [InlineData("http://sub.domain.hacker", "http://*.domain")] + [InlineData("https://sub.domain", "http://*.domain")] + public void IsOriginAnAllowedSubdomain_ReturnsFalse_WhenNotASubdomain(string origin, string allowedOrigin) + { + // Arrange + var policy = new CorsPolicy(); + policy.Origins.Add(allowedOrigin); + + // Act + var isAllowed = policy.IsOriginAnAllowedSubdomain(origin); + + // Assert + Assert.False(isAllowed); + } + } +} \ No newline at end of file diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyTests.cs new file mode 100644 index 0000000000..f49e30fabc --- /dev/null +++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyTests.cs @@ -0,0 +1,74 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsPolicyTest + { + [Fact] + public void Default_Constructor() + { + // Arrange & Act + var corsPolicy = new CorsPolicy(); + + // Assert + Assert.False(corsPolicy.AllowAnyHeader); + Assert.False(corsPolicy.AllowAnyMethod); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.False(corsPolicy.SupportsCredentials); + Assert.Empty(corsPolicy.ExposedHeaders); + Assert.Empty(corsPolicy.Headers); + Assert.Empty(corsPolicy.Methods); + Assert.Empty(corsPolicy.Origins); + Assert.Null(corsPolicy.PreflightMaxAge); + Assert.NotNull(corsPolicy.IsOriginAllowed); + } + + [Fact] + public void SettingNegativePreflightMaxAge_Throws() + { + // Arrange + var policy = new CorsPolicy(); + + // Act + var exception = Assert.Throws(() => + { + policy.PreflightMaxAge = TimeSpan.FromSeconds(-12); + }); + + // Assert + Assert.Equal( + $"PreflightMaxAge must be greater than or equal to 0.{Environment.NewLine}Parameter name: value", + exception.Message); + } + + [Fact] + public void ToString_ReturnsThePropertyValues() + { + // Arrange + var corsPolicy = new CorsPolicy + { + PreflightMaxAge = TimeSpan.FromSeconds(12), + SupportsCredentials = true + }; + corsPolicy.Headers.Add("foo"); + corsPolicy.Headers.Add("bar"); + corsPolicy.Origins.Add("http://example.com"); + corsPolicy.Origins.Add("http://example.org"); + corsPolicy.Methods.Add("GET"); + + // Act + var policyString = corsPolicy.ToString(); + + // Assert + Assert.Equal( + @"AllowAnyHeader: False, AllowAnyMethod: False, AllowAnyOrigin: False, PreflightMaxAge: 12,"+ + " SupportsCredentials: True, Origins: {http://example.com,http://example.org}, Methods: {GET},"+ + " Headers: {foo,bar}, ExposedHeaders: {}", + policyString); + } + } +} \ No newline at end of file diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsResultTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsResultTests.cs new file mode 100644 index 0000000000..bc6e4a2d3a --- /dev/null +++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsResultTests.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsResultTest + { + [Fact] + public void Default_Constructor() + { + // Arrange & Act + var result = new CorsResult(); + + // Assert + Assert.Empty(result.AllowedHeaders); + Assert.Empty(result.AllowedExposedHeaders); + Assert.Empty(result.AllowedMethods); + Assert.False(result.SupportsCredentials); + Assert.Null(result.AllowedOrigin); + Assert.Null(result.PreflightMaxAge); + } + + [Fact] + public void SettingNegativePreflightMaxAge_Throws() + { + // Arrange + var result = new CorsResult(); + + // Act + var exception = Assert.Throws(() => + { + result.PreflightMaxAge = TimeSpan.FromSeconds(-1); + }); + + // Assert + Assert.Equal( + $"PreflightMaxAge must be greater than or equal to 0.{Environment.NewLine}Parameter name: value", + exception.Message); + } + + [Fact] + public void ToString_ReturnsThePropertyValues() + { + // Arrange + var corsResult = new CorsResult + { + SupportsCredentials = true, + PreflightMaxAge = TimeSpan.FromSeconds(30), + AllowedOrigin = "*" + }; + corsResult.AllowedExposedHeaders.Add("foo"); + corsResult.AllowedHeaders.Add("bar"); + corsResult.AllowedHeaders.Add("baz"); + corsResult.AllowedMethods.Add("GET"); + + // Act + var result = corsResult.ToString(); + + // Assert + Assert.Equal( + @"AllowCredentials: True, PreflightMaxAge: 30, AllowOrigin: *," + + " AllowExposedHeaders: {foo}, AllowHeaders: {bar,baz}, AllowMethods: {GET}", + result); + } + } +} \ No newline at end of file diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs new file mode 100644 index 0000000000..8a71ce7b42 --- /dev/null +++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs @@ -0,0 +1,1167 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsServiceTests + { + [Fact] + public void EvaluatePolicy_NoOrigin_ReturnsInvalidResult() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext("GET", origin: null); + + // Act + var result = corsService.EvaluatePolicy(requestContext, new CorsPolicy()); + + // Assert + Assert.Null(result.AllowedOrigin); + Assert.False(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_NoMatchingOrigin_ReturnsInvalidResult() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add("bar"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Null(result.AllowedOrigin); + Assert.False(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_EmptyOriginsPolicy_ReturnsInvalidResult() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Null(result.AllowedOrigin); + Assert.False(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_IsOriginAllowedReturnsFalse_ReturnsInvalidResult() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy() + { + IsOriginAllowed = origin => false + }; + policy.Origins.Add("example.com"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Null(result.AllowedOrigin); + Assert.False(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_AllowAnyOrigin_DoesNotSupportCredentials_EmitsWildcardForOrigin() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(origin: "http://example.com"); + + var policy = new CorsPolicy + { + SupportsCredentials = false + }; + + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal("*", result.AllowedOrigin); + } + + [Fact] + public void EvaluatePolicy_AllowAnyOrigin_SupportsCredentials_AddsSpecificOrigin() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy + { + SupportsCredentials = true + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal("http://example.com", result.AllowedOrigin); + Assert.True(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_DoesNotSupportCredentials_AllowCredentialsReturnsFalse() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy + { + SupportsCredentials = false + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.False(result.SupportsCredentials); + } + + [Fact] + public void EvaluatePolicy_SupportsCredentials_AllowCredentialsReturnsTrue() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy + { + SupportsCredentials = true + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.True(result.SupportsCredentials); + } + + [Fact] + public void EvaluatePolicy_NoExposedHeaders_NoAllowExposedHeaders() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Empty(result.AllowedExposedHeaders); + } + + [Fact] + public void EvaluatePolicy_OneExposedHeaders_HeadersAllowed() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.ExposedHeaders.Add("foo"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(1, result.AllowedExposedHeaders.Count); + Assert.Contains("foo", result.AllowedExposedHeaders); + } + + [Fact] + public void EvaluatePolicy_ManyExposedHeaders_HeadersAllowed() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.ExposedHeaders.Add("foo"); + policy.ExposedHeaders.Add("bar"); + policy.ExposedHeaders.Add("baz"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(3, result.AllowedExposedHeaders.Count); + Assert.Contains("foo", result.AllowedExposedHeaders); + Assert.Contains("bar", result.AllowedExposedHeaders); + Assert.Contains("baz", result.AllowedExposedHeaders); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_MethodNotAllowed_ReturnsInvalidResult() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("GET"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Empty(result.AllowedMethods); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_MethodAllowed_ReturnsAllowMethods() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("PUT"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.NotNull(result); + 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); + + var writeList = sink.Writes.ToList(); + Assert.Equal("The request is a preflight request.", writeList[0].State.ToString()); + Assert.Equal(logData.OriginLogMessage, writeList[1].State.ToString()); + Assert.Equal(logData.PolicyLogMessage, writeList[2].State.ToString()); + Assert.Equal(logData.FailureReason, writeList[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); + + var writeList = sink.Writes.ToList(); + Assert.Equal("The request is a preflight request.", writeList[0].State.ToString()); + Assert.Equal("The request has an origin header: 'http://allowed.example.com'.", writeList[1].State.ToString()); + Assert.Equal("Policy execution successful.", writeList[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); + + var writeList = sink.Writes.ToList(); + Assert.Equal("The request is a preflight request.", writeList[0].State.ToString()); + Assert.Equal("The request does not have an origin header.", writeList[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); + + var writeList = sink.Writes.ToList(); + Assert.Equal("The request has an origin header: 'http://example.com'.", writeList[0].State.ToString()); + Assert.Equal("Policy execution failed.", writeList[1].State.ToString()); + Assert.Equal("Request origin http://example.com does not have permission to access the resource.", writeList[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); + + var writeList = sink.Writes.ToList(); + Assert.Equal("The request has an origin header: 'http://allowed.example.com'.", writeList[0].State.ToString()); + Assert.Equal("Policy execution successful.", writeList[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")] + public void EvaluatePolicy_CaseInsensitivePreflightRequest_OriginAllowed_ReturnsOrigin(string preflightMethod) + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext( + method: preflightMethod, + origin: "http://example.com", + accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Origins.Add("http://example.com"); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal("http://example.com", result.AllowedOrigin); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_IsOriginAllowedReturnsTrue_ReturnsOrigin() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext( + method: "OPTIONS", + origin: "http://example.com", + accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy + { + IsOriginAllowed = origin => true + }; + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal("http://example.com", result.AllowedOrigin); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_SupportsCredentials_AllowCredentialsReturnsTrue() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy + { + SupportsCredentials = true + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.True(result.SupportsCredentials); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_NoPreflightMaxAge_NoPreflightMaxAgeSet() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy + { + PreflightMaxAge = null + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Null(result.PreflightMaxAge); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_PreflightMaxAge_PreflightMaxAgeSet() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy + { + PreflightMaxAge = TimeSpan.FromSeconds(10) + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(TimeSpan.FromSeconds(10), result.PreflightMaxAge); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_AnyMethod_ReturnsRequestMethod() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "GET"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(1, result.AllowedMethods.Count); + Assert.Contains("GET", result.AllowedMethods); + } + + [Theory] + [InlineData("Put")] + [InlineData("PUT")] + public void EvaluatePolicy_CaseInsensitivePreflightRequest_ListedMethod_ReturnsSubsetOfListedMethods(string method) + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext( + method: "OPTIONS", + origin: "http://example.com", + accessControlRequestMethod: method); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("PUT"); + policy.Methods.Add("DELETE"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(1, result.AllowedMethods.Count); + Assert.Contains(method, result.AllowedMethods); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_NoHeadersRequested_AllowedAllHeaders_ReturnsEmptyHeaders() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + policy.Headers.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Empty(result.AllowedHeaders); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_HeadersRequested_AllowAllHeaders_ReturnsRequestedHeaders() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext( + method: "OPTIONS", + origin: "http://example.com", + accessControlRequestMethod: "PUT", + accessControlRequestHeaders: new[] { "foo", "bar" }); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + policy.Headers.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(2, result.AllowedHeaders.Count); + Assert.Contains("foo", result.AllowedHeaders); + Assert.Contains("bar", result.AllowedHeaders); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_HeadersRequested_AllowSomeHeaders_ReturnsSubsetOfListedHeaders() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext( + method: "OPTIONS", + origin: "http://example.com", + accessControlRequestMethod: "PUT", + accessControlRequestHeaders: new[] { "content-type", "accept" }); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + policy.Headers.Add("foo"); + policy.Headers.Add("bar"); + policy.Headers.Add("Content-Type"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(2, result.AllowedHeaders.Count); + Assert.Contains("Content-Type", result.AllowedHeaders, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_HeadersRequested_NotAllHeaderMatches_ReturnsInvalidResult() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext( + method: "OPTIONS", + origin: "http://example.com", + accessControlRequestMethod: "PUT", + accessControlRequestHeaders: new[] { "match", "noMatch" }); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + policy.Headers.Add("match"); + policy.Headers.Add("foo"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Empty(result.AllowedHeaders); + Assert.Empty(result.AllowedMethods); + Assert.Empty(result.AllowedExposedHeaders); + Assert.Null(result.AllowedOrigin); + } + + [Fact] + public void EvaluatePolicy_DoesCaseSensitiveComparison() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + + var policy = new CorsPolicy(); + policy.Methods.Add("POST"); + var httpContext = GetHttpContext(origin: null, accessControlRequestMethod: "post"); + + // Act + var result = corsService.EvaluatePolicy(httpContext, policy); + + // Assert + Assert.Empty(result.AllowedHeaders); + Assert.Empty(result.AllowedMethods); + Assert.Empty(result.AllowedExposedHeaders); + Assert.Null(result.AllowedOrigin); + } + + [Fact] + public void TryValidateOrigin_DoesCaseSensitiveComparison() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + + var policy = new CorsPolicy(); + policy.Origins.Add("http://Example.com"); + var httpContext = GetHttpContext(origin: "http://example.com"); + + // Act + var result = corsService.EvaluatePolicy(httpContext, policy); + + // Assert + Assert.Empty(result.AllowedHeaders); + Assert.Empty(result.AllowedMethods); + Assert.Empty(result.AllowedExposedHeaders); + Assert.Null(result.AllowedOrigin); + } + + + [Fact] + public void ApplyResult_ReturnsNoHeaders_ByDefault() + { + // Arrange + var result = new CorsResult(); + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Empty(httpContext.Response.Headers); + } + + [Fact] + public void ApplyResult_AllowOrigin_AllowOriginHeaderAdded() + { + // Arrange + var result = new CorsResult + { + AllowedOrigin = "http://example.com" + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("http://example.com", httpContext.Response.Headers["Access-Control-Allow-Origin"]); + } + + [Fact] + public void ApplyResult_NoAllowOrigin_AllowOriginHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + AllowedOrigin = null + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Origin", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_AllowCredentials_AllowCredentialsHeaderAdded() + { + // Arrange + var result = new CorsResult + { + SupportsCredentials = true + }; + + var service = new CorsService(new TestCorsOptions()); + + // Act + var httpContext = new DefaultHttpContext(); + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("true", httpContext.Response.Headers["Access-Control-Allow-Credentials"]); + } + + [Fact] + public void ApplyResult_AddVaryHeader_VaryHeaderAdded() + { + // Arrange + var result = new CorsResult + { + VaryByOrigin = true + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("Origin", httpContext.Response.Headers["Vary"]); + } + + [Fact] + public void ApplyResult_NoAllowCredentials_AllowCredentialsHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + SupportsCredentials = false + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Credentials", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_NoAllowMethods_AllowMethodsHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + // AllowMethods is empty by default + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Methods", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_OneAllowMethods_AllowMethodsHeaderAdded() + { + // Arrange + var result = new CorsResult(); + result.AllowedMethods.Add("PUT"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("PUT", httpContext.Response.Headers["Access-Control-Allow-Methods"]); + } + + [Fact] + public void ApplyResult_SomeSimpleAllowMethods_AllowMethodsHeaderAddedForNonSimpleMethods() + { + // Arrange + var result = new CorsResult(); + result.AllowedMethods.Add("PUT"); + result.AllowedMethods.Add("get"); + result.AllowedMethods.Add("DELETE"); + result.AllowedMethods.Add("POST"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Contains("Access-Control-Allow-Methods", httpContext.Response.Headers.Keys); + var value = Assert.Single(httpContext.Response.Headers.Values); + Assert.Equal(new[] { "PUT,DELETE" }, value); + string[] methods = httpContext.Response.Headers.GetCommaSeparatedValues("Access-Control-Allow-Methods"); + Assert.Equal(2, methods.Length); + Assert.Contains("PUT", methods); + Assert.Contains("DELETE", methods); + } + + [Fact] + public void ApplyResult_SimpleAllowMethods_AllowMethodsHeaderNotAdded() + { + // Arrange + var result = new CorsResult(); + result.AllowedMethods.Add("GET"); + result.AllowedMethods.Add("HEAD"); + result.AllowedMethods.Add("POST"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Methods", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_NoAllowHeaders_AllowHeadersHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + // AllowHeaders is empty by default + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Headers", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_OneAllowHeaders_AllowHeadersHeaderAdded() + { + // Arrange + var result = new CorsResult(); + result.AllowedHeaders.Add("foo"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("foo", httpContext.Response.Headers["Access-Control-Allow-Headers"]); + } + + [Fact] + public void ApplyResult_ManyAllowHeaders_AllowHeadersHeaderAdded() + { + // Arrange + var result = new CorsResult(); + result.AllowedHeaders.Add("foo"); + result.AllowedHeaders.Add("bar"); + result.AllowedHeaders.Add("baz"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Contains("Access-Control-Allow-Headers", httpContext.Response.Headers.Keys); + var value = Assert.Single(httpContext.Response.Headers.Values); + Assert.Equal(new[] { "foo,bar,baz" }, value); + string[] headerValues = httpContext.Response.Headers.GetCommaSeparatedValues("Access-Control-Allow-Headers"); + Assert.Equal(3, headerValues.Length); + Assert.Contains("foo", headerValues); + Assert.Contains("bar", headerValues); + Assert.Contains("baz", headerValues); + } + + [Fact] + public void ApplyResult_SomeSimpleAllowHeaders_AllowHeadersHeaderAddedForNonSimpleHeaders() + { + // Arrange + var result = new CorsResult(); + result.AllowedHeaders.Add("Content-Language"); + result.AllowedHeaders.Add("foo"); + result.AllowedHeaders.Add("bar"); + result.AllowedHeaders.Add("Accept"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Contains("Access-Control-Allow-Headers", httpContext.Response.Headers.Keys); + string[] headerValues = httpContext.Response.Headers.GetCommaSeparatedValues("Access-Control-Allow-Headers"); + Assert.Equal(2, headerValues.Length); + Assert.Contains("foo", headerValues); + Assert.Contains("bar", headerValues); + } + + [Fact] + public void ApplyResult_SimpleAllowHeaders_AllowHeadersHeaderNotAdded() + { + // Arrange + var result = new CorsResult(); + result.AllowedHeaders.Add("Accept"); + result.AllowedHeaders.Add("Accept-Language"); + result.AllowedHeaders.Add("Content-Language"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Headers", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_NoAllowExposedHeaders_ExposedHeadersHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + // AllowExposedHeaders is empty by default + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Expose-Headers", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_OneAllowExposedHeaders_ExposedHeadersHeaderAdded() + { + // Arrange + var result = new CorsResult(); + result.AllowedExposedHeaders.Add("foo"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("foo", httpContext.Response.Headers["Access-Control-Expose-Headers"]); + } + + [Fact] + public void ApplyResult_ManyAllowExposedHeaders_ExposedHeadersHeaderAdded() + { + // Arrange + var result = new CorsResult(); + result.AllowedExposedHeaders.Add("foo"); + result.AllowedExposedHeaders.Add("bar"); + result.AllowedExposedHeaders.Add("baz"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Contains("Access-Control-Expose-Headers", httpContext.Response.Headers.Keys); + var value = Assert.Single(httpContext.Response.Headers.Values); + Assert.Equal(new[] { "foo,bar,baz" }, value); + string[] exposedHeaderValues = httpContext.Response.Headers.GetCommaSeparatedValues("Access-Control-Expose-Headers"); + Assert.Equal(3, exposedHeaderValues.Length); + Assert.Contains("foo", exposedHeaderValues); + Assert.Contains("bar", exposedHeaderValues); + Assert.Contains("baz", exposedHeaderValues); + } + + [Fact] + public void ApplyResult_NoPreflightMaxAge_MaxAgeHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + PreflightMaxAge = null + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Max-Age", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_PreflightMaxAge_MaxAgeHeaderAdded() + { + // Arrange + var result = new CorsResult + { + PreflightMaxAge = TimeSpan.FromSeconds(30) + }; + var httpContext = new DefaultHttpContext(); + var service = new CorsService(new TestCorsOptions()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("30", httpContext.Response.Headers["Access-Control-Max-Age"]); + } + + [Fact] + public void EvaluatePolicy_MultiOriginsPolicy_ReturnsVaryByOriginHeader() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add("http://example.com"); + policy.Origins.Add("http://example-two.com"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.NotNull(result.AllowedOrigin); + Assert.True(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_MultiOriginsPolicy_NoMatchingOrigin_ReturnsInvalidResult() + { + // Arrange + var corsService = new CorsService(new TestCorsOptions()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add("http://example-two.com"); + policy.Origins.Add("http://example-three.com"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Null(result.AllowedOrigin); + Assert.False(result.VaryByOrigin); + } + + + private static HttpContext GetHttpContext( + string method = null, + string origin = null, + string accessControlRequestMethod = null, + string[] accessControlRequestHeaders = null) + { + var context = new DefaultHttpContext(); + + if (method != null) + { + context.Request.Method = method; + } + + if (origin != null) + { + context.Request.Headers.Add(CorsConstants.Origin, new[] { origin }); + } + + if (accessControlRequestMethod != null) + { + context.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { accessControlRequestMethod }); + } + + if (accessControlRequestHeaders != null) + { + context.Request.Headers.Add(CorsConstants.AccessControlRequestHeaders, accessControlRequestHeaders); + } + + 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; } + } + } +} diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsTestFixtureOfT.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsTestFixtureOfT.cs new file mode 100644 index 0000000000..aa698c62f2 --- /dev/null +++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsTestFixtureOfT.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class CorsTestFixture : IDisposable + where TStartup : class + { + private readonly TestServer _server; + + public CorsTestFixture() + { + var builder = new WebHostBuilder().UseStartup(); + _server = new TestServer(builder); + + Client = _server.CreateClient(); + Client.BaseAddress = new Uri("http://localhost"); + } + + public HttpClient Client { get; } + + public void Dispose() + { + Client.Dispose(); + _server.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/DefaultCorsPolicyProviderTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/DefaultCorsPolicyProviderTests.cs new file mode 100644 index 0000000000..d66b0e5653 --- /dev/null +++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/DefaultCorsPolicyProviderTests.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class DefaultPolicyProviderTests + { + [Fact] + public async Task UsesTheDefaultPolicyName() + { + // Arrange + var options = new CorsOptions(); + var policy = new CorsPolicy(); + options.AddPolicy(options.DefaultPolicyName, policy); + + var corsOptions = new TestCorsOptions + { + Value = options + }; + var policyProvider = new DefaultCorsPolicyProvider(corsOptions); + + // Act + var actualPolicy = await policyProvider.GetPolicyAsync(new DefaultHttpContext(), policyName: null); + + // Assert + Assert.Same(policy, actualPolicy); + } + + [Theory] + [InlineData("")] + [InlineData("policyName")] + public async Task GetsNamedPolicy(string policyName) + { + // Arrange + var options = new CorsOptions(); + var policy = new CorsPolicy(); + options.AddPolicy(policyName, policy); + + var corsOptions = new TestCorsOptions + { + Value = options + }; + var policyProvider = new DefaultCorsPolicyProvider(corsOptions); + + // Act + var actualPolicy = await policyProvider.GetPolicyAsync(new DefaultHttpContext(), policyName); + + // Assert + Assert.Same(policy, actualPolicy); + } + } +} \ No newline at end of file diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/Microsoft.AspNetCore.Cors.Test.csproj b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/Microsoft.AspNetCore.Cors.Test.csproj new file mode 100644 index 0000000000..ff45928446 --- /dev/null +++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/Microsoft.AspNetCore.Cors.Test.csproj @@ -0,0 +1,22 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + + + + + + + + diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/TestCorsOptions.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/TestCorsOptions.cs new file mode 100644 index 0000000000..782ebb2db7 --- /dev/null +++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/TestCorsOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public class TestCorsOptions : IOptions + { + public CorsOptions Value { get; set; } + } +} diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/UriHelpersTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/UriHelpersTests.cs new file mode 100644 index 0000000000..ac04cfc3fd --- /dev/null +++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/UriHelpersTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.AspNetCore.Cors.Infrastructure +{ + public sealed class UriHelpersTests + { + [Theory] + [MemberData(nameof(IsSubdomainOfTestData))] + public void TestIsSubdomainOf(Uri subdomain, Uri domain) + { + // Act + bool isSubdomain = UriHelpers.IsSubdomainOf(subdomain, domain); + + // Assert + Assert.True(isSubdomain); + } + + [Theory] + [MemberData(nameof(IsNotSubdomainOfTestData))] + public void TestIsSubdomainOf_ReturnsFalse_WhenNotSubdomain(Uri subdomain, Uri domain) + { + // Act + bool isSubdomain = UriHelpers.IsSubdomainOf(subdomain, domain); + + // Assert + Assert.False(isSubdomain); + } + + public static IEnumerable IsSubdomainOfTestData + { + get + { + return new[] + { + new object[] {new Uri("http://sub.domain"), new Uri("http://domain")}, + new object[] {new Uri("https://sub.domain"), new Uri("https://domain")}, + new object[] {new Uri("https://sub.domain:5678"), new Uri("https://domain:5678")}, + new object[] {new Uri("http://sub.sub.domain"), new Uri("http://domain")}, + new object[] {new Uri("http://sub.sub.domain"), new Uri("http://sub.domain")} + }; + } + } + + public static IEnumerable IsNotSubdomainOfTestData + { + get + { + return new[] + { + new object[] {new Uri("http://subdomain"), new Uri("http://domain")}, + new object[] {new Uri("https://sub.domain"), new Uri("http://domain")}, + new object[] {new Uri("https://sub.domain:1234"), new Uri("https://domain:5678")}, + new object[] {new Uri("http://domain.tld"), new Uri("http://domain")}, + new object[] {new Uri("http://sub.domain.tld"), new Uri("http://domain")}, + new object[] {new Uri("/relativeUri", UriKind.Relative), new Uri("http://domain")}, + new object[] {new Uri("http://sub.domain"), new Uri("/relative", UriKind.Relative)} + }; + } + } + } +} \ No newline at end of file diff --git a/src/CORS/test/WebSites/CorsMiddlewareWebSite/CorsMiddlewareWebSite.csproj b/src/CORS/test/WebSites/CorsMiddlewareWebSite/CorsMiddlewareWebSite.csproj new file mode 100644 index 0000000000..bd2ea479be --- /dev/null +++ b/src/CORS/test/WebSites/CorsMiddlewareWebSite/CorsMiddlewareWebSite.csproj @@ -0,0 +1,16 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + + diff --git a/src/CORS/test/WebSites/CorsMiddlewareWebSite/EchoMiddleware.cs b/src/CORS/test/WebSites/CorsMiddlewareWebSite/EchoMiddleware.cs new file mode 100644 index 0000000000..9036b33bf6 --- /dev/null +++ b/src/CORS/test/WebSites/CorsMiddlewareWebSite/EchoMiddleware.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace CorsMiddlewareWebSite +{ + public class EchoMiddleware + { + /// + /// Instantiates a new . + /// + /// The next middleware in the pipeline. + public EchoMiddleware(RequestDelegate next) + { + } + + /// + /// Echo the request's path in the 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)); + } + + context.Response.ContentType = "text/plain; charset=utf-8"; + var path = context.Request.PathBase + context.Request.Path + context.Request.QueryString; + return context.Response.WriteAsync(path, Encoding.UTF8); + } + } +} \ No newline at end of file diff --git a/src/CORS/test/WebSites/CorsMiddlewareWebSite/Startup.cs b/src/CORS/test/WebSites/CorsMiddlewareWebSite/Startup.cs new file mode 100644 index 0000000000..3043f59bcd --- /dev/null +++ b/src/CORS/test/WebSites/CorsMiddlewareWebSite/Startup.cs @@ -0,0 +1,33 @@ +// 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.Extensions.DependencyInjection; + +namespace CorsMiddlewareWebSite +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddCors(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseCors(policy => policy.WithOrigins("http://example.com")); + app.UseMiddleware(); + } + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} \ No newline at end of file diff --git a/src/CORS/test/WebSites/CorsMiddlewareWebSite/readme.md b/src/CORS/test/WebSites/CorsMiddlewareWebSite/readme.md new file mode 100644 index 0000000000..d7f8b28106 --- /dev/null +++ b/src/CORS/test/WebSites/CorsMiddlewareWebSite/readme.md @@ -0,0 +1,4 @@ +CorsMiddlewareWebSite +=== + +This web site illustrates how to use CorsMiddleware to apply a policy for entire application. diff --git a/src/CORS/test/WebSites/CorsMiddlewareWebSite/web.config b/src/CORS/test/WebSites/CorsMiddlewareWebSite/web.config new file mode 100644 index 0000000000..f7ac679334 --- /dev/null +++ b/src/CORS/test/WebSites/CorsMiddlewareWebSite/web.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/CORS/version.props b/src/CORS/version.props new file mode 100644 index 0000000000..669c874829 --- /dev/null +++ b/src/CORS/version.props @@ -0,0 +1,12 @@ + + + 2.1.1 + rtm + $(VersionPrefix) + $(VersionPrefix)-$(VersionSuffix)-final + t000 + a- + $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) + $(VersionSuffix)-$(BuildNumber) + +