diff --git a/.azure/pipelines/jobs/default-build.yml b/.azure/pipelines/jobs/default-build.yml index 3c8eaf7390..7de0612fad 100644 --- a/.azure/pipelines/jobs/default-build.yml +++ b/.azure/pipelines/jobs/default-build.yml @@ -121,7 +121,7 @@ jobs: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true TeamName: AspNetCore ${{ if and(eq(parameters.installJdk, 'true'), eq(parameters.agentOs, 'Windows')) }}: - JAVA_HOME: $(Agent.BuildDirectory)\.tools\jdk + JAVA_HOME: $(Agent.BuildDirectory)\.tools\jdk\win-x64 ${{ if or(ne(parameters.codeSign, true), ne(variables['System.TeamProject'], 'internal')) }}: _SignType: '' ${{ if and(eq(parameters.codeSign, true), eq(variables['System.TeamProject'], 'internal')) }}: @@ -146,10 +146,7 @@ jobs: command: custom arguments: 'locals all -clear' - ${{ if and(eq(parameters.installJdk, 'true'), eq(parameters.agentOs, 'Windows')) }}: - - powershell: | - ./eng/scripts/InstallJdk.ps1 '11.0.1' - Write-Host "##vso[task.prependpath]$env:JAVA_HOME\bin" - + - powershell: ./eng/scripts/InstallJdk.ps1 displayName: Install JDK 11 - ${{ if eq(parameters.isTestingJob, true) }}: - powershell: | diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b962971e33..ed445df453 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,6 +6,7 @@ /.config/ @aspnet/build /build/ @aspnet/build /eng/ @aspnet/build +/eng/common/ @dotnet-maestro-bot /eng/Versions.props @dotnet-maestro-bot @dougbu /eng/Version.Details.xml @dotnet-maestro-bot @dougbu /src/Components/ @SteveSandersonMS diff --git a/build.ps1 b/build.ps1 index 2cf4e51853..b382ae3867 100644 --- a/build.ps1 +++ b/build.ps1 @@ -303,10 +303,16 @@ $MSBuildArguments += "/p:_RunSign=$Sign" $MSBuildArguments += "/p:TargetArchitecture=$Architecture" $MSBuildArguments += "/p:TargetOsName=win" -if (($All -or $BuildJava) -and -not $NoBuildJava) { +if ($RunBuild -and ($All -or $BuildJava) -and -not $NoBuildJava) { $foundJdk = $false $javac = Get-Command javac -ErrorAction Ignore -CommandType Application - if ($env:JAVA_HOME) { + $localJdkPath = "$PSScriptRoot\.tools\jdk\win-x64\" + if (Test-Path "$localJdkPath\bin\javac.exe") { + $foundJdk = $true + Write-Host -f Magenta "Detected JDK in $localJdkPath (via local repo convention)" + $env:JAVA_HOME = $localJdkPath + } + elseif ($env:JAVA_HOME) { if (-not (Test-Path "${env:JAVA_HOME}\bin\javac.exe")) { Write-Error "The environment variable JAVA_HOME was set, but ${env:JAVA_HOME}\bin\javac.exe does not exist. Remove JAVA_HOME or update it to the correct location for the JDK. See https://www.bing.com/search?q=java_home for details." } @@ -323,12 +329,19 @@ if (($All -or $BuildJava) -and -not $NoBuildJava) { } else { try { - $jdkVersion = (Get-Item HKLM:\SOFTWARE\JavaSoft\JDK | Get-ItemProperty -name CurrentVersion).CurrentVersion - $javaHome = (Get-Item HKLM:\SOFTWARE\JavaSoft\JDK\$jdkVersion | Get-ItemProperty -Name JavaHome).JavaHome - if (Test-Path "${env:JAVA_HOME}\bin\java.exe") { - $env:JAVA_HOME = $javaHome - Write-Host -f Magenta "Detected JDK $jdkVersion in $env:JAVA_HOME (via registry)" - $foundJdk = $true + $jdkRegistryKeys = @( + "HKLM:\SOFTWARE\JavaSoft\JDK", # for JDK 10+ + "HKLM:\SOFTWARE\JavaSoft\Java Development Kit" # fallback for JDK 8 + ) + $jdkRegistryKey = $jdkRegistryKeys | Where-Object { Test-Path $_ } | Select-Object -First 1 + if ($jdkRegistryKey) { + $jdkVersion = (Get-Item $jdkRegistryKey | Get-ItemProperty -name CurrentVersion).CurrentVersion + $javaHome = (Get-Item $jdkRegistryKey\$jdkVersion | Get-ItemProperty -Name JavaHome).JavaHome + if (Test-Path "${javaHome}\bin\javac.exe") { + $env:JAVA_HOME = $javaHome + Write-Host -f Magenta "Detected JDK $jdkVersion in $env:JAVA_HOME (via registry)" + $foundJdk = $true + } } } catch { @@ -337,7 +350,7 @@ if (($All -or $BuildJava) -and -not $NoBuildJava) { } if (-not $foundJdk) { - Write-Error "Could not find the JDK. See $PSScriptRoot\docs\BuildFromSource.md for details on this requirement." + Write-Error "Could not find the JDK. Either run $PSScriptRoot\eng\scripts\InstallJdk.ps1 to install for this repo, or install the JDK globally on your machine (see $PSScriptRoot\docs\BuildFromSource.md for details)." } } diff --git a/build/SharedFx.targets b/build/SharedFx.targets index b1841de154..6aa51bf34e 100644 --- a/build/SharedFx.targets +++ b/build/SharedFx.targets @@ -12,6 +12,7 @@ + diff --git a/build/repo.targets b/build/repo.targets index e9e3a0a780..fe9c1c7a3d 100644 --- a/build/repo.targets +++ b/build/repo.targets @@ -137,7 +137,8 @@ + FrameworkOnlyPackages="@(AspNetCoreAppReference)" + SharedFrameworkTargetFramework="netcoreapp$(AspNetCoreMajorVersion).$(AspNetCoreMinorVersion)" /> diff --git a/build/sources.props b/build/sources.props index bcfbefc996..26d8be6677 100644 --- a/build/sources.props +++ b/build/sources.props @@ -27,6 +27,11 @@ $(RestoreSources); https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; + + + $(RestoreSources); + https://dotnet.myget.org/F/blazor-dev/api/v3/index.json; + https://dotnetcli.blob.core.windows.net/dotnet/ diff --git a/build/tasks/RemoveSharedFrameworkDependencies.cs b/build/tasks/RemoveSharedFrameworkDependencies.cs index 415a3c8c58..3066bb5a89 100644 --- a/build/tasks/RemoveSharedFrameworkDependencies.cs +++ b/build/tasks/RemoveSharedFrameworkDependencies.cs @@ -6,8 +6,11 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; +using System.Xml; +using System.Xml.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using NuGet.Frameworks; using NuGet.Packaging; using NuGet.Packaging.Core; @@ -22,6 +25,9 @@ namespace RepoTasks [Required] public ITaskItem[] FrameworkOnlyPackages { get; set; } + [Required] + public string SharedFrameworkTargetFramework { get; set; } + public override bool Execute() { Log.LogMessage("NuGet version = " + typeof(PackageArchiveReader).Assembly.GetName().Version); @@ -43,7 +49,7 @@ namespace RepoTasks using (var package = new ZipArchive(fileStream, ZipArchiveMode.Update)) using (var packageReader = new PackageArchiveReader(fileStream, leaveStreamOpen: true)) { - var dirty = false; + var referencesFrameworkOnlyAssembly = false; var nuspecFile = packageReader.GetNuspecFile(); using (var stream = package.OpenFile(nuspecFile)) { @@ -60,7 +66,7 @@ namespace RepoTasks { if (dependencyToRemove.Contains(dependency.Id)) { - dirty = true; + referencesFrameworkOnlyAssembly = true; Log.LogMessage($" Remove dependency on '{dependency.Id}'"); continue; } @@ -71,15 +77,27 @@ namespace RepoTasks updatedGroups.Add(updatedGroup); } - if (dirty) + if (referencesFrameworkOnlyAssembly) { packageBuilder.DependencyGroups.Clear(); packageBuilder.DependencyGroups.AddRange(updatedGroups); var updatedManifest = Manifest.Create(packageBuilder); + var inMemory = new MemoryStream(); + updatedManifest.Save(inMemory); + inMemory.Position = 0; + // Hack the raw nuspec to add the dependency + var rawNuspec = XDocument.Load(inMemory, LoadOptions.PreserveWhitespace); + var ns = rawNuspec.Root.GetDefaultNamespace(); + var metadata = rawNuspec.Root.Descendants(ns + "metadata").Single(); + metadata.Add( + new XElement(ns + "frameworkReferences", + new XElement(ns + "group", + new XAttribute("targetFramework", NuGetFramework.Parse(SharedFrameworkTargetFramework).GetFrameworkString()), + new XElement(ns + "frameworkReference", new XAttribute("name", "Microsoft.AspNetCore.App"))))); stream.Position = 0; stream.SetLength(0); - updatedManifest.Save(stream); + rawNuspec.Save(stream); } else { diff --git a/docs/BuildFromSource.md b/docs/BuildFromSource.md index 0524cd80f9..656bdf9d46 100644 --- a/docs/BuildFromSource.md +++ b/docs/BuildFromSource.md @@ -23,6 +23,10 @@ Building ASP.NET Core on Windows requires: * Java Development Kit 11 or newer. Either: * OpenJDK * Oracle's JDK + * To install a version of the JDK that will only be used by this repo, run [eng/scripts/InstallJdk.ps1](/eng/scripts/InstallJdk.ps1) + ```ps1 + PS> ./eng/scripts/InstallJdk.ps1 + ``` ### macOS/Linux @@ -77,21 +81,24 @@ Instead, we have many .sln files which include a sub-set of projects. These prin > :bulb: Pro tip: `dotnet new sln` and `dotnet sln` are one of the easiest ways to create and modify solutions. -### Known issue: NU1105 +### Common error: CS0006 -Opening solution files may produce an error code NU1105 with a message such +Opening solution files and building may produce an error code CS0006 with a message such -> Unable to find project information for 'C:\src\AspNetCore\src\Hosting\Abstractions\src\Microsoft.AspNetCore.Hosting.Abstractions.csproj'. Inside Visual Studio, this may be because the project is unloaded or not part of current solution. Otherwise the project file may be invalid or missing targets required for restore. +> Error CS0006 Metadata file 'C:\src\aspnet\AspNetCore\artifacts\bin\Microsoft.AspNetCore.Metadata\Debug\netstandard2.0\Microsoft.AspNetCore.Metadata.dll' could not be found -This is a known issue in NuGet () and we are working with them for a solution. See also to track progress on this. +The cause of this problem is that the solution you are using does not include the project that produces this .dll. This most often occurs after we have added new projects to the repo, but failed to update our .sln files to include the new project. In some cases, it is sometimes the intended behavior of the .sln which has been crafted to only include a subset of projects. -**The workaround** for now is to add all projects to the solution. You can either do this one by one using `dotnet sln` - - dotnet sln add C:\src\AspNetCore\src\Hosting\Abstractions\src\Microsoft.AspNetCore.Hosting.Abstractions.csproj - -Or you can use this script to automatically traverse the project reference graph, which then invokes `dotnet sln` for you: [eng/scripts/AddAllProjectRefsToSolution.ps1](/eng/scripts/AddAllProjectRefsToSolution.ps1). - - ./eng/scripts/AddAllProjectRefsToSolution.ps1 -WorkingDir src/Mvc/ +**You can fix this in one of two ways** +1. Build the project on command line. In most cases, running `build.cmd` on command line solve this problem. +2. Update the solution to include the missing project. You can either do this one by one using `dotnet sln` + ``` + dotnet sln add C:\src\AspNetCore\src\Hosting\Abstractions\src\Microsoft.AspNetCore.Hosting.Abstractions.csproj + ``` + Or you can use this script to automatically traverse the project reference graph, which then invokes `dotnet sln` for you: [eng/scripts/AddAllProjectRefsToSolution.ps1](/eng/scripts/AddAllProjectRefsToSolution.ps1). + ``` + ./eng/scripts/AddAllProjectRefsToSolution.ps1 -WorkingDir src/Mvc/ + ``` ## Building with Visual Studio Code diff --git a/docs/Helix.md b/docs/Helix.md index 762f89dc2d..6535bb59d4 100644 --- a/docs/Helix.md +++ b/docs/Helix.md @@ -61,6 +61,6 @@ Most tests that don't just work on helix automatically are ones that depend on t ## How to skip tests on helix There are two main ways to opt out of helix - Skipping the entire test project via `false` in csproj (the default value for this is IsTestProject). -- Skipping an individual test via `[SkipOnHelix]` which might require including a compile reference to: `` +- Skipping an individual test via `[SkipOnHelix("url to github issue")]` which might require including a compile reference to: `` Make sure to file an issue for any skipped tests and include that in a comment next to either of these diff --git a/eng/GenAPI.exclusions.txt b/eng/GenAPI.exclusions.txt index 61d8c412b2..4595fc772a 100644 --- a/eng/GenAPI.exclusions.txt +++ b/eng/GenAPI.exclusions.txt @@ -4,6 +4,8 @@ T:Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame T:Microsoft.AspNetCore.Mvc.ApplicationModels.PageParameterModel T:Microsoft.AspNetCore.Mvc.ApplicationModels.PagePropertyModel # Manually implemented - https://github.com/aspnet/AspNetCore/issues/8825 +T:Microsoft.AspNetCore.Components.AuthorizeView +T:Microsoft.AspNetCore.Components.CascadingAuthenticationState T:Microsoft.AspNetCore.Components.CascadingValue`1 T:Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator T:Microsoft.AspNetCore.Components.Forms.EditForm diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 067ba79788..fa3ee5688b 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -35,6 +35,7 @@ + @@ -48,6 +49,7 @@ + @@ -61,6 +63,7 @@ + @@ -82,6 +85,7 @@ + diff --git a/eng/SharedFramework.External.props b/eng/SharedFramework.External.props index b8cf631738..d735b0764f 100644 --- a/eng/SharedFramework.External.props +++ b/eng/SharedFramework.External.props @@ -56,9 +56,6 @@ - - - - + https://github.com/aspnet/AspNetCore-Tooling - 0cbfa9bc5f1096e8bd1d3b0e9101752d14ed63f2 + 62e33dac1d5ec88ab15d7af694c1adf29ffc4c59 - + https://github.com/aspnet/AspNetCore-Tooling - 0cbfa9bc5f1096e8bd1d3b0e9101752d14ed63f2 + 62e33dac1d5ec88ab15d7af694c1adf29ffc4c59 - + https://github.com/aspnet/AspNetCore-Tooling - 0cbfa9bc5f1096e8bd1d3b0e9101752d14ed63f2 + 62e33dac1d5ec88ab15d7af694c1adf29ffc4c59 - + https://github.com/aspnet/AspNetCore-Tooling - 0cbfa9bc5f1096e8bd1d3b0e9101752d14ed63f2 + 62e33dac1d5ec88ab15d7af694c1adf29ffc4c59 https://github.com/aspnet/EntityFrameworkCore @@ -384,13 +384,13 @@ https://github.com/aspnet/Extensions 8dfb4ece7ca9a6dea14264dafc38a0c953874559 - + https://github.com/dotnet/arcade - 30682cda0dd7ca1765463749dd91ec3cfec75eb9 + e913fb3b02d4089a91ff91c041c5f6e7c29038b0 - + https://github.com/dotnet/arcade - 30682cda0dd7ca1765463749dd91ec3cfec75eb9 + e913fb3b02d4089a91ff91c041c5f6e7c29038b0 https://github.com/aspnet/Extensions diff --git a/eng/Versions.props b/eng/Versions.props index c815033082..10a317d441 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -17,7 +17,7 @@ --> - 1.0.0-beta.19262.1 + 1.0.0-beta.19270.1 3.0.0-preview6-27714-15 3.0.0-preview6-27714-15 @@ -114,10 +114,10 @@ 3.0.0-preview6.19252.4 3.0.0-preview6.19252.4 - 3.0.0-preview6.19265.1 - 3.0.0-preview6.19265.1 - 3.0.0-preview6.19265.1 - 3.0.0-preview6.19265.1 + 3.0.0-preview6.19270.2 + 3.0.0-preview6.19270.2 + 3.0.0-preview6.19270.2 + 3.0.0-preview6.19270.2 - 0.10.0-preview-20190325.1 + 0.10.0-preview-20190523.1 2.1.1 2.2.0 diff --git a/eng/common/CIBuild.cmd b/eng/common/CIBuild.cmd new file mode 100644 index 0000000000..56c2f25ac2 --- /dev/null +++ b/eng/common/CIBuild.cmd @@ -0,0 +1,2 @@ +@echo off +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0Build.ps1""" -restore -build -test -sign -pack -publish -ci %*" \ No newline at end of file diff --git a/eng/common/CheckSymbols.ps1 b/eng/common/CheckSymbols.ps1 new file mode 100644 index 0000000000..b8d84607b8 --- /dev/null +++ b/eng/common/CheckSymbols.ps1 @@ -0,0 +1,158 @@ +param( + [Parameter(Mandatory=$true)][string] $InputPath, # Full path to directory where NuGet packages to be checked are stored + [Parameter(Mandatory=$true)][string] $ExtractPath, # Full path to directory where the packages will be extracted during validation + [Parameter(Mandatory=$true)][string] $SymbolToolPath # Full path to directory where dotnet symbol-tool was installed +) + +Add-Type -AssemblyName System.IO.Compression.FileSystem + +function FirstMatchingSymbolDescriptionOrDefault { + param( + [string] $FullPath, # Full path to the module that has to be checked + [string] $TargetServerParam, # Parameter to pass to `Symbol Tool` indicating the server to lookup for symbols + [string] $SymbolsPath + ) + + $FileName = [System.IO.Path]::GetFileName($FullPath) + $Extension = [System.IO.Path]::GetExtension($FullPath) + + # Those below are potential symbol files that the `dotnet symbol` might + # return. Which one will be returned depend on the type of file we are + # checking and which type of file was uploaded. + + # The file itself is returned + $SymbolPath = $SymbolsPath + "\" + $FileName + + # PDB file for the module + $PdbPath = $SymbolPath.Replace($Extension, ".pdb") + + # PDB file for R2R module (created by crossgen) + $NGenPdb = $SymbolPath.Replace($Extension, ".ni.pdb") + + # DBG file for a .so library + $SODbg = $SymbolPath.Replace($Extension, ".so.dbg") + + # DWARF file for a .dylib + $DylibDwarf = $SymbolPath.Replace($Extension, ".dylib.dwarf") + + .\dotnet-symbol.exe --symbols --modules --windows-pdbs $TargetServerParam $FullPath -o $SymbolsPath | Out-Null + + if (Test-Path $PdbPath) { + return "PDB" + } + elseif (Test-Path $NGenPdb) { + return "NGen PDB" + } + elseif (Test-Path $SODbg) { + return "DBG for SO" + } + elseif (Test-Path $DylibDwarf) { + return "Dwarf for Dylib" + } + elseif (Test-Path $SymbolPath) { + return "Module" + } + else { + return $null + } +} + +function CountMissingSymbols { + param( + [string] $PackagePath # Path to a NuGet package + ) + + # Ensure input file exist + if (!(Test-Path $PackagePath)) { + throw "Input file does not exist: $PackagePath" + } + + # Extensions for which we'll look for symbols + $RelevantExtensions = @(".dll", ".exe", ".so", ".dylib") + + # How many files are missing symbol information + $MissingSymbols = 0 + + $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) + $PackageGuid = New-Guid + $ExtractPath = Join-Path -Path $ExtractPath -ChildPath $PackageGuid + $SymbolsPath = Join-Path -Path $ExtractPath -ChildPath "Symbols" + + [System.IO.Compression.ZipFile]::ExtractToDirectory($PackagePath, $ExtractPath) + + # Makes easier to reference `symbol tool` + Push-Location $SymbolToolPath + + Get-ChildItem -Recurse $ExtractPath | + Where-Object {$RelevantExtensions -contains $_.Extension} | + ForEach-Object { + if ($_.FullName -Match "\\ref\\") { + Write-Host "`t Ignoring reference assembly file" $_.FullName + return + } + + $SymbolsOnMSDL = FirstMatchingSymbolDescriptionOrDefault $_.FullName "--microsoft-symbol-server" $SymbolsPath + $SymbolsOnSymWeb = FirstMatchingSymbolDescriptionOrDefault $_.FullName "--internal-server" $SymbolsPath + + Write-Host -NoNewLine "`t Checking file" $_.FullName "... " + + if ($SymbolsOnMSDL -ne $null -and $SymbolsOnSymWeb -ne $null) { + Write-Host "Symbols found on MSDL (" $SymbolsOnMSDL ") and SymWeb (" $SymbolsOnSymWeb ")" + } + else { + $MissingSymbols++ + + if ($SymbolsOnMSDL -eq $null -and $SymbolsOnSymWeb -eq $null) { + Write-Host "No symbols found on MSDL or SymWeb!" + } + else { + if ($SymbolsOnMSDL -eq $null) { + Write-Host "No symbols found on MSDL!" + } + else { + Write-Host "No symbols found on SymWeb!" + } + } + } + } + + Pop-Location + + return $MissingSymbols +} + +function CheckSymbolsAvailable { + if (Test-Path $ExtractPath) { + Remove-Item $ExtractPath -Force -Recurse -ErrorAction SilentlyContinue + } + + Get-ChildItem "$InputPath\*.nupkg" | + ForEach-Object { + $FileName = $_.Name + + # These packages from Arcade-Services include some native libraries that + # our current symbol uploader can't handle. Below is a workaround until + # we get issue: https://github.com/dotnet/arcade/issues/2457 sorted. + if ($FileName -Match "Microsoft\.DotNet\.Darc\.") { + Write-Host "Ignoring Arcade-services file: $FileName" + Write-Host + return + } + elseif ($FileName -Match "Microsoft\.DotNet\.Maestro\.Tasks\.") { + Write-Host "Ignoring Arcade-services file: $FileName" + Write-Host + return + } + + Write-Host "Validating $FileName " + $Status = CountMissingSymbols "$InputPath\$FileName" + + if ($Status -ne 0) { + Write-Error "Missing symbols for $Status modules in the package $FileName" + } + + Write-Host + } +} + +CheckSymbolsAvailable diff --git a/eng/common/PublishToPackageFeed.proj b/eng/common/PublishToPackageFeed.proj new file mode 100644 index 0000000000..e17f72644e --- /dev/null +++ b/eng/common/PublishToPackageFeed.proj @@ -0,0 +1,81 @@ + + + + + + netcoreapp2.1 + + + + + + + + + + + + + + + + + + + + + + + + + https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json + https://dotnetfeed.blob.core.windows.net/arcade-validation/index.json + https://dotnetfeed.blob.core.windows.net/aspnet-aspnetcore/index.json + https://dotnetfeed.blob.core.windows.net/aspnet-aspnetcore-tooling/index.json + https://dotnetfeed.blob.core.windows.net/aspnet-entityframeworkcore/index.json + https://dotnetfeed.blob.core.windows.net/aspnet-extensions/index.json + https://dotnetfeed.blob.core.windows.net/dotnet-coreclr/index.json + https://dotnetfeed.blob.core.windows.net/dotnet-sdk/index.json + https://dotnetfeed.blob.core.windows.net/dotnet-tools-internal/index.json + https://dotnetfeed.blob.core.windows.net/dotnet-toolset/index.json + https://dotnetfeed.blob.core.windows.net/dotnet-windowsdesktop/index.json + https://dotnetfeed.blob.core.windows.net/nuget-nugetclient/index.json + + + + + + + + + + + + diff --git a/eng/common/PublishToSymbolServers.proj b/eng/common/PublishToSymbolServers.proj new file mode 100644 index 0000000000..5d55e312b0 --- /dev/null +++ b/eng/common/PublishToSymbolServers.proj @@ -0,0 +1,82 @@ + + + + + + netcoreapp2.1 + + + + + + + + + + + + + + + + 3650 + true + false + + + + + + + + + + + + + + + + + diff --git a/eng/common/README.md b/eng/common/README.md new file mode 100644 index 0000000000..ff49c37152 --- /dev/null +++ b/eng/common/README.md @@ -0,0 +1,28 @@ +# Don't touch this folder + + uuuuuuuuuuuuuuuuuuuu + u" uuuuuuuuuuuuuuuuuu "u + u" u$$$$$$$$$$$$$$$$$$$$u "u + u" u$$$$$$$$$$$$$$$$$$$$$$$$u "u + u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u + u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u + u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u + $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ + $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ + $ $$$" ... "$... ...$" ... "$$$ ... "$$$ $ + $ $$$u `"$$$$$$$ $$$ $$$$$ $$ $$$ $$$ $ + $ $$$$$$uu "$$$$ $$$ $$$$$ $$ """ u$$$ $ + $ $$$""$$$ $$$$ $$$u "$$$" u$$ $$$$$$$$ $ + $ $$$$....,$$$$$..$$$$$....,$$$$..$$$$$$$$ $ + $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ + "u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u" + "u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u" + "u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u" + "u "$$$$$$$$$$$$$$$$$$$$$$$$" u" + "u "$$$$$$$$$$$$$$$$$$$$" u" + "u """""""""""""""""" u" + """""""""""""""""""" + +!!! Changes made in this directory are subject to being overwritten by automation !!! + +The files in this directory are shared by all Arcade repos and managed by automation. If you need to make changes to these files, open an issue or submit a pull request to https://github.com/dotnet/arcade first. diff --git a/eng/common/SigningValidation.proj b/eng/common/SigningValidation.proj new file mode 100644 index 0000000000..7045fb6fb9 --- /dev/null +++ b/eng/common/SigningValidation.proj @@ -0,0 +1,83 @@ + + + + + + netcoreapp2.1 + + + + + + + + $(NuGetPackageRoot)Microsoft.DotNet.SignCheck\$(SignCheckVersion)\tools\Microsoft.DotNet.SignCheck.exe + + $(PackageBasePath) + signcheck.log + signcheck.errors.log + signcheck.exclusions.txt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/common/SourceLinkValidation.ps1 b/eng/common/SourceLinkValidation.ps1 new file mode 100644 index 0000000000..cb2d28cb99 --- /dev/null +++ b/eng/common/SourceLinkValidation.ps1 @@ -0,0 +1,184 @@ +param( + [Parameter(Mandatory=$true)][string] $InputPath, # Full path to directory where Symbols.NuGet packages to be checked are stored + [Parameter(Mandatory=$true)][string] $ExtractPath, # Full path to directory where the packages will be extracted during validation + [Parameter(Mandatory=$true)][string] $SourceLinkToolPath, # Full path to directory where dotnet SourceLink CLI was installed + [Parameter(Mandatory=$true)][string] $GHRepoName, # GitHub name of the repo including the Org. E.g., dotnet/arcade + [Parameter(Mandatory=$true)][string] $GHCommit # GitHub commit SHA used to build the packages +) + +# Cache/HashMap (File -> Exist flag) used to consult whether a file exist +# in the repository at a specific commit point. This is populated by inserting +# all files present in the repo at a specific commit point. +$global:RepoFiles = @{} + +$ValidatePackage = { + param( + [string] $PackagePath # Full path to a Symbols.NuGet package + ) + + # Ensure input file exist + if (!(Test-Path $PackagePath)) { + throw "Input file does not exist: $PackagePath" + } + + # Extensions for which we'll look for SourceLink information + # For now we'll only care about Portable & Embedded PDBs + $RelevantExtensions = @(".dll", ".exe", ".pdb") + + Write-Host -NoNewLine "Validating" ([System.IO.Path]::GetFileName($PackagePath)) "... " + + $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) + $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageId + $FailedFiles = 0 + + Add-Type -AssemblyName System.IO.Compression.FileSystem + + [System.IO.Directory]::CreateDirectory($ExtractPath); + + $zip = [System.IO.Compression.ZipFile]::OpenRead($PackagePath) + + $zip.Entries | + Where-Object {$RelevantExtensions -contains [System.IO.Path]::GetExtension($_.Name)} | + ForEach-Object { + $FileName = $_.FullName + $Extension = [System.IO.Path]::GetExtension($_.Name) + $FakeName = -Join((New-Guid), $Extension) + $TargetFile = Join-Path -Path $ExtractPath -ChildPath $FakeName + + # We ignore resource DLLs + if ($FileName.EndsWith(".resources.dll")) { + return + } + + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $TargetFile, $true) + + $ValidateFile = { + param( + [string] $FullPath, # Full path to the module that has to be checked + [string] $RealPath, + [ref] $FailedFiles + ) + + # Makes easier to reference `sourcelink cli` + Push-Location $using:SourceLinkToolPath + + $SourceLinkInfos = .\sourcelink.exe print-urls $FullPath | Out-String + + if ($LASTEXITCODE -eq 0 -and -not ([string]::IsNullOrEmpty($SourceLinkInfos))) { + $NumFailedLinks = 0 + + # We only care about Http addresses + $Matches = (Select-String '(http[s]?)(:\/\/)([^\s,]+)' -Input $SourceLinkInfos -AllMatches).Matches + + if ($Matches.Count -ne 0) { + $Matches.Value | + ForEach-Object { + $Link = $_ + $CommitUrl = -Join("https://raw.githubusercontent.com/", $using:GHRepoName, "/", $using:GHCommit, "/") + $FilePath = $Link.Replace($CommitUrl, "") + $Status = 200 + $Cache = $using:RepoFiles + + if ( !($Cache.ContainsKey($FilePath)) ) { + try { + $Uri = $Link -as [System.URI] + + # Only GitHub links are valid + if ($Uri.AbsoluteURI -ne $null -and $Uri.Host -match "github") { + $Status = (Invoke-WebRequest -Uri $Link -UseBasicParsing -Method HEAD -TimeoutSec 5).StatusCode + } + else { + $Status = 0 + } + } + catch { + $Status = 0 + } + } + + if ($Status -ne 200) { + if ($NumFailedLinks -eq 0) { + if ($FailedFiles.Value -eq 0) { + Write-Host + } + + Write-Host "`tFile $RealPath has broken links:" + } + + Write-Host "`t`tFailed to retrieve $Link" + + $NumFailedLinks++ + } + } + } + + if ($NumFailedLinks -ne 0) { + $FailedFiles.value++ + $global:LASTEXITCODE = 1 + } + } + + Pop-Location + } + + &$ValidateFile $TargetFile $FileName ([ref]$FailedFiles) + } + + $zip.Dispose() + + if ($FailedFiles -eq 0) { + Write-Host "Passed." + } +} + +function ValidateSourceLinkLinks { + if (!($GHRepoName -Match "^[^\s\/]+/[^\s\/]+$")) { + Write-Host "GHRepoName should be in the format /" + $global:LASTEXITCODE = 1 + return + } + + if (!($GHCommit -Match "^[0-9a-fA-F]{40}$")) { + Write-Host "GHCommit should be a 40 chars hexadecimal string" + $global:LASTEXITCODE = 1 + return + } + + $RepoTreeURL = -Join("https://api.github.com/repos/", $GHRepoName, "/git/trees/", $GHCommit, "?recursive=1") + $CodeExtensions = @(".cs", ".vb", ".fs", ".fsi", ".fsx", ".fsscript") + + try { + # Retrieve the list of files in the repo at that particular commit point and store them in the RepoFiles hash + $Data = Invoke-WebRequest $RepoTreeURL | ConvertFrom-Json | Select-Object -ExpandProperty tree + + foreach ($file in $Data) { + $Extension = [System.IO.Path]::GetExtension($file.path) + + if ($CodeExtensions.Contains($Extension)) { + $RepoFiles[$file.path] = 1 + } + } + } + catch { + Write-Host "Problems downloading the list of files from the repo. Url used: $RepoTreeURL" + $global:LASTEXITCODE = 1 + return + } + + if (Test-Path $ExtractPath) { + Remove-Item $ExtractPath -Force -Recurse -ErrorAction SilentlyContinue + } + + # Process each NuGet package in parallel + $Jobs = @() + Get-ChildItem "$InputPath\*.symbols.nupkg" | + ForEach-Object { + $Jobs += Start-Job -ScriptBlock $ValidatePackage -ArgumentList $_.FullName + } + + foreach ($Job in $Jobs) { + Wait-Job -Id $Job.Id | Receive-Job + } +} + +Measure-Command { ValidateSourceLinkLinks } diff --git a/eng/common/build.ps1 b/eng/common/build.ps1 new file mode 100644 index 0000000000..d7e3799ebd --- /dev/null +++ b/eng/common/build.ps1 @@ -0,0 +1,138 @@ +[CmdletBinding(PositionalBinding=$false)] +Param( + [string][Alias('c')]$configuration = "Debug", + [string] $projects, + [string][Alias('v')]$verbosity = "minimal", + [string] $msbuildEngine = $null, + [bool] $warnAsError = $true, + [bool] $nodeReuse = $true, + [switch][Alias('r')]$restore, + [switch] $deployDeps, + [switch][Alias('b')]$build, + [switch] $rebuild, + [switch] $deploy, + [switch][Alias('t')]$test, + [switch] $integrationTest, + [switch] $performanceTest, + [switch] $sign, + [switch] $pack, + [switch] $publish, + [switch][Alias('bl')]$binaryLog, + [switch] $ci, + [switch] $prepareMachine, + [switch] $help, + [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties +) + +. $PSScriptRoot\tools.ps1 + +function Print-Usage() { + Write-Host "Common settings:" + Write-Host " -configuration Build configuration: 'Debug' or 'Release' (short: -c)" + Write-Host " -verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic] (short: -v)" + Write-Host " -binaryLog Output binary log (short: -bl)" + Write-Host " -help Print help and exit" + Write-Host "" + + Write-Host "Actions:" + Write-Host " -restore Restore dependencies (short: -r)" + Write-Host " -build Build solution (short: -b)" + Write-Host " -rebuild Rebuild solution" + Write-Host " -deploy Deploy built VSIXes" + Write-Host " -deployDeps Deploy dependencies (e.g. VSIXes for integration tests)" + Write-Host " -test Run all unit tests in the solution (short: -t)" + Write-Host " -integrationTest Run all integration tests in the solution" + Write-Host " -performanceTest Run all performance tests in the solution" + Write-Host " -pack Package build outputs into NuGet packages and Willow components" + Write-Host " -sign Sign build outputs" + Write-Host " -publish Publish artifacts (e.g. symbols)" + Write-Host "" + + Write-Host "Advanced settings:" + Write-Host " -projects Semi-colon delimited list of sln/proj's to build. Globbing is supported (*.sln)" + Write-Host " -ci Set when running on CI server" + Write-Host " -prepareMachine Prepare machine for CI run, clean up processes after build" + Write-Host " -warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" + Write-Host " -msbuildEngine Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)." + Write-Host "" + + Write-Host "Command line arguments not listed above are passed thru to msbuild." + Write-Host "The above arguments can be shortened as much as to be unambiguous (e.g. -co for configuration, -t for test, etc.)." +} + +function InitializeCustomToolset { + if (-not $restore) { + return + } + + $script = Join-Path $EngRoot "restore-toolset.ps1" + + if (Test-Path $script) { + . $script + } +} + +function Build { + $toolsetBuildProj = InitializeToolset + InitializeCustomToolset + + $bl = if ($binaryLog) { "/bl:" + (Join-Path $LogDir "Build.binlog") } else { "" } + + if ($projects) { + # Re-assign properties to a new variable because PowerShell doesn't let us append properties directly for unclear reasons. + # Explicitly set the type as string[] because otherwise PowerShell would make this char[] if $properties is empty. + [string[]] $msbuildArgs = $properties + $msbuildArgs += "/p:Projects=$projects" + $properties = $msbuildArgs + } + + MSBuild $toolsetBuildProj ` + $bl ` + /p:Configuration=$configuration ` + /p:RepoRoot=$RepoRoot ` + /p:Restore=$restore ` + /p:DeployDeps=$deployDeps ` + /p:Build=$build ` + /p:Rebuild=$rebuild ` + /p:Deploy=$deploy ` + /p:Test=$test ` + /p:Pack=$pack ` + /p:IntegrationTest=$integrationTest ` + /p:PerformanceTest=$performanceTest ` + /p:Sign=$sign ` + /p:Publish=$publish ` + @properties +} + +try { + if ($help -or (($null -ne $properties) -and ($properties.Contains("/help") -or $properties.Contains("/?")))) { + Print-Usage + exit 0 + } + + if ($ci) { + $binaryLog = $true + $nodeReuse = $false + } + + # Import custom tools configuration, if present in the repo. + # Note: Import in global scope so that the script set top-level variables without qualification. + $configureToolsetScript = Join-Path $EngRoot "configure-toolset.ps1" + if (Test-Path $configureToolsetScript) { + . $configureToolsetScript + } + + if (($restore) -and ($null -eq $env:DisableNativeToolsetInstalls)) { + InitializeNativeTools + } + + Build +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} + +ExitWithExitCode 0 diff --git a/eng/common/build.sh b/eng/common/build.sh new file mode 100755 index 0000000000..ce846d888d --- /dev/null +++ b/eng/common/build.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env bash + +# Stop script if unbound variable found (use ${var:-} if intentional) +set -u + +# Stop script if command returns non-zero exit code. +# Prevents hidden errors caused by missing error code propagation. +set -e + +usage() +{ + echo "Common settings:" + echo " --configuration Build configuration: 'Debug' or 'Release' (short: -c)" + echo " --verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic] (short: -v)" + echo " --binaryLog Create MSBuild binary log (short: -bl)" + echo " --help Print help and exit (short: -h)" + echo "" + + echo "Actions:" + echo " --restore Restore dependencies (short: -r)" + echo " --build Build solution (short: -b)" + echo " --rebuild Rebuild solution" + echo " --test Run all unit tests in the solution (short: -t)" + echo " --integrationTest Run all integration tests in the solution" + echo " --performanceTest Run all performance tests in the solution" + echo " --pack Package build outputs into NuGet packages and Willow components" + echo " --sign Sign build outputs" + echo " --publish Publish artifacts (e.g. symbols)" + echo "" + + echo "Advanced settings:" + echo " --projects Project or solution file(s) to build" + echo " --ci Set when running on CI server" + echo " --prepareMachine Prepare machine for CI run, clean up processes after build" + echo " --nodeReuse Sets nodereuse msbuild parameter ('true' or 'false')" + echo " --warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" + echo "" + echo "Command line arguments not listed above are passed thru to msbuild." + echo "Arguments can also be passed in with a single hyphen." +} + +source="${BASH_SOURCE[0]}" + +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +restore=false +build=false +rebuild=false +test=false +integration_test=false +performance_test=false +pack=false +publish=false +sign=false +public=false +ci=false + +warn_as_error=true +node_reuse=true +binary_log=false + +projects='' +configuration='Debug' +prepare_machine=false +verbosity='minimal' + +properties='' + +while [[ $# > 0 ]]; do + opt="$(echo "${1/#--/-}" | awk '{print tolower($0)}')" + case "$opt" in + -help|-h) + usage + exit 0 + ;; + -configuration|-c) + configuration=$2 + shift + ;; + -verbosity|-v) + verbosity=$2 + shift + ;; + -binarylog|-bl) + binary_log=true + ;; + -restore|-r) + restore=true + ;; + -build|-b) + build=true + ;; + -rebuild) + rebuild=true + ;; + -pack) + pack=true + ;; + -test|-t) + test=true + ;; + -integrationtest) + integration_test=true + ;; + -performancetest) + performance_test=true + ;; + -sign) + sign=true + ;; + -publish) + publish=true + ;; + -preparemachine) + prepare_machine=true + ;; + -projects) + projects=$2 + shift + ;; + -ci) + ci=true + ;; + -warnaserror) + warn_as_error=$2 + shift + ;; + -nodereuse) + node_reuse=$2 + shift + ;; + *) + properties="$properties $1" + ;; + esac + + shift +done + +if [[ "$ci" == true ]]; then + binary_log=true + node_reuse=false +fi + +. "$scriptroot/tools.sh" + +function InitializeCustomToolset { + local script="$eng_root/restore-toolset.sh" + + if [[ -a "$script" ]]; then + . "$script" + fi +} + +function Build { + InitializeToolset + InitializeCustomToolset + + if [[ ! -z "$projects" ]]; then + properties="$properties /p:Projects=$projects" + fi + + local bl="" + if [[ "$binary_log" == true ]]; then + bl="/bl:\"$log_dir/Build.binlog\"" + fi + + MSBuild $_InitializeToolset \ + $bl \ + /p:Configuration=$configuration \ + /p:RepoRoot="$repo_root" \ + /p:Restore=$restore \ + /p:Build=$build \ + /p:Rebuild=$rebuild \ + /p:Test=$test \ + /p:Pack=$pack \ + /p:IntegrationTest=$integration_test \ + /p:PerformanceTest=$performance_test \ + /p:Sign=$sign \ + /p:Publish=$publish \ + $properties + + ExitWithExitCode 0 +} + +# Import custom tools configuration, if present in the repo. +configure_toolset_script="$eng_root/configure-toolset.sh" +if [[ -a "$configure_toolset_script" ]]; then + . "$configure_toolset_script" +fi + +# TODO: https://github.com/dotnet/arcade/issues/1468 +# Temporary workaround to avoid breaking change. +# Remove once repos are updated. +if [[ -n "${useInstalledDotNetCli:-}" ]]; then + use_installed_dotnet_cli="$useInstalledDotNetCli" +fi + +if [[ "$restore" == true && -z ${DisableNativeToolsetInstalls:-} ]]; then + InitializeNativeTools +fi + +Build diff --git a/eng/common/cibuild.sh b/eng/common/cibuild.sh new file mode 100755 index 0000000000..1a02c0dec8 --- /dev/null +++ b/eng/common/cibuild.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" + +# resolve $SOURCE until the file is no longer a symlink +while [[ -h $source ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + + # if $source was a relative symlink, we need to resolve it relative to the path where + # the symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. "$scriptroot/build.sh" --restore --build --test --pack --publish --ci $@ \ No newline at end of file diff --git a/eng/common/cross/android/arm/toolchain.cmake b/eng/common/cross/android/arm/toolchain.cmake new file mode 100644 index 0000000000..a7e1c73501 --- /dev/null +++ b/eng/common/cross/android/arm/toolchain.cmake @@ -0,0 +1,41 @@ +set(CROSS_NDK_TOOLCHAIN $ENV{ROOTFS_DIR}/../) +set(CROSS_ROOTFS ${CROSS_NDK_TOOLCHAIN}/sysroot) +set(CLR_CMAKE_PLATFORM_ANDROID "Android") + +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_VERSION 1) +set(CMAKE_SYSTEM_PROCESSOR arm) + +## Specify the toolchain +set(TOOLCHAIN "arm-linux-androideabi") +set(CMAKE_PREFIX_PATH ${CROSS_NDK_TOOLCHAIN}) +set(TOOLCHAIN_PREFIX ${TOOLCHAIN}-) + +find_program(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}clang) +find_program(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}clang++) +find_program(CMAKE_ASM_COMPILER ${TOOLCHAIN_PREFIX}clang) +find_program(CMAKE_AR ${TOOLCHAIN_PREFIX}ar) +find_program(CMAKE_LD ${TOOLCHAIN_PREFIX}ar) +find_program(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy) +find_program(CMAKE_OBJDUMP ${TOOLCHAIN_PREFIX}objdump) + +add_compile_options(--sysroot=${CROSS_ROOTFS}) +add_compile_options(-fPIE) +add_compile_options(-mfloat-abi=soft) +include_directories(SYSTEM ${CROSS_NDK_TOOLCHAIN}/include/c++/4.9.x/) +include_directories(SYSTEM ${CROSS_NDK_TOOLCHAIN}/include/c++/4.9.x/arm-linux-androideabi/) + +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} -B ${CROSS_ROOTFS}/usr/lib/gcc/${TOOLCHAIN}") +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} -L${CROSS_ROOTFS}/lib/${TOOLCHAIN}") +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} --sysroot=${CROSS_ROOTFS}") +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} -fPIE -pie") + +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${CROSS_LINK_FLAGS}" CACHE STRING "" FORCE) +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${CROSS_LINK_FLAGS}" CACHE STRING "" FORCE) +set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${CROSS_LINK_FLAGS}" CACHE STRING "" FORCE) + +set(CMAKE_FIND_ROOT_PATH "${CROSS_ROOTFS}") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/eng/common/cross/android/arm64/toolchain.cmake b/eng/common/cross/android/arm64/toolchain.cmake new file mode 100644 index 0000000000..29415899c1 --- /dev/null +++ b/eng/common/cross/android/arm64/toolchain.cmake @@ -0,0 +1,42 @@ +set(CROSS_NDK_TOOLCHAIN $ENV{ROOTFS_DIR}/../) +set(CROSS_ROOTFS ${CROSS_NDK_TOOLCHAIN}/sysroot) +set(CLR_CMAKE_PLATFORM_ANDROID "Android") + +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_VERSION 1) +set(CMAKE_SYSTEM_PROCESSOR aarch64) + +## Specify the toolchain +set(TOOLCHAIN "aarch64-linux-android") +set(CMAKE_PREFIX_PATH ${CROSS_NDK_TOOLCHAIN}) +set(TOOLCHAIN_PREFIX ${TOOLCHAIN}-) + +find_program(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}clang) +find_program(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}clang++) +find_program(CMAKE_ASM_COMPILER ${TOOLCHAIN_PREFIX}clang) +find_program(CMAKE_AR ${TOOLCHAIN_PREFIX}ar) +find_program(CMAKE_LD ${TOOLCHAIN_PREFIX}ar) +find_program(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy) +find_program(CMAKE_OBJDUMP ${TOOLCHAIN_PREFIX}objdump) + +add_compile_options(--sysroot=${CROSS_ROOTFS}) +add_compile_options(-fPIE) + +## Needed for Android or bionic specific conditionals +add_compile_options(-D__ANDROID__) +add_compile_options(-D__BIONIC__) + +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} -B ${CROSS_ROOTFS}/usr/lib/gcc/${TOOLCHAIN}") +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} -L${CROSS_ROOTFS}/lib/${TOOLCHAIN}") +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} --sysroot=${CROSS_ROOTFS}") +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} -fPIE -pie") + +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${CROSS_LINK_FLAGS}" CACHE STRING "" FORCE) +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${CROSS_LINK_FLAGS}" CACHE STRING "" FORCE) +set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${CROSS_LINK_FLAGS}" CACHE STRING "" FORCE) + +set(CMAKE_FIND_ROOT_PATH "${CROSS_ROOTFS}") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/eng/common/cross/arm/sources.list.bionic b/eng/common/cross/arm/sources.list.bionic new file mode 100644 index 0000000000..2109557409 --- /dev/null +++ b/eng/common/cross/arm/sources.list.bionic @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse diff --git a/eng/common/cross/arm/sources.list.jessie b/eng/common/cross/arm/sources.list.jessie new file mode 100644 index 0000000000..4d142ac9b1 --- /dev/null +++ b/eng/common/cross/arm/sources.list.jessie @@ -0,0 +1,3 @@ +# Debian (sid) # UNSTABLE +deb http://ftp.debian.org/debian/ sid main contrib non-free +deb-src http://ftp.debian.org/debian/ sid main contrib non-free diff --git a/eng/common/cross/arm/sources.list.trusty b/eng/common/cross/arm/sources.list.trusty new file mode 100644 index 0000000000..07d8f88d82 --- /dev/null +++ b/eng/common/cross/arm/sources.list.trusty @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ trusty main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ trusty-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ trusty-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ trusty-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty-security main restricted universe multiverse \ No newline at end of file diff --git a/eng/common/cross/arm/sources.list.xenial b/eng/common/cross/arm/sources.list.xenial new file mode 100644 index 0000000000..eacd86b7df --- /dev/null +++ b/eng/common/cross/arm/sources.list.xenial @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted universe multiverse \ No newline at end of file diff --git a/eng/common/cross/arm/sources.list.zesty b/eng/common/cross/arm/sources.list.zesty new file mode 100644 index 0000000000..ea2c14a787 --- /dev/null +++ b/eng/common/cross/arm/sources.list.zesty @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ zesty main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-security main restricted universe multiverse diff --git a/eng/common/cross/arm/trusty-lttng-2.4.patch b/eng/common/cross/arm/trusty-lttng-2.4.patch new file mode 100644 index 0000000000..8e4dd7ae71 --- /dev/null +++ b/eng/common/cross/arm/trusty-lttng-2.4.patch @@ -0,0 +1,71 @@ +From e72c9d7ead60e3317bd6d1fade995c07021c947b Mon Sep 17 00:00:00 2001 +From: Mathieu Desnoyers +Date: Thu, 7 May 2015 13:25:04 -0400 +Subject: [PATCH] Fix: building probe providers with C++ compiler + +Robert Daniels wrote: +> > I'm attempting to use lttng userspace tracing with a C++ application +> > on an ARM platform. I'm using GCC 4.8.4 on Linux 3.14 with the 2.6 +> > release of lttng. I've compiled lttng-modules, lttng-ust, and +> > lttng-tools and have been able to get a simple test working with C +> > code. When I attempt to run the hello.cxx test on my target it will +> > segfault. +> +> +> I spent a little time digging into this issue and finally discovered the +> cause of my segfault with ARM C++ tracepoints. +> +> There is a struct called 'lttng_event' in ust-events.h which contains an +> empty union 'u'. This was the cause of my issue. Under C, this empty union +> compiles to a zero byte member while under C++ it compiles to a one byte +> member, and in my case was four-byte aligned which caused my C++ code to +> have the 'cds_list_head node' offset incorrectly by four bytes. This lead +> to an incorrect linked list structure which caused my issue. +> +> Since this union is empty, I simply removed it from the struct and everything +> worked correctly. +> +> I don't know the history or purpose behind this empty union so I'd like to +> know if this is a safe fix. If it is I can submit a patch with the union +> removed. + +That's a very nice catch! + +We do not support building tracepoint probe provider with +g++ yet, as stated in lttng-ust(3): + +"- Note for C++ support: although an application instrumented with + tracepoints can be compiled with g++, tracepoint probes should be + compiled with gcc (only tested with gcc so far)." + +However, if it works fine with this fix, then I'm tempted to take it, +especially because removing the empty union does not appear to affect +the layout of struct lttng_event as seen from liblttng-ust, which must +be compiled with a C compiler, and from probe providers compiled with +a C compiler. So all we are changing is the layout of a probe provider +compiled with a C++ compiler, which is anyway buggy at the moment, +because it is not compatible with the layout expected by liblttng-ust +compiled with a C compiler. + +Reported-by: Robert Daniels +Signed-off-by: Mathieu Desnoyers +--- + include/lttng/ust-events.h | 2 -- + 1 file changed, 2 deletions(-) + +diff --git a/usr/include/lttng/ust-events.h b/usr/include/lttng/ust-events.h +index 328a875..3d7a274 100644 +--- a/usr/include/lttng/ust-events.h ++++ b/usr/include/lttng/ust-events.h +@@ -407,8 +407,6 @@ struct lttng_event { + void *_deprecated1; + struct lttng_ctx *ctx; + enum lttng_ust_instrumentation instrumentation; +- union { +- } u; + struct cds_list_head node; /* Event list in session */ + struct cds_list_head _deprecated2; + void *_deprecated3; +-- +2.7.4 + diff --git a/eng/common/cross/arm/trusty.patch b/eng/common/cross/arm/trusty.patch new file mode 100644 index 0000000000..2f2972f8eb --- /dev/null +++ b/eng/common/cross/arm/trusty.patch @@ -0,0 +1,97 @@ +diff -u -r a/usr/include/urcu/uatomic/generic.h b/usr/include/urcu/uatomic/generic.h +--- a/usr/include/urcu/uatomic/generic.h 2014-03-28 06:04:42.000000000 +0900 ++++ b/usr/include/urcu/uatomic/generic.h 2017-02-13 10:35:21.189927116 +0900 +@@ -65,17 +65,17 @@ + switch (len) { + #ifdef UATOMIC_HAS_ATOMIC_BYTE + case 1: +- return __sync_val_compare_and_swap_1(addr, old, _new); ++ return __sync_val_compare_and_swap_1((uint8_t *) addr, old, _new); + #endif + #ifdef UATOMIC_HAS_ATOMIC_SHORT + case 2: +- return __sync_val_compare_and_swap_2(addr, old, _new); ++ return __sync_val_compare_and_swap_2((uint16_t *) addr, old, _new); + #endif + case 4: +- return __sync_val_compare_and_swap_4(addr, old, _new); ++ return __sync_val_compare_and_swap_4((uint32_t *) addr, old, _new); + #if (CAA_BITS_PER_LONG == 64) + case 8: +- return __sync_val_compare_and_swap_8(addr, old, _new); ++ return __sync_val_compare_and_swap_8((uint64_t *) addr, old, _new); + #endif + } + _uatomic_link_error(); +@@ -100,20 +100,20 @@ + switch (len) { + #ifdef UATOMIC_HAS_ATOMIC_BYTE + case 1: +- __sync_and_and_fetch_1(addr, val); ++ __sync_and_and_fetch_1((uint8_t *) addr, val); + return; + #endif + #ifdef UATOMIC_HAS_ATOMIC_SHORT + case 2: +- __sync_and_and_fetch_2(addr, val); ++ __sync_and_and_fetch_2((uint16_t *) addr, val); + return; + #endif + case 4: +- __sync_and_and_fetch_4(addr, val); ++ __sync_and_and_fetch_4((uint32_t *) addr, val); + return; + #if (CAA_BITS_PER_LONG == 64) + case 8: +- __sync_and_and_fetch_8(addr, val); ++ __sync_and_and_fetch_8((uint64_t *) addr, val); + return; + #endif + } +@@ -139,20 +139,20 @@ + switch (len) { + #ifdef UATOMIC_HAS_ATOMIC_BYTE + case 1: +- __sync_or_and_fetch_1(addr, val); ++ __sync_or_and_fetch_1((uint8_t *) addr, val); + return; + #endif + #ifdef UATOMIC_HAS_ATOMIC_SHORT + case 2: +- __sync_or_and_fetch_2(addr, val); ++ __sync_or_and_fetch_2((uint16_t *) addr, val); + return; + #endif + case 4: +- __sync_or_and_fetch_4(addr, val); ++ __sync_or_and_fetch_4((uint32_t *) addr, val); + return; + #if (CAA_BITS_PER_LONG == 64) + case 8: +- __sync_or_and_fetch_8(addr, val); ++ __sync_or_and_fetch_8((uint64_t *) addr, val); + return; + #endif + } +@@ -180,17 +180,17 @@ + switch (len) { + #ifdef UATOMIC_HAS_ATOMIC_BYTE + case 1: +- return __sync_add_and_fetch_1(addr, val); ++ return __sync_add_and_fetch_1((uint8_t *) addr, val); + #endif + #ifdef UATOMIC_HAS_ATOMIC_SHORT + case 2: +- return __sync_add_and_fetch_2(addr, val); ++ return __sync_add_and_fetch_2((uint16_t *) addr, val); + #endif + case 4: +- return __sync_add_and_fetch_4(addr, val); ++ return __sync_add_and_fetch_4((uint32_t *) addr, val); + #if (CAA_BITS_PER_LONG == 64) + case 8: +- return __sync_add_and_fetch_8(addr, val); ++ return __sync_add_and_fetch_8((uint64_t *) addr, val); + #endif + } + _uatomic_link_error(); diff --git a/eng/common/cross/arm64/sources.list.bionic b/eng/common/cross/arm64/sources.list.bionic new file mode 100644 index 0000000000..2109557409 --- /dev/null +++ b/eng/common/cross/arm64/sources.list.bionic @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse diff --git a/eng/common/cross/arm64/sources.list.trusty b/eng/common/cross/arm64/sources.list.trusty new file mode 100644 index 0000000000..07d8f88d82 --- /dev/null +++ b/eng/common/cross/arm64/sources.list.trusty @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ trusty main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ trusty-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ trusty-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ trusty-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty-security main restricted universe multiverse \ No newline at end of file diff --git a/eng/common/cross/arm64/sources.list.xenial b/eng/common/cross/arm64/sources.list.xenial new file mode 100644 index 0000000000..eacd86b7df --- /dev/null +++ b/eng/common/cross/arm64/sources.list.xenial @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted universe multiverse \ No newline at end of file diff --git a/eng/common/cross/arm64/sources.list.zesty b/eng/common/cross/arm64/sources.list.zesty new file mode 100644 index 0000000000..ea2c14a787 --- /dev/null +++ b/eng/common/cross/arm64/sources.list.zesty @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ zesty main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-security main restricted universe multiverse diff --git a/eng/common/cross/armel/sources.list.jessie b/eng/common/cross/armel/sources.list.jessie new file mode 100644 index 0000000000..3d9c3059d8 --- /dev/null +++ b/eng/common/cross/armel/sources.list.jessie @@ -0,0 +1,3 @@ +# Debian (jessie) # Stable +deb http://ftp.debian.org/debian/ jessie main contrib non-free +deb-src http://ftp.debian.org/debian/ jessie main contrib non-free diff --git a/eng/common/cross/armel/tizen-build-rootfs.sh b/eng/common/cross/armel/tizen-build-rootfs.sh new file mode 100755 index 0000000000..87c48e78fb --- /dev/null +++ b/eng/common/cross/armel/tizen-build-rootfs.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -e + +__ARM_SOFTFP_CrossDir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +__TIZEN_CROSSDIR="$__ARM_SOFTFP_CrossDir/tizen" + +if [[ -z "$ROOTFS_DIR" ]]; then + echo "ROOTFS_DIR is not defined." + exit 1; +fi + +# Clean-up (TODO-Cleanup: We may already delete $ROOTFS_DIR at ./cross/build-rootfs.sh.) +# hk0110 +if [ -d "$ROOTFS_DIR" ]; then + umount $ROOTFS_DIR/* + rm -rf $ROOTFS_DIR +fi + +TIZEN_TMP_DIR=$ROOTFS_DIR/tizen_tmp +mkdir -p $TIZEN_TMP_DIR + +# Download files +echo ">>Start downloading files" +VERBOSE=1 $__ARM_SOFTFP_CrossDir/tizen-fetch.sh $TIZEN_TMP_DIR +echo "<>Start constructing Tizen rootfs" +TIZEN_RPM_FILES=`ls $TIZEN_TMP_DIR/*.rpm` +cd $ROOTFS_DIR +for f in $TIZEN_RPM_FILES; do + rpm2cpio $f | cpio -idm --quiet +done +echo "<>Start configuring Tizen rootfs" +rm ./usr/lib/libunwind.so +ln -s libunwind.so.8 ./usr/lib/libunwind.so +ln -sfn asm-arm ./usr/include/asm +patch -p1 < $__TIZEN_CROSSDIR/tizen.patch +echo "</dev/null; then + VERBOSE=0 +fi + +Log() +{ + if [ $VERBOSE -ge $1 ]; then + echo ${@:2} + fi +} + +Inform() +{ + Log 1 -e "\x1B[0;34m$@\x1B[m" +} + +Debug() +{ + Log 2 -e "\x1B[0;32m$@\x1B[m" +} + +Error() +{ + >&2 Log 0 -e "\x1B[0;31m$@\x1B[m" +} + +Fetch() +{ + URL=$1 + FILE=$2 + PROGRESS=$3 + if [ $VERBOSE -ge 1 ] && [ $PROGRESS ]; then + CURL_OPT="--progress-bar" + else + CURL_OPT="--silent" + fi + curl $CURL_OPT $URL > $FILE +} + +hash curl 2> /dev/null || { Error "Require 'curl' Aborting."; exit 1; } +hash xmllint 2> /dev/null || { Error "Require 'xmllint' Aborting."; exit 1; } +hash sha256sum 2> /dev/null || { Error "Require 'sha256sum' Aborting."; exit 1; } + +TMPDIR=$1 +if [ ! -d $TMPDIR ]; then + TMPDIR=./tizen_tmp + Debug "Create temporary directory : $TMPDIR" + mkdir -p $TMPDIR +fi + +TIZEN_URL=http://download.tizen.org/releases/milestone/tizen +BUILD_XML=build.xml +REPOMD_XML=repomd.xml +PRIMARY_XML=primary.xml +TARGET_URL="http://__not_initialized" + +Xpath_get() +{ + XPATH_RESULT='' + XPATH=$1 + XML_FILE=$2 + RESULT=$(xmllint --xpath $XPATH $XML_FILE) + if [[ -z ${RESULT// } ]]; then + Error "Can not find target from $XML_FILE" + Debug "Xpath = $XPATH" + exit 1 + fi + XPATH_RESULT=$RESULT +} + +fetch_tizen_pkgs_init() +{ + TARGET=$1 + PROFILE=$2 + Debug "Initialize TARGET=$TARGET, PROFILE=$PROFILE" + + TMP_PKG_DIR=$TMPDIR/tizen_${PROFILE}_pkgs + if [ -d $TMP_PKG_DIR ]; then rm -rf $TMP_PKG_DIR; fi + mkdir -p $TMP_PKG_DIR + + PKG_URL=$TIZEN_URL/$PROFILE/latest + + BUILD_XML_URL=$PKG_URL/$BUILD_XML + TMP_BUILD=$TMP_PKG_DIR/$BUILD_XML + TMP_REPOMD=$TMP_PKG_DIR/$REPOMD_XML + TMP_PRIMARY=$TMP_PKG_DIR/$PRIMARY_XML + TMP_PRIMARYGZ=${TMP_PRIMARY}.gz + + Fetch $BUILD_XML_URL $TMP_BUILD + + Debug "fetch $BUILD_XML_URL to $TMP_BUILD" + + TARGET_XPATH="//build/buildtargets/buildtarget[@name=\"$TARGET\"]/repo[@type=\"binary\"]/text()" + Xpath_get $TARGET_XPATH $TMP_BUILD + TARGET_PATH=$XPATH_RESULT + TARGET_URL=$PKG_URL/$TARGET_PATH + + REPOMD_URL=$TARGET_URL/repodata/repomd.xml + PRIMARY_XPATH='string(//*[local-name()="data"][@type="primary"]/*[local-name()="location"]/@href)' + + Fetch $REPOMD_URL $TMP_REPOMD + + Debug "fetch $REPOMD_URL to $TMP_REPOMD" + + Xpath_get $PRIMARY_XPATH $TMP_REPOMD + PRIMARY_XML_PATH=$XPATH_RESULT + PRIMARY_URL=$TARGET_URL/$PRIMARY_XML_PATH + + Fetch $PRIMARY_URL $TMP_PRIMARYGZ + + Debug "fetch $PRIMARY_URL to $TMP_PRIMARYGZ" + + gunzip $TMP_PRIMARYGZ + + Debug "unzip $TMP_PRIMARYGZ to $TMP_PRIMARY" +} + +fetch_tizen_pkgs() +{ + ARCH=$1 + PACKAGE_XPATH_TPL='string(//*[local-name()="metadata"]/*[local-name()="package"][*[local-name()="name"][text()="_PKG_"]][*[local-name()="arch"][text()="_ARCH_"]]/*[local-name()="location"]/@href)' + + PACKAGE_CHECKSUM_XPATH_TPL='string(//*[local-name()="metadata"]/*[local-name()="package"][*[local-name()="name"][text()="_PKG_"]][*[local-name()="arch"][text()="_ARCH_"]]/*[local-name()="checksum"]/text())' + + for pkg in ${@:2} + do + Inform "Fetching... $pkg" + XPATH=${PACKAGE_XPATH_TPL/_PKG_/$pkg} + XPATH=${XPATH/_ARCH_/$ARCH} + Xpath_get $XPATH $TMP_PRIMARY + PKG_PATH=$XPATH_RESULT + + XPATH=${PACKAGE_CHECKSUM_XPATH_TPL/_PKG_/$pkg} + XPATH=${XPATH/_ARCH_/$ARCH} + Xpath_get $XPATH $TMP_PRIMARY + CHECKSUM=$XPATH_RESULT + + PKG_URL=$TARGET_URL/$PKG_PATH + PKG_FILE=$(basename $PKG_PATH) + PKG_PATH=$TMPDIR/$PKG_FILE + + Debug "Download $PKG_URL to $PKG_PATH" + Fetch $PKG_URL $PKG_PATH true + + echo "$CHECKSUM $PKG_PATH" | sha256sum -c - > /dev/null + if [ $? -ne 0 ]; then + Error "Fail to fetch $PKG_URL to $PKG_PATH" + Debug "Checksum = $CHECKSUM" + exit 1 + fi + done +} + +Inform "Initialize arm base" +fetch_tizen_pkgs_init standard base +Inform "fetch common packages" +fetch_tizen_pkgs armv7l gcc glibc glibc-devel libicu libicu-devel +fetch_tizen_pkgs noarch linux-glibc-devel +Inform "fetch coreclr packages" +fetch_tizen_pkgs armv7l lldb lldb-devel libgcc libstdc++ libstdc++-devel libunwind libunwind-devel tizen-release lttng-ust-devel lttng-ust userspace-rcu-devel userspace-rcu +Inform "fetch corefx packages" +fetch_tizen_pkgs armv7l libcom_err libcom_err-devel zlib zlib-devel libopenssl libopenssl-devel krb5 krb5-devel libcurl libcurl-devel + +Inform "Initialize standard unified" +fetch_tizen_pkgs_init standard unified +Inform "fetch corefx packages" +fetch_tizen_pkgs armv7l gssdp gssdp-devel + diff --git a/eng/common/cross/armel/tizen/tizen-dotnet.ks b/eng/common/cross/armel/tizen/tizen-dotnet.ks new file mode 100644 index 0000000000..506d455bd4 --- /dev/null +++ b/eng/common/cross/armel/tizen/tizen-dotnet.ks @@ -0,0 +1,50 @@ +lang en_US.UTF-8 +keyboard us +timezone --utc Asia/Seoul + +part / --fstype="ext4" --size=3500 --ondisk=mmcblk0 --label rootfs --fsoptions=defaults,noatime + +rootpw tizen +desktop --autologinuser=root +user --name root --groups audio,video --password 'tizen' + +repo --name=standard --baseurl=http://download.tizen.org/releases/milestone/tizen/unified/latest/repos/standard/packages/ --ssl_verify=no +repo --name=base --baseurl=http://download.tizen.org/releases/milestone/tizen/base/latest/repos/standard/packages/ --ssl_verify=no + +%packages +tar +gzip + +sed +grep +gawk +perl + +binutils +findutils +util-linux +lttng-ust +userspace-rcu +procps-ng +tzdata +ca-certificates + + +### Core FX +libicu +libunwind +iputils +zlib +krb5 +libcurl +libopenssl + +%end + +%post + +### Update /tmp privilege +chmod 777 /tmp +#################################### + +%end diff --git a/eng/common/cross/armel/tizen/tizen.patch b/eng/common/cross/armel/tizen/tizen.patch new file mode 100644 index 0000000000..d223427c97 --- /dev/null +++ b/eng/common/cross/armel/tizen/tizen.patch @@ -0,0 +1,18 @@ +diff -u -r a/usr/lib/libc.so b/usr/lib/libc.so +--- a/usr/lib/libc.so 2016-12-30 23:00:08.284951863 +0900 ++++ b/usr/lib/libc.so 2016-12-30 23:00:32.140951815 +0900 +@@ -2,4 +2,4 @@ + Use the shared library, but some functions are only in + the static library, so try that secondarily. */ + OUTPUT_FORMAT(elf32-littlearm) +-GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a AS_NEEDED ( /lib/ld-linux.so.3 ) ) ++GROUP ( libc.so.6 libc_nonshared.a AS_NEEDED ( ld-linux.so.3 ) ) +diff -u -r a/usr/lib/libpthread.so b/usr/lib/libpthread.so +--- a/usr/lib/libpthread.so 2016-12-30 23:00:19.408951841 +0900 ++++ b/usr/lib/libpthread.so 2016-12-30 23:00:39.068951801 +0900 +@@ -2,4 +2,4 @@ + Use the shared library, but some functions are only in + the static library, so try that secondarily. */ + OUTPUT_FORMAT(elf32-littlearm) +-GROUP ( /lib/libpthread.so.0 /usr/lib/libpthread_nonshared.a ) ++GROUP ( libpthread.so.0 libpthread_nonshared.a ) diff --git a/eng/common/cross/build-android-rootfs.sh b/eng/common/cross/build-android-rootfs.sh new file mode 100755 index 0000000000..adceda877a --- /dev/null +++ b/eng/common/cross/build-android-rootfs.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +set -e +__NDK_Version=r14 + +usage() +{ + echo "Creates a toolchain and sysroot used for cross-compiling for Android." + echo. + echo "Usage: $0 [BuildArch] [ApiLevel]" + echo. + echo "BuildArch is the target architecture of Android. Currently only arm64 is supported." + echo "ApiLevel is the target Android API level. API levels usually match to Android releases. See https://source.android.com/source/build-numbers.html" + echo. + echo "By default, the toolchain and sysroot will be generated in cross/android-rootfs/toolchain/[BuildArch]. You can change this behavior" + echo "by setting the TOOLCHAIN_DIR environment variable" + echo. + echo "By default, the NDK will be downloaded into the cross/android-rootfs/android-ndk-$__NDK_Version directory. If you already have an NDK installation," + echo "you can set the NDK_DIR environment variable to have this script use that installation of the NDK." + echo "By default, this script will generate a file, android_platform, in the root of the ROOTFS_DIR directory that contains the RID for the supported and tested Android build: android.21-arm64. This file is to replace '/etc/os-release', which is not available for Android." + exit 1 +} + +__ApiLevel=21 # The minimum platform for arm64 is API level 21 +__BuildArch=arm64 +__AndroidArch=aarch64 +__AndroidToolchain=aarch64-linux-android + +for i in "$@" + do + lowerI="$(echo $i | awk '{print tolower($0)}')" + case $lowerI in + -?|-h|--help) + usage + exit 1 + ;; + arm64) + __BuildArch=arm64 + __AndroidArch=aarch64 + __AndroidToolchain=aarch64-linux-android + ;; + arm) + __BuildArch=arm + __AndroidArch=arm + __AndroidToolchain=arm-linux-androideabi + ;; + *[0-9]) + __ApiLevel=$i + ;; + *) + __UnprocessedBuildArgs="$__UnprocessedBuildArgs $i" + ;; + esac +done + +# Obtain the location of the bash script to figure out where the root of the repo is. +__CrossDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +__Android_Cross_Dir="$__CrossDir/android-rootfs" +__NDK_Dir="$__Android_Cross_Dir/android-ndk-$__NDK_Version" +__libunwind_Dir="$__Android_Cross_Dir/libunwind" +__lldb_Dir="$__Android_Cross_Dir/lldb" +__ToolchainDir="$__Android_Cross_Dir/toolchain/$__BuildArch" + +if [[ -n "$TOOLCHAIN_DIR" ]]; then + __ToolchainDir=$TOOLCHAIN_DIR +fi + +if [[ -n "$NDK_DIR" ]]; then + __NDK_Dir=$NDK_DIR +fi + +echo "Target API level: $__ApiLevel" +echo "Target architecture: $__BuildArch" +echo "NDK location: $__NDK_Dir" +echo "Target Toolchain location: $__ToolchainDir" + +# Download the NDK if required +if [ ! -d $__NDK_Dir ]; then + echo Downloading the NDK into $__NDK_Dir + mkdir -p $__NDK_Dir + wget -nv -nc --show-progress https://dl.google.com/android/repository/android-ndk-$__NDK_Version-linux-x86_64.zip -O $__Android_Cross_Dir/android-ndk-$__NDK_Version-linux-x86_64.zip + unzip -q $__Android_Cross_Dir/android-ndk-$__NDK_Version-linux-x86_64.zip -d $__Android_Cross_Dir +fi + +if [ ! -d $__lldb_Dir ]; then + mkdir -p $__lldb_Dir + echo Downloading LLDB into $__lldb_Dir + wget -nv -nc --show-progress https://dl.google.com/android/repository/lldb-2.3.3614996-linux-x86_64.zip -O $__Android_Cross_Dir/lldb-2.3.3614996-linux-x86_64.zip + unzip -q $__Android_Cross_Dir/lldb-2.3.3614996-linux-x86_64.zip -d $__lldb_Dir +fi + +# Create the RootFS for both arm64 as well as aarch +rm -rf $__Android_Cross_Dir/toolchain + +echo Generating the $__BuildArch toolchain +$__NDK_Dir/build/tools/make_standalone_toolchain.py --arch $__BuildArch --api $__ApiLevel --install-dir $__ToolchainDir + +# Install the required packages into the toolchain +# TODO: Add logic to get latest pkg version instead of specific version number +rm -rf $__Android_Cross_Dir/deb/ +rm -rf $__Android_Cross_Dir/tmp + +mkdir -p $__Android_Cross_Dir/deb/ +mkdir -p $__Android_Cross_Dir/tmp/$arch/ +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libicu_60.2_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libicu_60.2_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libicu-dev_60.2_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libicu-dev_60.2_$__AndroidArch.deb + +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libandroid-glob-dev_0.4_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libandroid-glob-dev_0.4_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libandroid-glob_0.4_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libandroid-glob_0.4_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libandroid-support-dev_22_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libandroid-support-dev_22_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libandroid-support_22_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libandroid-support_22_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/liblzma-dev_5.2.3_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/liblzma-dev_5.2.3_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/liblzma_5.2.3_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/liblzma_5.2.3_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libunwind-dev_1.2.20170304_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libunwind-dev_1.2.20170304_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libunwind_1.2.20170304_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libunwind_1.2.20170304_$__AndroidArch.deb + +echo Unpacking Termux packages +dpkg -x $__Android_Cross_Dir/deb/libicu_60.2_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/libicu-dev_60.2_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/libandroid-glob-dev_0.4_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/libandroid-glob_0.4_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/libandroid-support-dev_22_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/libandroid-support_22_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/liblzma-dev_5.2.3_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/liblzma_5.2.3_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/libunwind-dev_1.2.20170304_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/libunwind_1.2.20170304_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ + +cp -R $__Android_Cross_Dir/tmp/$__AndroidArch/data/data/com.termux/files/usr/* $__ToolchainDir/sysroot/usr/ + +# Generate platform file for build.sh script to assign to __DistroRid +echo "Generating platform file..." + +echo "RID=android.21-arm64" > $__ToolchainDir/sysroot/android_platform +echo Now run: +echo CONFIG_DIR=\`realpath cross/android/$__BuildArch\` ROOTFS_DIR=\`realpath $__ToolchainDir/sysroot\` ./build.sh cross $__BuildArch skipgenerateversion skipnuget cmakeargs -DENABLE_LLDBPLUGIN=0 + diff --git a/eng/common/cross/build-rootfs.sh b/eng/common/cross/build-rootfs.sh new file mode 100755 index 0000000000..83ec39195c --- /dev/null +++ b/eng/common/cross/build-rootfs.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash + +usage() +{ + echo "Usage: $0 [BuildArch] [LinuxCodeName] [lldbx.y] [--skipunmount] --rootfs ]" + echo "BuildArch can be: arm(default), armel, arm64, x86" + echo "LinuxCodeName - optional, Code name for Linux, can be: trusty, xenial(default), zesty, bionic, alpine. If BuildArch is armel, LinuxCodeName is jessie(default) or tizen." + echo "lldbx.y - optional, LLDB version, can be: lldb3.9(default), lldb4.0, lldb5.0, lldb6.0 no-lldb. Ignored for alpine" + echo "--skipunmount - optional, will skip the unmount of rootfs folder." + exit 1 +} + +__LinuxCodeName=xenial +__CrossDir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +__InitialDir=$PWD +__BuildArch=arm +__UbuntuArch=armhf +__UbuntuRepo="http://ports.ubuntu.com/" +__LLDB_Package="liblldb-3.9-dev" +__SkipUnmount=0 + +# base development support +__UbuntuPackages="build-essential" + +__AlpinePackages="alpine-base" +__AlpinePackages+=" build-base" +__AlpinePackages+=" linux-headers" +__AlpinePackages+=" lldb-dev" +__AlpinePackages+=" llvm-dev" + +# symlinks fixer +__UbuntuPackages+=" symlinks" + +# CoreCLR and CoreFX dependencies +__UbuntuPackages+=" libicu-dev" +__UbuntuPackages+=" liblttng-ust-dev" +__UbuntuPackages+=" libunwind8-dev" + +__AlpinePackages+=" gettext-dev" +__AlpinePackages+=" icu-dev" +__AlpinePackages+=" libunwind-dev" +__AlpinePackages+=" lttng-ust-dev" + +# CoreFX dependencies +__UbuntuPackages+=" libcurl4-openssl-dev" +__UbuntuPackages+=" libkrb5-dev" +__UbuntuPackages+=" libssl-dev" +__UbuntuPackages+=" zlib1g-dev" + +__AlpinePackages+=" curl-dev" +__AlpinePackages+=" krb5-dev" +__AlpinePackages+=" openssl-dev" +__AlpinePackages+=" zlib-dev" + +__UnprocessedBuildArgs= +while :; do + if [ $# -le 0 ]; then + break + fi + + lowerI="$(echo $1 | awk '{print tolower($0)}')" + case $lowerI in + -?|-h|--help) + usage + exit 1 + ;; + arm) + __BuildArch=arm + __UbuntuArch=armhf + __AlpineArch=armhf + __QEMUArch=arm + ;; + arm64) + __BuildArch=arm64 + __UbuntuArch=arm64 + __AlpineArch=aarch64 + __QEMUArch=aarch64 + ;; + armel) + __BuildArch=armel + __UbuntuArch=armel + __UbuntuRepo="http://ftp.debian.org/debian/" + __LinuxCodeName=jessie + ;; + x86) + __BuildArch=x86 + __UbuntuArch=i386 + __UbuntuRepo="http://archive.ubuntu.com/ubuntu/" + ;; + lldb3.6) + __LLDB_Package="lldb-3.6-dev" + ;; + lldb3.8) + __LLDB_Package="lldb-3.8-dev" + ;; + lldb3.9) + __LLDB_Package="liblldb-3.9-dev" + ;; + lldb4.0) + __LLDB_Package="liblldb-4.0-dev" + ;; + lldb5.0) + __LLDB_Package="liblldb-5.0-dev" + ;; + lldb6.0) + __LLDB_Package="liblldb-6.0-dev" + ;; + no-lldb) + unset __LLDB_Package + ;; + trusty) # Ubuntu 14.04 + if [ "$__LinuxCodeName" != "jessie" ]; then + __LinuxCodeName=trusty + fi + ;; + xenial) # Ubunry 16.04 + if [ "$__LinuxCodeName" != "jessie" ]; then + __LinuxCodeName=xenial + fi + ;; + zesty) # Ununtu 17.04 + if [ "$__LinuxCodeName" != "jessie" ]; then + __LinuxCodeName=zesty + fi + ;; + bionic) # Ubuntu 18.04 + if [ "$__LinuxCodeName" != "jessie" ]; then + __LinuxCodeName=bionic + fi + ;; + jessie) # Debian 8 + __LinuxCodeName=jessie + __UbuntuRepo="http://ftp.debian.org/debian/" + ;; + # TBD Stretch -> Debian 9, Buster -> Debian 10 + tizen) + if [ "$__BuildArch" != "armel" ]; then + echo "Tizen is available only for armel." + usage; + exit 1; + fi + __LinuxCodeName= + __UbuntuRepo= + __Tizen=tizen + ;; + alpine) + __LinuxCodeName=alpine + __UbuntuRepo= + ;; + --skipunmount) + __SkipUnmount=1 + ;; + --rootfsdir|-rootfsdir) + shift + __RootfsDir=$1 + ;; + *) + __UnprocessedBuildArgs="$__UnprocessedBuildArgs $1" + ;; + esac + + shift +done + +if [ "$__BuildArch" == "armel" ]; then + __LLDB_Package="lldb-3.5-dev" +fi +__UbuntuPackages+=" ${__LLDB_Package:-}" + +if [ -z "$__RootfsDir" ] && [ ! -z "$ROOTFS_DIR" ]; then + __RootfsDir=$ROOTFS_DIR +fi + +if [ -z "$__RootfsDir" ]; then + __RootfsDir="$__CrossDir/rootfs/$__BuildArch" +fi + +if [ -d "$__RootfsDir" ]; then + if [ $__SkipUnmount == 0 ]; then + umount $__RootfsDir/* + fi + rm -rf $__RootfsDir +fi + +if [[ "$__LinuxCodeName" == "alpine" ]]; then + __ApkToolsVersion=2.9.1 + __AlpineVersion=3.7 + __ApkToolsDir=$(mktemp -d) + wget https://github.com/alpinelinux/apk-tools/releases/download/v$__ApkToolsVersion/apk-tools-$__ApkToolsVersion-x86_64-linux.tar.gz -P $__ApkToolsDir + tar -xf $__ApkToolsDir/apk-tools-$__ApkToolsVersion-x86_64-linux.tar.gz -C $__ApkToolsDir + mkdir -p $__RootfsDir/usr/bin + cp -v /usr/bin/qemu-$__QEMUArch-static $__RootfsDir/usr/bin + $__ApkToolsDir/apk-tools-$__ApkToolsVersion/apk \ + -X http://dl-cdn.alpinelinux.org/alpine/v$__AlpineVersion/main \ + -X http://dl-cdn.alpinelinux.org/alpine/v$__AlpineVersion/community \ + -X http://dl-cdn.alpinelinux.org/alpine/edge/testing \ + -U --allow-untrusted --root $__RootfsDir --arch $__AlpineArch --initdb \ + add $__AlpinePackages + rm -r $__ApkToolsDir +elif [[ -n $__LinuxCodeName ]]; then + qemu-debootstrap --arch $__UbuntuArch $__LinuxCodeName $__RootfsDir $__UbuntuRepo + cp $__CrossDir/$__BuildArch/sources.list.$__LinuxCodeName $__RootfsDir/etc/apt/sources.list + chroot $__RootfsDir apt-get update + chroot $__RootfsDir apt-get -f -y install + chroot $__RootfsDir apt-get -y install $__UbuntuPackages + chroot $__RootfsDir symlinks -cr /usr + + if [ $__SkipUnmount == 0 ]; then + umount $__RootfsDir/* + fi + + if [[ "$__BuildArch" == "arm" && "$__LinuxCodeName" == "trusty" ]]; then + pushd $__RootfsDir + patch -p1 < $__CrossDir/$__BuildArch/trusty.patch + patch -p1 < $__CrossDir/$__BuildArch/trusty-lttng-2.4.patch + popd + fi +elif [ "$__Tizen" == "tizen" ]; then + ROOTFS_DIR=$__RootfsDir $__CrossDir/$__BuildArch/tizen-build-rootfs.sh +else + echo "Unsupported target platform." + usage; + exit 1 +fi diff --git a/eng/common/cross/toolchain.cmake b/eng/common/cross/toolchain.cmake new file mode 100644 index 0000000000..071d411241 --- /dev/null +++ b/eng/common/cross/toolchain.cmake @@ -0,0 +1,138 @@ +set(CROSS_ROOTFS $ENV{ROOTFS_DIR}) + +set(TARGET_ARCH_NAME $ENV{TARGET_BUILD_ARCH}) +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_VERSION 1) + +if(TARGET_ARCH_NAME STREQUAL "armel") + set(CMAKE_SYSTEM_PROCESSOR armv7l) + set(TOOLCHAIN "arm-linux-gnueabi") + if("$ENV{__DistroRid}" MATCHES "tizen.*") + set(TIZEN_TOOLCHAIN "armv7l-tizen-linux-gnueabi/6.2.1") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "arm") + set(CMAKE_SYSTEM_PROCESSOR armv7l) + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/armv6-alpine-linux-musleabihf) + set(TOOLCHAIN "armv6-alpine-linux-musleabihf") + else() + set(TOOLCHAIN "arm-linux-gnueabihf") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "arm64") + set(CMAKE_SYSTEM_PROCESSOR aarch64) + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/aarch64-alpine-linux-musl) + set(TOOLCHAIN "aarch64-alpine-linux-musl") + else() + set(TOOLCHAIN "aarch64-linux-gnu") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "x86") + set(CMAKE_SYSTEM_PROCESSOR i686) + set(TOOLCHAIN "i686-linux-gnu") +else() + message(FATAL_ERROR "Arch is ${TARGET_ARCH_NAME}. Only armel, arm, arm64 and x86 are supported!") +endif() + +# Specify include paths +if(TARGET_ARCH_NAME STREQUAL "armel") + if(DEFINED TIZEN_TOOLCHAIN) + include_directories(SYSTEM ${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}/include/c++/) + include_directories(SYSTEM ${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}/include/c++/armv7l-tizen-linux-gnueabi) + endif() +endif() + +# add_compile_param - adds only new options without duplicates. +# arg0 - list with result options, arg1 - list with new options. +# arg2 - optional argument, quick summary string for optional using CACHE FORCE mode. +macro(add_compile_param) + if(NOT ${ARGC} MATCHES "^(2|3)$") + message(FATAL_ERROR "Wrong using add_compile_param! Two or three parameters must be given! See add_compile_param description.") + endif() + foreach(OPTION ${ARGV1}) + if(NOT ${ARGV0} MATCHES "${OPTION}($| )") + set(${ARGV0} "${${ARGV0}} ${OPTION}") + if(${ARGC} EQUAL "3") # CACHE FORCE mode + set(${ARGV0} "${${ARGV0}}" CACHE STRING "${ARGV2}" FORCE) + endif() + endif() + endforeach() +endmacro() + +# Specify link flags +add_compile_param(CROSS_LINK_FLAGS "--sysroot=${CROSS_ROOTFS}") +add_compile_param(CROSS_LINK_FLAGS "--gcc-toolchain=${CROSS_ROOTFS}/usr") +add_compile_param(CROSS_LINK_FLAGS "--target=${TOOLCHAIN}") +add_compile_param(CROSS_LINK_FLAGS "-fuse-ld=gold") + +if(TARGET_ARCH_NAME STREQUAL "armel") + if(DEFINED TIZEN_TOOLCHAIN) # For Tizen only + add_compile_param(CROSS_LINK_FLAGS "-B${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}") + add_compile_param(CROSS_LINK_FLAGS "-L${CROSS_ROOTFS}/lib") + add_compile_param(CROSS_LINK_FLAGS "-L${CROSS_ROOTFS}/usr/lib") + add_compile_param(CROSS_LINK_FLAGS "-L${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "x86") + add_compile_param(CROSS_LINK_FLAGS "-m32") +endif() + +add_compile_param(CMAKE_EXE_LINKER_FLAGS "${CROSS_LINK_FLAGS}" "TOOLCHAIN_EXE_LINKER_FLAGS") +add_compile_param(CMAKE_SHARED_LINKER_FLAGS "${CROSS_LINK_FLAGS}" "TOOLCHAIN_EXE_LINKER_FLAGS") +add_compile_param(CMAKE_MODULE_LINKER_FLAGS "${CROSS_LINK_FLAGS}" "TOOLCHAIN_EXE_LINKER_FLAGS") + +# Specify compile options +add_compile_options("--sysroot=${CROSS_ROOTFS}") +add_compile_options("--target=${TOOLCHAIN}") +add_compile_options("--gcc-toolchain=${CROSS_ROOTFS}/usr") + +if(TARGET_ARCH_NAME MATCHES "^(arm|armel|arm64)$") + set(CMAKE_C_COMPILER_TARGET ${TOOLCHAIN}) + set(CMAKE_CXX_COMPILER_TARGET ${TOOLCHAIN}) + set(CMAKE_ASM_COMPILER_TARGET ${TOOLCHAIN}) +endif() + +if(TARGET_ARCH_NAME MATCHES "^(arm|armel)$") + add_compile_options(-mthumb) + add_compile_options(-mfpu=vfpv3) + if(TARGET_ARCH_NAME STREQUAL "armel") + add_compile_options(-mfloat-abi=softfp) + if(DEFINED TIZEN_TOOLCHAIN) + add_compile_options(-Wno-deprecated-declarations) # compile-time option + add_compile_options(-D__extern_always_inline=inline) # compile-time option + endif() + endif() +elseif(TARGET_ARCH_NAME STREQUAL "x86") + add_compile_options(-m32) + add_compile_options(-Wno-error=unused-command-line-argument) +endif() + +# Set LLDB include and library paths +if(TARGET_ARCH_NAME MATCHES "^(arm|armel|x86)$") + if(TARGET_ARCH_NAME STREQUAL "x86") + set(LLVM_CROSS_DIR "$ENV{LLVM_CROSS_HOME}") + else() # arm/armel case + set(LLVM_CROSS_DIR "$ENV{LLVM_ARM_HOME}") + endif() + if(LLVM_CROSS_DIR) + set(WITH_LLDB_LIBS "${LLVM_CROSS_DIR}/lib/" CACHE STRING "") + set(WITH_LLDB_INCLUDES "${LLVM_CROSS_DIR}/include" CACHE STRING "") + set(LLDB_H "${WITH_LLDB_INCLUDES}" CACHE STRING "") + set(LLDB "${LLVM_CROSS_DIR}/lib/liblldb.so" CACHE STRING "") + else() + if(TARGET_ARCH_NAME STREQUAL "x86") + set(WITH_LLDB_LIBS "${CROSS_ROOTFS}/usr/lib/i386-linux-gnu" CACHE STRING "") + set(CHECK_LLVM_DIR "${CROSS_ROOTFS}/usr/lib/llvm-3.8/include") + if(EXISTS "${CHECK_LLVM_DIR}" AND IS_DIRECTORY "${CHECK_LLVM_DIR}") + set(WITH_LLDB_INCLUDES "${CHECK_LLVM_DIR}") + else() + set(WITH_LLDB_INCLUDES "${CROSS_ROOTFS}/usr/lib/llvm-3.6/include") + endif() + else() # arm/armel case + set(WITH_LLDB_LIBS "${CROSS_ROOTFS}/usr/lib/${TOOLCHAIN}" CACHE STRING "") + set(WITH_LLDB_INCLUDES "${CROSS_ROOTFS}/usr/lib/llvm-3.6/include" CACHE STRING "") + endif() + endif() +endif() + +set(CMAKE_FIND_ROOT_PATH "${CROSS_ROOTFS}") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/eng/common/cross/x86/sources.list.bionic b/eng/common/cross/x86/sources.list.bionic new file mode 100644 index 0000000000..a71ccadcff --- /dev/null +++ b/eng/common/cross/x86/sources.list.bionic @@ -0,0 +1,11 @@ +deb http://archive.ubuntu.com/ubuntu/ bionic main restricted universe +deb-src http://archive.ubuntu.com/ubuntu/ bionic main restricted universe + +deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted universe +deb-src http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted universe + +deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted +deb-src http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted + +deb http://archive.ubuntu.com/ubuntu/ bionic-security main restricted universe multiverse +deb-src http://archive.ubuntu.com/ubuntu/ bionic-security main restricted universe multiverse diff --git a/eng/common/cross/x86/sources.list.trusty b/eng/common/cross/x86/sources.list.trusty new file mode 100644 index 0000000000..9b3085436e --- /dev/null +++ b/eng/common/cross/x86/sources.list.trusty @@ -0,0 +1,11 @@ +deb http://archive.ubuntu.com/ubuntu/ trusty main restricted universe +deb-src http://archive.ubuntu.com/ubuntu/ trusty main restricted universe + +deb http://archive.ubuntu.com/ubuntu/ trusty-updates main restricted universe +deb-src http://archive.ubuntu.com/ubuntu/ trusty-updates main restricted universe + +deb http://archive.ubuntu.com/ubuntu/ trusty-backports main restricted +deb-src http://archive.ubuntu.com/ubuntu/ trusty-backports main restricted + +deb http://archive.ubuntu.com/ubuntu/ trusty-security main restricted universe multiverse +deb-src http://archive.ubuntu.com/ubuntu/ trusty-security main restricted universe multiverse diff --git a/eng/common/cross/x86/sources.list.xenial b/eng/common/cross/x86/sources.list.xenial new file mode 100644 index 0000000000..ad9c5a0144 --- /dev/null +++ b/eng/common/cross/x86/sources.list.xenial @@ -0,0 +1,11 @@ +deb http://archive.ubuntu.com/ubuntu/ xenial main restricted universe +deb-src http://archive.ubuntu.com/ubuntu/ xenial main restricted universe + +deb http://archive.ubuntu.com/ubuntu/ xenial-updates main restricted universe +deb-src http://archive.ubuntu.com/ubuntu/ xenial-updates main restricted universe + +deb http://archive.ubuntu.com/ubuntu/ xenial-backports main restricted +deb-src http://archive.ubuntu.com/ubuntu/ xenial-backports main restricted + +deb http://archive.ubuntu.com/ubuntu/ xenial-security main restricted universe multiverse +deb-src http://archive.ubuntu.com/ubuntu/ xenial-security main restricted universe multiverse diff --git a/eng/common/darc-init.ps1 b/eng/common/darc-init.ps1 new file mode 100644 index 0000000000..81ffd16779 --- /dev/null +++ b/eng/common/darc-init.ps1 @@ -0,0 +1,32 @@ +param ( + $darcVersion = $null +) + +$verbosity = "m" +. $PSScriptRoot\tools.ps1 + +function InstallDarcCli ($darcVersion) { + $darcCliPackageName = "microsoft.dotnet.darc" + + $dotnetRoot = InitializeDotNetCli -install:$true + $dotnet = "$dotnetRoot\dotnet.exe" + $toolList = Invoke-Expression "& `"$dotnet`" tool list -g" + + if ($toolList -like "*$darcCliPackageName*") { + Invoke-Expression "& `"$dotnet`" tool uninstall $darcCliPackageName -g" + } + + # Until we can anonymously query the BAR API for the latest arcade-services + # build applied to the PROD channel, this is hardcoded. + if (-not $darcVersion) { + $darcVersion = '1.1.0-beta.19205.4' + } + + $arcadeServicesSource = 'https://dotnetfeed.blob.core.windows.net/dotnet-arcade/index.json' + + Write-Host "Installing Darc CLI version $darcVersion..." + Write-Host "You may need to restart your command window if this is the first dotnet tool you have installed." + Invoke-Expression "& `"$dotnet`" tool install $darcCliPackageName --version $darcVersion --add-source '$arcadeServicesSource' -v $verbosity -g" +} + +InstallDarcCli $darcVersion diff --git a/eng/common/darc-init.sh b/eng/common/darc-init.sh new file mode 100755 index 0000000000..bd7eb46398 --- /dev/null +++ b/eng/common/darc-init.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +darcVersion="1.1.0-beta.19205.4" + +while [[ $# > 0 ]]; do + opt="$(echo "$1" | awk '{print tolower($0)}')" + case "$opt" in + --darcversion) + darcVersion=$2 + shift + ;; + *) + echo "Invalid argument: $1" + usage + exit 1 + ;; + esac + + shift +done + +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" +verbosity=m + +. "$scriptroot/tools.sh" + +function InstallDarcCli { + local darc_cli_package_name="microsoft.dotnet.darc" + + InitializeDotNetCli + local dotnet_root=$_InitializeDotNetCli + + local uninstall_command=`$dotnet_root/dotnet tool uninstall $darc_cli_package_name -g` + local tool_list=$($dotnet_root/dotnet tool list -g) + if [[ $tool_list = *$darc_cli_package_name* ]]; then + echo $($dotnet_root/dotnet tool uninstall $darc_cli_package_name -g) + fi + + local arcadeServicesSource="https://dotnetfeed.blob.core.windows.net/dotnet-arcade/index.json" + + echo "Installing Darc CLI version $toolset_version..." + echo "You may need to restart your command shell if this is the first dotnet tool you have installed." + echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity -g) +} + +InstallDarcCli diff --git a/eng/common/dotnet-install.cmd b/eng/common/dotnet-install.cmd new file mode 100644 index 0000000000..b1c2642e76 --- /dev/null +++ b/eng/common/dotnet-install.cmd @@ -0,0 +1,2 @@ +@echo off +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0dotnet-install.ps1""" %*" \ No newline at end of file diff --git a/eng/common/dotnet-install.ps1 b/eng/common/dotnet-install.ps1 new file mode 100644 index 0000000000..5987943fd6 --- /dev/null +++ b/eng/common/dotnet-install.ps1 @@ -0,0 +1,22 @@ +[CmdletBinding(PositionalBinding=$false)] +Param( + [string] $verbosity = "minimal", + [string] $architecture = "", + [string] $version = "Latest", + [string] $runtime = "dotnet" +) + +. $PSScriptRoot\tools.ps1 + +try { + $dotnetRoot = Join-Path $RepoRoot ".dotnet" + InstallDotNet $dotnetRoot $version $architecture $runtime $true +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} + +ExitWithExitCode 0 \ No newline at end of file diff --git a/eng/common/dotnet-install.sh b/eng/common/dotnet-install.sh new file mode 100755 index 0000000000..c3072c958a --- /dev/null +++ b/eng/common/dotnet-install.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +version='Latest' +architecture='' +runtime='dotnet' +while [[ $# > 0 ]]; do + opt="$(echo "$1" | awk '{print tolower($0)}')" + case "$opt" in + -version|-v) + shift + version="$1" + ;; + -architecture|-a) + shift + architecture="$1" + ;; + -runtime|-r) + shift + runtime="$1" + ;; + *) + echo "Invalid argument: $1" + usage + exit 1 + ;; + esac + shift +done + +. "$scriptroot/tools.sh" +dotnetRoot="$repo_root/.dotnet" +InstallDotNet $dotnetRoot $version "$architecture" $runtime true || { + local exit_code=$? + echo "dotnet-install.sh failed (exit code '$exit_code')." >&2 + ExitWithExitCode $exit_code +} + +ExitWithExitCode 0 diff --git a/eng/common/generate-graph-files.ps1 b/eng/common/generate-graph-files.ps1 new file mode 100644 index 0000000000..a05b84f798 --- /dev/null +++ b/eng/common/generate-graph-files.ps1 @@ -0,0 +1,87 @@ +Param( + [Parameter(Mandatory=$true)][string] $barToken, # Token generated at https://maestro-prod.westus2.cloudapp.azure.com/Account/Tokens + [Parameter(Mandatory=$true)][string] $gitHubPat, # GitHub personal access token from https://github.com/settings/tokens (no auth scopes needed) + [Parameter(Mandatory=$true)][string] $azdoPat, # Azure Dev Ops tokens from https://dev.azure.com/dnceng/_details/security/tokens (code read scope needed) + [Parameter(Mandatory=$true)][string] $outputFolder, # Where the graphviz.txt file will be created + [string] $darcVersion = '1.1.0-beta.19175.6', # darc's version + [string] $graphvizVersion = '2.38', # GraphViz version + [switch] $includeToolset # Whether the graph should include toolset dependencies or not. i.e. arcade, optimization. For more about + # toolset dependencies see https://github.com/dotnet/arcade/blob/master/Documentation/Darc.md#toolset-vs-product-dependencies +) + +$ErrorActionPreference = "Stop" +. $PSScriptRoot\tools.ps1 + +Import-Module -Name (Join-Path $PSScriptRoot "native\CommonLibrary.psm1") + +function CheckExitCode ([string]$stage) +{ + $exitCode = $LASTEXITCODE + if ($exitCode -ne 0) { + Write-Host "Something failed in stage: '$stage'. Check for errors above. Exiting now..." + ExitWithExitCode $exitCode + } +} + +try { + Push-Location $PSScriptRoot + + Write-Host "Installing darc..." + . .\darc-init.ps1 -darcVersion $darcVersion + CheckExitCode "Running darc-init" + + $engCommonBaseDir = Join-Path $PSScriptRoot "native\" + $graphvizInstallDir = CommonLibrary\Get-NativeInstallDirectory + $nativeToolBaseUri = "https://netcorenativeassets.blob.core.windows.net/resource-packages/external" + $installBin = Join-Path $graphvizInstallDir "bin" + + Write-Host "Installing dot..." + .\native\install-tool.ps1 -ToolName graphviz -InstallPath $installBin -BaseUri $nativeToolBaseUri -CommonLibraryDirectory $engCommonBaseDir -Version $graphvizVersion -Verbose + + $darcExe = "$env:USERPROFILE\.dotnet\tools" + $darcExe = Resolve-Path "$darcExe\darc.exe" + + Create-Directory $outputFolder + + # Generate 3 graph descriptions: + # 1. Flat with coherency information + # 2. Graphviz (dot) file + # 3. Standard dependency graph + $graphVizFilePath = "$outputFolder\graphviz.txt" + $graphVizImageFilePath = "$outputFolder\graph.png" + $normalGraphFilePath = "$outputFolder\graph-full.txt" + $flatGraphFilePath = "$outputFolder\graph-flat.txt" + $baseOptions = "get-dependency-graph --github-pat $gitHubPat --azdev-pat $azdoPat --password $barToken" + + if ($includeToolset) { + Write-Host "Toolsets will be included in the graph..." + $baseOptions += " --include-toolset" + } + + Write-Host "Generating standard dependency graph..." + Invoke-Expression "& `"$darcExe`" $baseOptions --output-file $normalGraphFilePath" + CheckExitCode "Generating normal dependency graph" + + Write-Host "Generating flat dependency graph and graphviz file..." + Invoke-Expression "& `"$darcExe`" $baseOptions --flat --coherency --graphviz $graphVizFilePath --output-file $flatGraphFilePath" + CheckExitCode "Generating flat and graphviz dependency graph" + + Write-Host "Generating graph image $graphVizFilePath" + $dotFilePath = Join-Path $installBin "graphviz\$graphvizVersion\release\bin\dot.exe" + Invoke-Expression "& `"$dotFilePath`" -Tpng -o'$graphVizImageFilePath' `"$graphVizFilePath`"" + CheckExitCode "Generating graphviz image" + + Write-Host "'$graphVizFilePath', '$flatGraphFilePath', '$normalGraphFilePath' and '$graphVizImageFilePath' created!" +} +catch { + if (!$includeToolset) { + Write-Host "This might be a toolset repo which includes only toolset dependencies. " -NoNewline -ForegroundColor Yellow + Write-Host "Since -includeToolset is not set there is no graph to create. Include -includeToolset and try again..." -ForegroundColor Yellow + } + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} finally { + Pop-Location +} \ No newline at end of file diff --git a/eng/common/helixpublish.proj b/eng/common/helixpublish.proj new file mode 100644 index 0000000000..d7f185856e --- /dev/null +++ b/eng/common/helixpublish.proj @@ -0,0 +1,26 @@ + + + + msbuild + + + + + %(Identity) + + + + + + $(WorkItemDirectory) + $(WorkItemCommand) + $(WorkItemTimeout) + + + + + + + + + diff --git a/eng/common/init-tools-native.cmd b/eng/common/init-tools-native.cmd new file mode 100644 index 0000000000..438cd548c4 --- /dev/null +++ b/eng/common/init-tools-native.cmd @@ -0,0 +1,3 @@ +@echo off +powershell -NoProfile -NoLogo -ExecutionPolicy ByPass -command "& """%~dp0init-tools-native.ps1""" %*" +exit /b %ErrorLevel% \ No newline at end of file diff --git a/eng/common/init-tools-native.ps1 b/eng/common/init-tools-native.ps1 new file mode 100644 index 0000000000..a4306bd37e --- /dev/null +++ b/eng/common/init-tools-native.ps1 @@ -0,0 +1,142 @@ +<# +.SYNOPSIS +Entry point script for installing native tools + +.DESCRIPTION +Reads $RepoRoot\global.json file to determine native assets to install +and executes installers for those tools + +.PARAMETER BaseUri +Base file directory or Url from which to acquire tool archives + +.PARAMETER InstallDirectory +Directory to install native toolset. This is a command-line override for the default +Install directory precedence order: +- InstallDirectory command-line override +- NETCOREENG_INSTALL_DIRECTORY environment variable +- (default) %USERPROFILE%/.netcoreeng/native + +.PARAMETER Clean +Switch specifying to not install anything, but cleanup native asset folders + +.PARAMETER Force +Clean and then install tools + +.PARAMETER DownloadRetries +Total number of retry attempts + +.PARAMETER RetryWaitTimeInSeconds +Wait time between retry attempts in seconds + +.PARAMETER GlobalJsonFile +File path to global.json file + +.NOTES +#> +[CmdletBinding(PositionalBinding=$false)] +Param ( + [string] $BaseUri = "https://netcorenativeassets.blob.core.windows.net/resource-packages/external", + [string] $InstallDirectory, + [switch] $Clean = $False, + [switch] $Force = $False, + [int] $DownloadRetries = 5, + [int] $RetryWaitTimeInSeconds = 30, + [string] $GlobalJsonFile +) + +if (!$GlobalJsonFile) { + $GlobalJsonFile = Join-Path (Get-Item $PSScriptRoot).Parent.Parent.FullName "global.json" +} + +Set-StrictMode -version 2.0 +$ErrorActionPreference="Stop" + +Import-Module -Name (Join-Path $PSScriptRoot "native\CommonLibrary.psm1") + +try { + # Define verbose switch if undefined + $Verbose = $VerbosePreference -Eq "Continue" + + $EngCommonBaseDir = Join-Path $PSScriptRoot "native\" + $NativeBaseDir = $InstallDirectory + if (!$NativeBaseDir) { + $NativeBaseDir = CommonLibrary\Get-NativeInstallDirectory + } + $Env:CommonLibrary_NativeInstallDir = $NativeBaseDir + $InstallBin = Join-Path $NativeBaseDir "bin" + $InstallerPath = Join-Path $EngCommonBaseDir "install-tool.ps1" + + # Process tools list + Write-Host "Processing $GlobalJsonFile" + If (-Not (Test-Path $GlobalJsonFile)) { + Write-Host "Unable to find '$GlobalJsonFile'" + exit 0 + } + $NativeTools = Get-Content($GlobalJsonFile) -Raw | + ConvertFrom-Json | + Select-Object -Expand "native-tools" -ErrorAction SilentlyContinue + if ($NativeTools) { + $NativeTools.PSObject.Properties | ForEach-Object { + $ToolName = $_.Name + $ToolVersion = $_.Value + $LocalInstallerCommand = $InstallerPath + $LocalInstallerCommand += " -ToolName $ToolName" + $LocalInstallerCommand += " -InstallPath $InstallBin" + $LocalInstallerCommand += " -BaseUri $BaseUri" + $LocalInstallerCommand += " -CommonLibraryDirectory $EngCommonBaseDir" + $LocalInstallerCommand += " -Version $ToolVersion" + + if ($Verbose) { + $LocalInstallerCommand += " -Verbose" + } + if (Get-Variable 'Force' -ErrorAction 'SilentlyContinue') { + if($Force) { + $LocalInstallerCommand += " -Force" + } + } + if ($Clean) { + $LocalInstallerCommand += " -Clean" + } + + Write-Verbose "Installing $ToolName version $ToolVersion" + Write-Verbose "Executing '$LocalInstallerCommand'" + Invoke-Expression "$LocalInstallerCommand" + if ($LASTEXITCODE -Ne "0") { + $errMsg = "$ToolName installation failed" + if ((Get-Variable 'DoNotAbortNativeToolsInstallationOnFailure' -ErrorAction 'SilentlyContinue') -and $DoNotAbortNativeToolsInstallationOnFailure) { + Write-Warning $errMsg + $toolInstallationFailure = $true + } else { + Write-Error $errMsg + exit 1 + } + } + } + + if ((Get-Variable 'toolInstallationFailure' -ErrorAction 'SilentlyContinue') -and $toolInstallationFailure) { + exit 1 + } + } + else { + Write-Host "No native tools defined in global.json" + exit 0 + } + + if ($Clean) { + exit 0 + } + if (Test-Path $InstallBin) { + Write-Host "Native tools are available from" (Convert-Path -Path $InstallBin) + Write-Host "##vso[task.prependpath]$(Convert-Path -Path $InstallBin)" + } + else { + Write-Error "Native tools install directory does not exist, installation failed" + exit 1 + } + exit 0 +} +catch { + Write-Host $_ + Write-Host $_.Exception + exit 1 +} diff --git a/eng/common/init-tools-native.sh b/eng/common/init-tools-native.sh new file mode 100755 index 0000000000..fc72d13948 --- /dev/null +++ b/eng/common/init-tools-native.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +base_uri='https://netcorenativeassets.blob.core.windows.net/resource-packages/external' +install_directory='' +clean=false +force=false +download_retries=5 +retry_wait_time_seconds=30 +global_json_file="$(dirname "$(dirname "${scriptroot}")")/global.json" +declare -A native_assets + +. $scriptroot/native/common-library.sh + +while (($# > 0)); do + lowerI="$(echo $1 | awk '{print tolower($0)}')" + case $lowerI in + --baseuri) + base_uri=$2 + shift 2 + ;; + --installdirectory) + install_directory=$2 + shift 2 + ;; + --clean) + clean=true + shift 1 + ;; + --force) + force=true + shift 1 + ;; + --downloadretries) + download_retries=$2 + shift 2 + ;; + --retrywaittimeseconds) + retry_wait_time_seconds=$2 + shift 2 + ;; + --help) + echo "Common settings:" + echo " --installdirectory Directory to install native toolset." + echo " This is a command-line override for the default" + echo " Install directory precedence order:" + echo " - InstallDirectory command-line override" + echo " - NETCOREENG_INSTALL_DIRECTORY environment variable" + echo " - (default) %USERPROFILE%/.netcoreeng/native" + echo "" + echo " --clean Switch specifying not to install anything, but cleanup native asset folders" + echo " --force Clean and then install tools" + echo " --help Print help and exit" + echo "" + echo "Advanced settings:" + echo " --baseuri Base URI for where to download native tools from" + echo " --downloadretries Number of times a download should be attempted" + echo " --retrywaittimeseconds Wait time between download attempts" + echo "" + exit 0 + ;; + esac +done + +function ReadGlobalJsonNativeTools { + # Get the native-tools section from the global.json. + local native_tools_section=$(cat $global_json_file | awk '/"native-tools"/,/}/') + # Only extract the contents of the object. + local native_tools_list=$(echo $native_tools_section | awk -F"[{}]" '{print $2}') + native_tools_list=${native_tools_list//[\" ]/} + native_tools_list=${native_tools_list//,/$'\n'} + native_tools_list="$(echo -e "${native_tools_list}" | tr -d '[:space:]')" + + local old_IFS=$IFS + while read -r line; do + # Lines are of the form: 'tool:version' + IFS=: + while read -r key value; do + native_assets[$key]=$value + done <<< "$line" + done <<< "$native_tools_list" + IFS=$old_IFS + + return 0; +} + +native_base_dir=$install_directory +if [[ -z $install_directory ]]; then + native_base_dir=$(GetNativeInstallDirectory) +fi + +install_bin="${native_base_dir}/bin" + +ReadGlobalJsonNativeTools + +if [[ ${#native_assets[@]} -eq 0 ]]; then + echo "No native tools defined in global.json" + exit 0; +else + native_installer_dir="$scriptroot/native" + for tool in "${!native_assets[@]}" + do + tool_version=${native_assets[$tool]} + installer_name="install-$tool.sh" + installer_command="$native_installer_dir/$installer_name" + installer_command+=" --baseuri $base_uri" + installer_command+=" --installpath $install_bin" + installer_command+=" --version $tool_version" + + if [[ $force = true ]]; then + installer_command+=" --force" + fi + + if [[ $clean = true ]]; then + installer_command+=" --clean" + fi + + $installer_command + + if [[ $? != 0 ]]; then + echo "Execution Failed" >&2 + exit 1 + fi + done +fi + +if [[ $clean = true ]]; then + exit 0 +fi + +if [[ -d $install_bin ]]; then + echo "Native tools are available from $install_bin" + echo "##vso[task.prependpath]$install_bin" +else + echo "Native tools install directory does not exist, installation failed" >&2 + exit 1 +fi + +exit 0 diff --git a/eng/common/internal/Directory.Build.props b/eng/common/internal/Directory.Build.props new file mode 100644 index 0000000000..e33179ef37 --- /dev/null +++ b/eng/common/internal/Directory.Build.props @@ -0,0 +1,4 @@ + + + + diff --git a/eng/common/internal/Tools.csproj b/eng/common/internal/Tools.csproj new file mode 100644 index 0000000000..1a39a7ef3f --- /dev/null +++ b/eng/common/internal/Tools.csproj @@ -0,0 +1,27 @@ + + + + + net472 + false + + + + + + + + + + + https://devdiv.pkgs.visualstudio.com/_packaging/dotnet-core-internal-tooling/nuget/v3/index.json; + + + $(RestoreSources); + https://devdiv.pkgs.visualstudio.com/_packaging/VS/nuget/v3/index.json; + + + + + + diff --git a/eng/common/msbuild.ps1 b/eng/common/msbuild.ps1 new file mode 100644 index 0000000000..b37fd3d5e9 --- /dev/null +++ b/eng/common/msbuild.ps1 @@ -0,0 +1,27 @@ +[CmdletBinding(PositionalBinding=$false)] +Param( + [string] $verbosity = "minimal", + [bool] $warnAsError = $true, + [bool] $nodeReuse = $true, + [switch] $ci, + [switch] $prepareMachine, + [Parameter(ValueFromRemainingArguments=$true)][String[]]$extraArgs +) + +. $PSScriptRoot\tools.ps1 + +try { + if ($ci) { + $nodeReuse = $false + } + + MSBuild @extraArgs +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} + +ExitWithExitCode 0 \ No newline at end of file diff --git a/eng/common/msbuild.sh b/eng/common/msbuild.sh new file mode 100755 index 0000000000..8160cd5a59 --- /dev/null +++ b/eng/common/msbuild.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" + +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +verbosity='minimal' +warn_as_error=true +node_reuse=true +prepare_machine=false +extra_args='' + +while (($# > 0)); do + lowerI="$(echo $1 | awk '{print tolower($0)}')" + case $lowerI in + --verbosity) + verbosity=$2 + shift 2 + ;; + --warnaserror) + warn_as_error=$2 + shift 2 + ;; + --nodereuse) + node_reuse=$2 + shift 2 + ;; + --ci) + ci=true + shift 1 + ;; + --preparemachine) + prepare_machine=true + shift 1 + ;; + *) + extra_args="$extra_args $1" + shift 1 + ;; + esac +done + +. "$scriptroot/tools.sh" + +if [[ "$ci" == true ]]; then + node_reuse=false +fi + +MSBuild $extra_args +ExitWithExitCode 0 diff --git a/eng/common/native/CommonLibrary.psm1 b/eng/common/native/CommonLibrary.psm1 new file mode 100644 index 0000000000..f286ae0cde --- /dev/null +++ b/eng/common/native/CommonLibrary.psm1 @@ -0,0 +1,358 @@ +<# +.SYNOPSIS +Helper module to install an archive to a directory + +.DESCRIPTION +Helper module to download and extract an archive to a specified directory + +.PARAMETER Uri +Uri of artifact to download + +.PARAMETER InstallDirectory +Directory to extract artifact contents to + +.PARAMETER Force +Force download / extraction if file or contents already exist. Default = False + +.PARAMETER DownloadRetries +Total number of retry attempts. Default = 5 + +.PARAMETER RetryWaitTimeInSeconds +Wait time between retry attempts in seconds. Default = 30 + +.NOTES +Returns False if download or extraction fail, True otherwise +#> +function DownloadAndExtract { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $Uri, + [Parameter(Mandatory=$True)] + [string] $InstallDirectory, + [switch] $Force = $False, + [int] $DownloadRetries = 5, + [int] $RetryWaitTimeInSeconds = 30 + ) + # Define verbose switch if undefined + $Verbose = $VerbosePreference -Eq "Continue" + + $TempToolPath = CommonLibrary\Get-TempPathFilename -Path $Uri + + # Download native tool + $DownloadStatus = CommonLibrary\Get-File -Uri $Uri ` + -Path $TempToolPath ` + -DownloadRetries $DownloadRetries ` + -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds ` + -Force:$Force ` + -Verbose:$Verbose + + if ($DownloadStatus -Eq $False) { + Write-Error "Download failed" + return $False + } + + # Extract native tool + $UnzipStatus = CommonLibrary\Expand-Zip -ZipPath $TempToolPath ` + -OutputDirectory $InstallDirectory ` + -Force:$Force ` + -Verbose:$Verbose + + if ($UnzipStatus -Eq $False) { + Write-Error "Unzip failed" + return $False + } + return $True +} + +<# +.SYNOPSIS +Download a file, retry on failure + +.DESCRIPTION +Download specified file and retry if attempt fails + +.PARAMETER Uri +Uri of file to download. If Uri is a local path, the file will be copied instead of downloaded + +.PARAMETER Path +Path to download or copy uri file to + +.PARAMETER Force +Overwrite existing file if present. Default = False + +.PARAMETER DownloadRetries +Total number of retry attempts. Default = 5 + +.PARAMETER RetryWaitTimeInSeconds +Wait time between retry attempts in seconds Default = 30 + +#> +function Get-File { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $Uri, + [Parameter(Mandatory=$True)] + [string] $Path, + [int] $DownloadRetries = 5, + [int] $RetryWaitTimeInSeconds = 30, + [switch] $Force = $False + ) + $Attempt = 0 + + if ($Force) { + if (Test-Path $Path) { + Remove-Item $Path -Force + } + } + if (Test-Path $Path) { + Write-Host "File '$Path' already exists, skipping download" + return $True + } + + $DownloadDirectory = Split-Path -ErrorAction Ignore -Path "$Path" -Parent + if (-Not (Test-Path $DownloadDirectory)) { + New-Item -path $DownloadDirectory -force -itemType "Directory" | Out-Null + } + + if (Test-Path -IsValid -Path $Uri) { + Write-Verbose "'$Uri' is a file path, copying file to '$Path'" + Copy-Item -Path $Uri -Destination $Path + return $? + } + else { + Write-Verbose "Downloading $Uri" + while($Attempt -Lt $DownloadRetries) + { + try { + Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $Path + Write-Verbose "Downloaded to '$Path'" + return $True + } + catch { + $Attempt++ + if ($Attempt -Lt $DownloadRetries) { + $AttemptsLeft = $DownloadRetries - $Attempt + Write-Warning "Download failed, $AttemptsLeft attempts remaining, will retry in $RetryWaitTimeInSeconds seconds" + Start-Sleep -Seconds $RetryWaitTimeInSeconds + } + else { + Write-Error $_ + Write-Error $_.Exception + } + } + } + } + + return $False +} + +<# +.SYNOPSIS +Generate a shim for a native tool + +.DESCRIPTION +Creates a wrapper script (shim) that passes arguments forward to native tool assembly + +.PARAMETER ShimName +The name of the shim + +.PARAMETER ShimDirectory +The directory where shims are stored + +.PARAMETER ToolFilePath +Path to file that shim forwards to + +.PARAMETER Force +Replace shim if already present. Default = False + +.NOTES +Returns $True if generating shim succeeds, $False otherwise +#> +function New-ScriptShim { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $ShimName, + [Parameter(Mandatory=$True)] + [string] $ShimDirectory, + [Parameter(Mandatory=$True)] + [string] $ToolFilePath, + [Parameter(Mandatory=$True)] + [string] $BaseUri, + [switch] $Force + ) + try { + Write-Verbose "Generating '$ShimName' shim" + + if (-Not (Test-Path $ToolFilePath)){ + Write-Error "Specified tool file path '$ToolFilePath' does not exist" + return $False + } + + # WinShimmer is a small .NET Framework program that creates .exe shims to bootstrapped programs + # Many of the checks for installed programs expect a .exe extension for Windows tools, rather + # than a .bat or .cmd file. + # Source: https://github.com/dotnet/arcade/tree/master/src/WinShimmer + if (-Not (Test-Path "$ShimDirectory\WinShimmer\winshimmer.exe")) { + $InstallStatus = DownloadAndExtract -Uri "$BaseUri/windows/winshimmer/WinShimmer.zip" ` + -InstallDirectory $ShimDirectory\WinShimmer ` + -Force:$Force ` + -DownloadRetries 2 ` + -RetryWaitTimeInSeconds 5 ` + -Verbose:$Verbose + } + + if ((Test-Path (Join-Path $ShimDirectory "$ShimName.exe"))) { + Write-Host "$ShimName.exe already exists; replacing..." + Remove-Item (Join-Path $ShimDirectory "$ShimName.exe") + } + + Invoke-Expression "$ShimDirectory\WinShimmer\winshimmer.exe $ShimName $ToolFilePath $ShimDirectory" + return $True + } + catch { + Write-Host $_ + Write-Host $_.Exception + return $False + } +} + +<# +.SYNOPSIS +Returns the machine architecture of the host machine + +.NOTES +Returns 'x64' on 64 bit machines + Returns 'x86' on 32 bit machines +#> +function Get-MachineArchitecture { + $ProcessorArchitecture = $Env:PROCESSOR_ARCHITECTURE + $ProcessorArchitectureW6432 = $Env:PROCESSOR_ARCHITEW6432 + if($ProcessorArchitecture -Eq "X86") + { + if(($ProcessorArchitectureW6432 -Eq "") -Or + ($ProcessorArchitectureW6432 -Eq "X86")) { + return "x86" + } + $ProcessorArchitecture = $ProcessorArchitectureW6432 + } + if (($ProcessorArchitecture -Eq "AMD64") -Or + ($ProcessorArchitecture -Eq "IA64") -Or + ($ProcessorArchitecture -Eq "ARM64")) { + return "x64" + } + return "x86" +} + +<# +.SYNOPSIS +Get the name of a temporary folder under the native install directory +#> +function Get-TempDirectory { + return Join-Path (Get-NativeInstallDirectory) "temp/" +} + +function Get-TempPathFilename { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $Path + ) + $TempDir = CommonLibrary\Get-TempDirectory + $TempFilename = Split-Path $Path -leaf + $TempPath = Join-Path $TempDir $TempFilename + return $TempPath +} + +<# +.SYNOPSIS +Returns the base directory to use for native tool installation + +.NOTES +Returns the value of the NETCOREENG_INSTALL_DIRECTORY if that environment variable +is set, or otherwise returns an install directory under the %USERPROFILE% +#> +function Get-NativeInstallDirectory { + $InstallDir = $Env:NETCOREENG_INSTALL_DIRECTORY + if (!$InstallDir) { + $InstallDir = Join-Path $Env:USERPROFILE ".netcoreeng/native/" + } + return $InstallDir +} + +<# +.SYNOPSIS +Unzip an archive + +.DESCRIPTION +Powershell module to unzip an archive to a specified directory + +.PARAMETER ZipPath (Required) +Path to archive to unzip + +.PARAMETER OutputDirectory (Required) +Output directory for archive contents + +.PARAMETER Force +Overwrite output directory contents if they already exist + +.NOTES +- Returns True and does not perform an extraction if output directory already exists but Overwrite is not True. +- Returns True if unzip operation is successful +- Returns False if Overwrite is True and it is unable to remove contents of OutputDirectory +- Returns False if unable to extract zip archive +#> +function Expand-Zip { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $ZipPath, + [Parameter(Mandatory=$True)] + [string] $OutputDirectory, + [switch] $Force + ) + + Write-Verbose "Extracting '$ZipPath' to '$OutputDirectory'" + try { + if ((Test-Path $OutputDirectory) -And (-Not $Force)) { + Write-Host "Directory '$OutputDirectory' already exists, skipping extract" + return $True + } + if (Test-Path $OutputDirectory) { + Write-Verbose "'Force' is 'True', but '$OutputDirectory' exists, removing directory" + Remove-Item $OutputDirectory -Force -Recurse + if ($? -Eq $False) { + Write-Error "Unable to remove '$OutputDirectory'" + return $False + } + } + if (-Not (Test-Path $OutputDirectory)) { + New-Item -path $OutputDirectory -Force -itemType "Directory" | Out-Null + } + + Add-Type -assembly "system.io.compression.filesystem" + [io.compression.zipfile]::ExtractToDirectory("$ZipPath", "$OutputDirectory") + if ($? -Eq $False) { + Write-Error "Unable to extract '$ZipPath'" + return $False + } + } + catch { + Write-Host $_ + Write-Host $_.Exception + + return $False + } + return $True +} + +export-modulemember -function DownloadAndExtract +export-modulemember -function Expand-Zip +export-modulemember -function Get-File +export-modulemember -function Get-MachineArchitecture +export-modulemember -function Get-NativeInstallDirectory +export-modulemember -function Get-TempDirectory +export-modulemember -function Get-TempPathFilename +export-modulemember -function New-ScriptShim diff --git a/eng/common/native/common-library.sh b/eng/common/native/common-library.sh new file mode 100755 index 0000000000..271bddfac5 --- /dev/null +++ b/eng/common/native/common-library.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash + +function GetNativeInstallDirectory { + local install_dir + + if [[ -z $NETCOREENG_INSTALL_DIRECTORY ]]; then + install_dir=$HOME/.netcoreeng/native/ + else + install_dir=$NETCOREENG_INSTALL_DIRECTORY + fi + + echo $install_dir + return 0 +} + +function GetTempDirectory { + + echo $(GetNativeInstallDirectory)temp/ + return 0 +} + +function ExpandZip { + local zip_path=$1 + local output_directory=$2 + local force=${3:-false} + + echo "Extracting $zip_path to $output_directory" + if [[ -d $output_directory ]] && [[ $force = false ]]; then + echo "Directory '$output_directory' already exists, skipping extract" + return 0 + fi + + if [[ -d $output_directory ]]; then + echo "'Force flag enabled, but '$output_directory' exists. Removing directory" + rm -rf $output_directory + if [[ $? != 0 ]]; then + echo Unable to remove '$output_directory'>&2 + return 1 + fi + fi + + echo "Creating directory: '$output_directory'" + mkdir -p $output_directory + + echo "Extracting archive" + tar -xf $zip_path -C $output_directory + if [[ $? != 0 ]]; then + echo "Unable to extract '$zip_path'" >&2 + return 1 + fi + + return 0 +} + +function GetCurrentOS { + local unameOut="$(uname -s)" + case $unameOut in + Linux*) echo "Linux";; + Darwin*) echo "MacOS";; + esac + return 0 +} + +function GetFile { + local uri=$1 + local path=$2 + local force=${3:-false} + local download_retries=${4:-5} + local retry_wait_time_seconds=${5:-30} + + if [[ -f $path ]]; then + if [[ $force = false ]]; then + echo "File '$path' already exists. Skipping download" + return 0 + else + rm -rf $path + fi + fi + + if [[ -f $uri ]]; then + echo "'$uri' is a file path, copying file to '$path'" + cp $uri $path + return $? + fi + + echo "Downloading $uri" + # Use curl if available, otherwise use wget + if command -v curl > /dev/null; then + curl "$uri" -sSL --retry $download_retries --retry-delay $retry_wait_time_seconds --create-dirs -o "$path" --fail + else + wget -q -O "$path" "$uri" --tries="$download_retries" + fi + + return $? +} + +function GetTempPathFileName { + local path=$1 + + local temp_dir=$(GetTempDirectory) + local temp_file_name=$(basename $path) + echo $temp_dir$temp_file_name + return 0 +} + +function DownloadAndExtract { + local uri=$1 + local installDir=$2 + local force=${3:-false} + local download_retries=${4:-5} + local retry_wait_time_seconds=${5:-30} + + local temp_tool_path=$(GetTempPathFileName $uri) + + echo "downloading to: $temp_tool_path" + + # Download file + GetFile "$uri" "$temp_tool_path" $force $download_retries $retry_wait_time_seconds + if [[ $? != 0 ]]; then + echo "Failed to download '$uri' to '$temp_tool_path'." >&2 + return 1 + fi + + # Extract File + echo "extracting from $temp_tool_path to $installDir" + ExpandZip "$temp_tool_path" "$installDir" $force $download_retries $retry_wait_time_seconds + if [[ $? != 0 ]]; then + echo "Failed to extract '$temp_tool_path' to '$installDir'." >&2 + return 1 + fi + + return 0 +} + +function NewScriptShim { + local shimpath=$1 + local tool_file_path=$2 + local force=${3:-false} + + echo "Generating '$shimpath' shim" + if [[ -f $shimpath ]]; then + if [[ $force = false ]]; then + echo "File '$shimpath' already exists." >&2 + return 1 + else + rm -rf $shimpath + fi + fi + + if [[ ! -f $tool_file_path ]]; then + echo "Specified tool file path:'$tool_file_path' does not exist" >&2 + return 1 + fi + + local shim_contents=$'#!/usr/bin/env bash\n' + shim_contents+="SHIMARGS="$'$1\n' + shim_contents+="$tool_file_path"$' $SHIMARGS\n' + + # Write shim file + echo "$shim_contents" > $shimpath + + chmod +x $shimpath + + echo "Finished generating shim '$shimpath'" + + return $? +} + diff --git a/eng/common/native/install-cmake.sh b/eng/common/native/install-cmake.sh new file mode 100755 index 0000000000..293af6017d --- /dev/null +++ b/eng/common/native/install-cmake.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. $scriptroot/common-library.sh + +base_uri= +install_path= +version= +clean=false +force=false +download_retries=5 +retry_wait_time_seconds=30 + +while (($# > 0)); do + lowerI="$(echo $1 | awk '{print tolower($0)}')" + case $lowerI in + --baseuri) + base_uri=$2 + shift 2 + ;; + --installpath) + install_path=$2 + shift 2 + ;; + --version) + version=$2 + shift 2 + ;; + --clean) + clean=true + shift 1 + ;; + --force) + force=true + shift 1 + ;; + --downloadretries) + download_retries=$2 + shift 2 + ;; + --retrywaittimeseconds) + retry_wait_time_seconds=$2 + shift 2 + ;; + --help) + echo "Common settings:" + echo " --baseuri Base file directory or Url wrom which to acquire tool archives" + echo " --installpath Base directory to install native tool to" + echo " --clean Don't install the tool, just clean up the current install of the tool" + echo " --force Force install of tools even if they previously exist" + echo " --help Print help and exit" + echo "" + echo "Advanced settings:" + echo " --downloadretries Total number of retry attempts" + echo " --retrywaittimeseconds Wait time between retry attempts in seconds" + echo "" + exit 0 + ;; + esac +done + +tool_name="cmake" +tool_os=$(GetCurrentOS) +tool_folder=$(echo $tool_os | awk '{print tolower($0)}') +tool_arch="x86_64" +tool_name_moniker="$tool_name-$version-$tool_os-$tool_arch" +tool_install_directory="$install_path/$tool_name/$version" +tool_file_path="$tool_install_directory/$tool_name_moniker/bin/$tool_name" +shim_path="$install_path/$tool_name.sh" +uri="${base_uri}/$tool_folder/cmake/$tool_name_moniker.tar.gz" + +# Clean up tool and installers +if [[ $clean = true ]]; then + echo "Cleaning $tool_install_directory" + if [[ -d $tool_install_directory ]]; then + rm -rf $tool_install_directory + fi + + echo "Cleaning $shim_path" + if [[ -f $shim_path ]]; then + rm -rf $shim_path + fi + + tool_temp_path=$(GetTempPathFileName $uri) + echo "Cleaning $tool_temp_path" + if [[ -f $tool_temp_path ]]; then + rm -rf $tool_temp_path + fi + + exit 0 +fi + +# Install tool +if [[ -f $tool_file_path ]] && [[ $force = false ]]; then + echo "$tool_name ($version) already exists, skipping install" + exit 0 +fi + +DownloadAndExtract $uri $tool_install_directory $force $download_retries $retry_wait_time_seconds + +if [[ $? != 0 ]]; then + echo "Installation failed" >&2 + exit 1 +fi + +# Generate Shim +# Always rewrite shims so that we are referencing the expected version +NewScriptShim $shim_path $tool_file_path true + +if [[ $? != 0 ]]; then + echo "Shim generation failed" >&2 + exit 1 +fi + +exit 0 \ No newline at end of file diff --git a/eng/common/native/install-tool.ps1 b/eng/common/native/install-tool.ps1 new file mode 100644 index 0000000000..635ab3fd41 --- /dev/null +++ b/eng/common/native/install-tool.ps1 @@ -0,0 +1,130 @@ +<# +.SYNOPSIS +Install native tool + +.DESCRIPTION +Install cmake native tool from Azure blob storage + +.PARAMETER InstallPath +Base directory to install native tool to + +.PARAMETER BaseUri +Base file directory or Url from which to acquire tool archives + +.PARAMETER CommonLibraryDirectory +Path to folder containing common library modules + +.PARAMETER Force +Force install of tools even if they previously exist + +.PARAMETER Clean +Don't install the tool, just clean up the current install of the tool + +.PARAMETER DownloadRetries +Total number of retry attempts + +.PARAMETER RetryWaitTimeInSeconds +Wait time between retry attempts in seconds + +.NOTES +Returns 0 if install succeeds, 1 otherwise +#> +[CmdletBinding(PositionalBinding=$false)] +Param ( + [Parameter(Mandatory=$True)] + [string] $ToolName, + [Parameter(Mandatory=$True)] + [string] $InstallPath, + [Parameter(Mandatory=$True)] + [string] $BaseUri, + [Parameter(Mandatory=$True)] + [string] $Version, + [string] $CommonLibraryDirectory = $PSScriptRoot, + [switch] $Force = $False, + [switch] $Clean = $False, + [int] $DownloadRetries = 5, + [int] $RetryWaitTimeInSeconds = 30 +) + +# Import common library modules +Import-Module -Name (Join-Path $CommonLibraryDirectory "CommonLibrary.psm1") + +try { + # Define verbose switch if undefined + $Verbose = $VerbosePreference -Eq "Continue" + + $Arch = CommonLibrary\Get-MachineArchitecture + $ToolOs = "win64" + if($Arch -Eq "x32") { + $ToolOs = "win32" + } + $ToolNameMoniker = "$ToolName-$Version-$ToolOs-$Arch" + $ToolInstallDirectory = Join-Path $InstallPath "$ToolName\$Version\" + $Uri = "$BaseUri/windows/$ToolName/$ToolNameMoniker.zip" + $ShimPath = Join-Path $InstallPath "$ToolName.exe" + + if ($Clean) { + Write-Host "Cleaning $ToolInstallDirectory" + if (Test-Path $ToolInstallDirectory) { + Remove-Item $ToolInstallDirectory -Force -Recurse + } + Write-Host "Cleaning $ShimPath" + if (Test-Path $ShimPath) { + Remove-Item $ShimPath -Force + } + $ToolTempPath = CommonLibrary\Get-TempPathFilename -Path $Uri + Write-Host "Cleaning $ToolTempPath" + if (Test-Path $ToolTempPath) { + Remove-Item $ToolTempPath -Force + } + exit 0 + } + + # Install tool + if ((Test-Path $ToolInstallDirectory) -And (-Not $Force)) { + Write-Verbose "$ToolName ($Version) already exists, skipping install" + } + else { + $InstallStatus = CommonLibrary\DownloadAndExtract -Uri $Uri ` + -InstallDirectory $ToolInstallDirectory ` + -Force:$Force ` + -DownloadRetries $DownloadRetries ` + -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds ` + -Verbose:$Verbose + + if ($InstallStatus -Eq $False) { + Write-Error "Installation failed" + exit 1 + } + } + + $ToolFilePath = Get-ChildItem $ToolInstallDirectory -Recurse -Filter "$ToolName.exe" | % { $_.FullName } + if (@($ToolFilePath).Length -Gt 1) { + Write-Error "There are multiple copies of $ToolName in $($ToolInstallDirectory): `n$(@($ToolFilePath | out-string))" + exit 1 + } elseif (@($ToolFilePath).Length -Lt 1) { + Write-Error "$ToolName was not found in $ToolFilePath." + exit 1 + } + + # Generate shim + # Always rewrite shims so that we are referencing the expected version + $GenerateShimStatus = CommonLibrary\New-ScriptShim -ShimName $ToolName ` + -ShimDirectory $InstallPath ` + -ToolFilePath "$ToolFilePath" ` + -BaseUri $BaseUri ` + -Force:$Force ` + -Verbose:$Verbose + + if ($GenerateShimStatus -Eq $False) { + Write-Error "Generate shim failed" + return 1 + } + + exit 0 +} +catch { + Write-Host $_ + Write-Host $_.Exception + exit 1 +} diff --git a/eng/common/sdk-task.ps1 b/eng/common/sdk-task.ps1 new file mode 100644 index 0000000000..d0eec5163e --- /dev/null +++ b/eng/common/sdk-task.ps1 @@ -0,0 +1,79 @@ +[CmdletBinding(PositionalBinding=$false)] +Param( + [string] $configuration = "Debug", + [string] $task, + [string] $verbosity = "minimal", + [string] $msbuildEngine = $null, + [switch] $restore, + [switch] $prepareMachine, + [switch] $help, + [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties +) + +$ci = $true +$binaryLog = $true +$warnAsError = $true + +. $PSScriptRoot\tools.ps1 + +function Print-Usage() { + Write-Host "Common settings:" + Write-Host " -task Name of Arcade task (name of a project in SdkTasks directory of the Arcade SDK package)" + Write-Host " -restore Restore dependencies" + Write-Host " -verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]" + Write-Host " -help Print help and exit" + Write-Host "" + + Write-Host "Advanced settings:" + Write-Host " -prepareMachine Prepare machine for CI run" + Write-Host " -msbuildEngine Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)." + Write-Host "" + Write-Host "Command line arguments not listed above are passed thru to msbuild." +} + +function Build([string]$target) { + $logSuffix = if ($target -eq "Execute") { "" } else { ".$target" } + $log = Join-Path $LogDir "$task$logSuffix.binlog" + $outputPath = Join-Path $ToolsetDir "$task\\" + + MSBuild $taskProject ` + /bl:$log ` + /t:$target ` + /p:Configuration=$configuration ` + /p:RepoRoot=$RepoRoot ` + /p:BaseIntermediateOutputPath=$outputPath ` + @properties +} + +try { + if ($help -or (($null -ne $properties) -and ($properties.Contains("/help") -or $properties.Contains("/?")))) { + Print-Usage + exit 0 + } + + if ($task -eq "") { + Write-Host "Missing required parameter '-task '" -ForegroundColor Red + Print-Usage + ExitWithExitCode 1 + } + + $taskProject = GetSdkTaskProject $task + if (!(Test-Path $taskProject)) { + Write-Host "Unknown task: $task" -ForegroundColor Red + ExitWithExitCode 1 + } + + if ($restore) { + Build "Restore" + } + + Build "Execute" +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} + +ExitWithExitCode 0 diff --git a/eng/common/templates/job/generate-graph-files.yml b/eng/common/templates/job/generate-graph-files.yml new file mode 100644 index 0000000000..e54ce956f9 --- /dev/null +++ b/eng/common/templates/job/generate-graph-files.yml @@ -0,0 +1,48 @@ +parameters: + # Optional: dependencies of the job + dependsOn: '' + + # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool + pool: {} + + # Optional: Include toolset dependencies in the generated graph files + includeToolset: false + +jobs: +- job: Generate_Graph_Files + + dependsOn: ${{ parameters.dependsOn }} + + displayName: Generate Graph Files + + pool: ${{ parameters.pool }} + + variables: + # Publish-Build-Assets provides: MaestroAccessToken, BotAccount-dotnet-maestro-bot-PAT + # DotNet-AllOrgs-Darc-Pats provides: dn-bot-devdiv-dnceng-rw-code-pat + - group: Publish-Build-Assets + - group: DotNet-AllOrgs-Darc-Pats + - name: _GraphArguments + value: -gitHubPat $(BotAccount-dotnet-maestro-bot-PAT) + -azdoPat $(dn-bot-devdiv-dnceng-rw-code-pat) + -barToken $(MaestroAccessToken) + -outputFolder '$(Build.StagingDirectory)/GraphFiles/' + - ${{ if ne(parameters.includeToolset, 'false') }}: + - name: _GraphArguments + value: ${{ variables._GraphArguments }} -includeToolset + + steps: + - task: PowerShell@2 + displayName: Generate Graph Files + inputs: + filePath: eng\common\generate-graph-files.ps1 + arguments: $(_GraphArguments) + continueOnError: true + - task: PublishBuildArtifacts@1 + displayName: Publish Graph to Artifacts + inputs: + PathtoPublish: '$(Build.StagingDirectory)/GraphFiles' + PublishLocation: Container + ArtifactName: GraphFiles + continueOnError: true + condition: always() diff --git a/eng/common/templates/job/job.yml b/eng/common/templates/job/job.yml new file mode 100644 index 0000000000..1814e0ab61 --- /dev/null +++ b/eng/common/templates/job/job.yml @@ -0,0 +1,205 @@ +parameters: +# Job schema parameters - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job + cancelTimeoutInMinutes: '' + + condition: '' + + continueOnError: false + + container: '' + + dependsOn: '' + + displayName: '' + + steps: [] + + pool: '' + + strategy: '' + + timeoutInMinutes: '' + + variables: [] + + workspace: '' + +# Job base template specific parameters + # Optional: Enable installing Microbuild plugin + # if 'true', these "variables" must be specified in the variables object or as part of the queue matrix + # _TeamName - the name of your team + # _SignType - 'test' or 'real' + enableMicrobuild: false + + # Optional: Include PublishBuildArtifacts task + enablePublishBuildArtifacts: false + + # Optional: Enable publishing to the build asset registry + enablePublishBuildAssets: false + + # Optional: Include PublishTestResults task + enablePublishTestResults: false + + # Optional: enable sending telemetry + enableTelemetry: false + + # Optional: define the helix repo for telemetry (example: 'dotnet/arcade') + helixRepo: '' + + # Optional: define the helix type for telemetry (example: 'build/product/') + helixType: '' + + # Required: name of the job + name: '' + + # Optional: should run as a public build even in the internal project + # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. + runAsPublic: false + +# Internal resources (telemetry, microbuild) can only be accessed from non-public projects, +# and some (Microbuild) should only be applied to non-PR cases for internal builds. + +jobs: +- job: ${{ parameters.name }} + + ${{ if ne(parameters.cancelTimeoutInMinutes, '') }}: + cancelTimeoutInMinutes: ${{ parameters.cancelTimeoutInMinutes }} + + ${{ if ne(parameters.condition, '') }}: + condition: ${{ parameters.condition }} + + ${{ if ne(parameters.container, '') }}: + container: ${{ parameters.container }} + + ${{ if ne(parameters.continueOnError, '') }}: + continueOnError: ${{ parameters.continueOnError }} + + ${{ if ne(parameters.dependsOn, '') }}: + dependsOn: ${{ parameters.dependsOn }} + + ${{ if ne(parameters.displayName, '') }}: + displayName: ${{ parameters.displayName }} + + ${{ if ne(parameters.pool, '') }}: + pool: ${{ parameters.pool }} + + ${{ if ne(parameters.strategy, '') }}: + strategy: ${{ parameters.strategy }} + + ${{ if ne(parameters.timeoutInMinutes, '') }}: + timeoutInMinutes: ${{ parameters.timeoutInMinutes }} + + variables: + - ${{ if eq(parameters.enableTelemetry, 'true') }}: + - name: DOTNET_CLI_TELEMETRY_PROFILE + value: '$(Build.Repository.Uri)' + - ${{ each variable in parameters.variables }}: + # handle name-value variable syntax + # example: + # - name: [key] + # value: [value] + - ${{ if ne(variable.name, '') }}: + - name: ${{ variable.name }} + value: ${{ variable.value }} + + # handle variable groups + - ${{ if ne(variable.group, '') }}: + - group: ${{ variable.group }} + + # handle key-value variable syntax. + # example: + # - [key]: [value] + - ${{ if and(eq(variable.name, ''), eq(variable.group, '')) }}: + - ${{ each pair in variable }}: + - name: ${{ pair.key }} + value: ${{ pair.value }} + + # DotNet-HelixApi-Access provides 'HelixApiAccessToken' for internal builds + - ${{ if and(eq(parameters.enableTelemetry, 'true'), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - group: DotNet-HelixApi-Access + + ${{ if ne(parameters.workspace, '') }}: + workspace: ${{ parameters.workspace }} + + steps: + - ${{ if eq(parameters.enableTelemetry, 'true') }}: + # Telemetry tasks are built from https://github.com/dotnet/arcade-extensions + - task: sendStartTelemetry@0 + displayName: 'Send Helix Start Telemetry' + inputs: + helixRepo: ${{ parameters.helixRepo }} + ${{ if ne(parameters.helixType, '') }}: + helixType: ${{ parameters.helixType }} + buildConfig: $(_BuildConfig) + runAsPublic: ${{ parameters.runAsPublic }} + continueOnError: ${{ parameters.continueOnError }} + condition: always() + + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: MicroBuildSigningPlugin@2 + displayName: Install MicroBuild plugin + inputs: + signType: $(_SignType) + zipSources: false + feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + env: + TeamName: $(_TeamName) + continueOnError: ${{ parameters.continueOnError }} + condition: and(succeeded(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) + + - ${{ each step in parameters.steps }}: + - ${{ step }} + + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: MicroBuildCleanup@1 + displayName: Execute Microbuild cleanup tasks + condition: and(always(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} + env: + TeamName: $(_TeamName) + + - ${{ if eq(parameters.enableTelemetry, 'true') }}: + # Telemetry tasks are built from https://github.com/dotnet/arcade-extensions + - task: sendEndTelemetry@0 + displayName: 'Send Helix End Telemetry' + continueOnError: ${{ parameters.continueOnError }} + condition: always() + + - ${{ if eq(parameters.enablePublishBuildArtifacts, 'true') }}: + - task: PublishBuildArtifacts@1 + displayName: Publish Logs + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)' + PublishLocation: Container + ArtifactName: $(Agent.Os)_$(Agent.JobName) + continueOnError: true + condition: always() + + - ${{ if eq(parameters.enablePublishTestResults, 'true') }}: + - task: PublishTestResults@2 + displayName: Publish Test Results + inputs: + testResultsFormat: 'xUnit' + testResultsFiles: '*.xml' + searchFolder: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)' + continueOnError: true + condition: always() + + - ${{ if and(eq(parameters.enablePublishBuildAssets, true), ne(variables['_PublishUsingPipelines'], 'true'), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: CopyFiles@2 + displayName: Gather Asset Manifests + inputs: + SourceFolder: '$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/AssetManifest' + TargetFolder: '$(Build.StagingDirectory)/AssetManifests' + continueOnError: ${{ parameters.continueOnError }} + condition: and(succeeded(), eq(variables['_DotNetPublishToBlobFeed'], 'true')) + - task: PublishBuildArtifacts@1 + displayName: Push Asset Manifests + inputs: + PathtoPublish: '$(Build.StagingDirectory)/AssetManifests' + PublishLocation: Container + ArtifactName: AssetManifests + continueOnError: ${{ parameters.continueOnError }} + condition: and(succeeded(), eq(variables['_DotNetPublishToBlobFeed'], 'true')) diff --git a/eng/common/templates/job/publish-build-assets.yml b/eng/common/templates/job/publish-build-assets.yml new file mode 100644 index 0000000000..620bd3c62e --- /dev/null +++ b/eng/common/templates/job/publish-build-assets.yml @@ -0,0 +1,70 @@ +parameters: + configuration: 'Debug' + + # Optional: condition for the job to run + condition: '' + + # Optional: 'true' if future jobs should run even if this job fails + continueOnError: false + + # Optional: dependencies of the job + dependsOn: '' + + # Optional: Include PublishBuildArtifacts task + enablePublishBuildArtifacts: false + + # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool + pool: {} + + # Optional: should run as a public build even in the internal project + # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. + runAsPublic: false + + # Optional: whether the build's artifacts will be published using release pipelines or direct feed publishing + publishUsingPipelines: false + +jobs: +- job: Asset_Registry_Publish + + dependsOn: ${{ parameters.dependsOn }} + + displayName: Publish to Build Asset Registry + + pool: ${{ parameters.pool }} + + variables: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - name: _BuildConfig + value: ${{ parameters.configuration }} + - group: Publish-Build-Assets + + steps: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: DownloadBuildArtifacts@0 + displayName: Download artifact + inputs: + artifactName: AssetManifests + downloadPath: '$(Build.StagingDirectory)/Download' + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + - task: PowerShell@2 + displayName: Publish Build Assets + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishBuildAssets -restore -msbuildEngine dotnet + /p:ManifestsPath='$(Build.StagingDirectory)/Download/AssetManifests' + /p:BuildAssetRegistryToken=$(MaestroAccessToken) + /p:MaestroApiEndpoint=https://maestro-prod.westus2.cloudapp.azure.com + /p:PublishUsingPipelines=${{ parameters.publishUsingPipelines }} + /p:Configuration=$(_BuildConfig) + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + - ${{ if eq(parameters.enablePublishBuildArtifacts, 'true') }}: + - task: PublishBuildArtifacts@1 + displayName: Publish Logs to VSTS + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)' + PublishLocation: Container + ArtifactName: $(Agent.Os)_PublishBuildAssets + continueOnError: true + condition: always() diff --git a/eng/common/templates/jobs/jobs.yml b/eng/common/templates/jobs/jobs.yml new file mode 100644 index 0000000000..6a2f98c036 --- /dev/null +++ b/eng/common/templates/jobs/jobs.yml @@ -0,0 +1,90 @@ +parameters: + # Optional: 'true' if failures in job.yml job should not fail the job + continueOnError: false + + # Optional: Enable installing Microbuild plugin + # if 'true', these "variables" must be specified in the variables object or as part of the queue matrix + # _TeamName - the name of your team + # _SignType - 'test' or 'real' + enableMicrobuild: false + + # Optional: Include PublishBuildArtifacts task + enablePublishBuildArtifacts: false + + # Optional: Enable publishing to the build asset registry + enablePublishBuildAssets: false + + # Optional: Enable publishing using release pipelines + enablePublishUsingPipelines: false + + graphFileGeneration: + # Optional: Enable generating the graph files at the end of the build + enabled: false + # Optional: Include toolset dependencies in the generated graph files + includeToolset: false + + # Optional: Include PublishTestResults task + enablePublishTestResults: false + + # Optional: enable sending telemetry + # if enabled then the 'helixRepo' parameter should also be specified + enableTelemetry: false + + # Required: A collection of jobs to run - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job + jobs: [] + + # Optional: define the helix repo for telemetry (example: 'dotnet/arcade') + helixRepo: '' + + # Optional: Override automatically derived dependsOn value for "publish build assets" job + publishBuildAssetsDependsOn: '' + + # Optional: should run as a public build even in the internal project + # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. + runAsPublic: false + +# Internal resources (telemetry, microbuild) can only be accessed from non-public projects, +# and some (Microbuild) should only be applied to non-PR cases for internal builds. + +jobs: +- ${{ each job in parameters.jobs }}: + - template: ../job/job.yml + parameters: + # pass along parameters + ${{ each parameter in parameters }}: + ${{ if ne(parameter.key, 'jobs') }}: + ${{ parameter.key }}: ${{ parameter.value }} + + # pass along job properties + ${{ each property in job }}: + ${{ if ne(property.key, 'job') }}: + ${{ property.key }}: ${{ property.value }} + + name: ${{ job.job }} + +- ${{ if and(eq(parameters.enablePublishBuildAssets, true), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - template: ../job/publish-build-assets.yml + parameters: + continueOnError: ${{ parameters.continueOnError }} + dependsOn: + - ${{ if ne(parameters.publishBuildAssetsDependsOn, '') }}: + - ${{ each job in parameters.publishBuildAssetsDependsOn }}: + - ${{ job.job }} + - ${{ if eq(parameters.publishBuildAssetsDependsOn, '') }}: + - ${{ each job in parameters.jobs }}: + - ${{ job.job }} + pool: + vmImage: vs2017-win2016 + runAsPublic: ${{ parameters.runAsPublic }} + publishUsingPipelines: ${{ parameters.enablePublishUsingPipelines }} + enablePublishBuildArtifacts: ${{ parameters.enablePublishBuildArtifacts }} + +- ${{ if and(eq(parameters.graphFileGeneration.enabled, true), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - template: ../job/generate-graph-files.yml + parameters: + continueOnError: ${{ parameters.continueOnError }} + includeToolset: ${{ parameters.graphFileGeneration.includeToolset }} + dependsOn: + - Asset_Registry_Publish + pool: + vmImage: vs2017-win2016 diff --git a/eng/common/templates/phases/base.yml b/eng/common/templates/phases/base.yml new file mode 100644 index 0000000000..0123cf43b1 --- /dev/null +++ b/eng/common/templates/phases/base.yml @@ -0,0 +1,130 @@ +parameters: + # Optional: Clean sources before building + clean: true + + # Optional: Git fetch depth + fetchDepth: '' + + # Optional: name of the phase (not specifying phase name may cause name collisions) + name: '' + # Optional: display name of the phase + displayName: '' + + # Optional: condition for the job to run + condition: '' + + # Optional: dependencies of the phase + dependsOn: '' + + # Required: A defined YAML queue + queue: {} + + # Required: build steps + steps: [] + + # Optional: variables + variables: {} + + # Optional: should run as a public build even in the internal project + # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. + runAsPublic: false + + ## Telemetry variables + + # Optional: enable sending telemetry + # if 'true', these "variables" must be specified in the variables object or as part of the queue matrix + # _HelixBuildConfig - differentiate between Debug, Release, other + # _HelixSource - Example: build/product + # _HelixType - Example: official/dotnet/arcade/$(Build.SourceBranch) + enableTelemetry: false + + # Optional: Enable installing Microbuild plugin + # if 'true', these "variables" must be specified in the variables object or as part of the queue matrix + # _TeamName - the name of your team + # _SignType - 'test' or 'real' + enableMicrobuild: false + +# Internal resources (telemetry, microbuild) can only be accessed from non-public projects, +# and some (Microbuild) should only be applied to non-PR cases for internal builds. + +phases: +- phase: ${{ parameters.name }} + + ${{ if ne(parameters.displayName, '') }}: + displayName: ${{ parameters.displayName }} + + ${{ if ne(parameters.condition, '') }}: + condition: ${{ parameters.condition }} + + ${{ if ne(parameters.dependsOn, '') }}: + dependsOn: ${{ parameters.dependsOn }} + + queue: ${{ parameters.queue }} + + ${{ if ne(parameters.variables, '') }}: + variables: + ${{ insert }}: ${{ parameters.variables }} + + steps: + - checkout: self + clean: ${{ parameters.clean }} + ${{ if ne(parameters.fetchDepth, '') }}: + fetchDepth: ${{ parameters.fetchDepth }} + + - ${{ if eq(parameters.enableTelemetry, 'true') }}: + - template: /eng/common/templates/steps/telemetry-start.yml + parameters: + buildConfig: $(_HelixBuildConfig) + helixSource: $(_HelixSource) + helixType: $(_HelixType) + runAsPublic: ${{ parameters.runAsPublic }} + + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + # Internal only resource, and Microbuild signing shouldn't be applied to PRs. + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: MicroBuildSigningPlugin@2 + displayName: Install MicroBuild plugin + inputs: + signType: $(_SignType) + zipSources: false + feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + + env: + TeamName: $(_TeamName) + continueOnError: false + condition: and(succeeded(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) + + # Run provided build steps + - ${{ parameters.steps }} + + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + # Internal only resources + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: MicroBuildCleanup@1 + displayName: Execute Microbuild cleanup tasks + condition: and(always(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) + env: + TeamName: $(_TeamName) + + - ${{ if eq(parameters.enableTelemetry, 'true') }}: + - template: /eng/common/templates/steps/telemetry-end.yml + parameters: + helixSource: $(_HelixSource) + helixType: $(_HelixType) + + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: CopyFiles@2 + displayName: Gather Asset Manifests + inputs: + SourceFolder: '$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/AssetManifest' + TargetFolder: '$(Build.StagingDirectory)/AssetManifests' + continueOnError: false + condition: and(succeeded(), eq(variables['_DotNetPublishToBlobFeed'], 'true')) + - task: PublishBuildArtifacts@1 + displayName: Push Asset Manifests + inputs: + PathtoPublish: '$(Build.StagingDirectory)/AssetManifests' + PublishLocation: Container + ArtifactName: AssetManifests + continueOnError: false + condition: and(succeeded(), eq(variables['_DotNetPublishToBlobFeed'], 'true')) diff --git a/eng/common/templates/phases/publish-build-assets.yml b/eng/common/templates/phases/publish-build-assets.yml new file mode 100644 index 0000000000..a0a8074282 --- /dev/null +++ b/eng/common/templates/phases/publish-build-assets.yml @@ -0,0 +1,51 @@ +parameters: + dependsOn: '' + queue: {} + configuration: 'Debug' + condition: succeeded() + continueOnError: false + runAsPublic: false + publishUsingPipelines: false +phases: + - phase: Asset_Registry_Publish + displayName: Publish to Build Asset Registry + dependsOn: ${{ parameters.dependsOn }} + queue: ${{ parameters.queue }} + variables: + _BuildConfig: ${{ parameters.configuration }} + steps: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: DownloadBuildArtifacts@0 + displayName: Download artifact + inputs: + artifactName: AssetManifests + downloadPath: '$(Build.StagingDirectory)/Download' + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + - task: AzureKeyVault@1 + inputs: + azureSubscription: 'DotNet-Engineering-Services_KeyVault' + KeyVaultName: EngKeyVault + SecretsFilter: 'MaestroAccessToken' + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + - task: PowerShell@2 + displayName: Publish Build Assets + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishBuildAssets -restore -msbuildEngine dotnet + /p:ManifestsPath='$(Build.StagingDirectory)/Download/AssetManifests' + /p:BuildAssetRegistryToken=$(MaestroAccessToken) + /p:MaestroApiEndpoint=https://maestro-prod.westus2.cloudapp.azure.com + /p:PublishUsingPipelines=${{ parameters.publishUsingPipelines }} + /p:Configuration=$(_BuildConfig) + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + - task: PublishBuildArtifacts@1 + displayName: Publish Logs to VSTS + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)' + PublishLocation: Container + ArtifactName: $(Agent.Os)_Asset_Registry_Publish + continueOnError: true + condition: always() diff --git a/eng/common/templates/steps/build-reason.yml b/eng/common/templates/steps/build-reason.yml new file mode 100644 index 0000000000..eba58109b5 --- /dev/null +++ b/eng/common/templates/steps/build-reason.yml @@ -0,0 +1,12 @@ +# build-reason.yml +# Description: runs steps if build.reason condition is valid. conditions is a string of valid build reasons +# to include steps (',' separated). +parameters: + conditions: '' + steps: [] + +steps: + - ${{ if and( not(startsWith(parameters.conditions, 'not')), contains(parameters.conditions, variables['build.reason'])) }}: + - ${{ parameters.steps }} + - ${{ if and( startsWith(parameters.conditions, 'not'), not(contains(parameters.conditions, variables['build.reason']))) }}: + - ${{ parameters.steps }} diff --git a/eng/common/templates/steps/run-on-unix.yml b/eng/common/templates/steps/run-on-unix.yml new file mode 100644 index 0000000000..e1733814f6 --- /dev/null +++ b/eng/common/templates/steps/run-on-unix.yml @@ -0,0 +1,7 @@ +parameters: + agentOs: '' + steps: [] + +steps: +- ${{ if ne(parameters.agentOs, 'Windows_NT') }}: + - ${{ parameters.steps }} diff --git a/eng/common/templates/steps/run-on-windows.yml b/eng/common/templates/steps/run-on-windows.yml new file mode 100644 index 0000000000..73e7e9c275 --- /dev/null +++ b/eng/common/templates/steps/run-on-windows.yml @@ -0,0 +1,7 @@ +parameters: + agentOs: '' + steps: [] + +steps: +- ${{ if eq(parameters.agentOs, 'Windows_NT') }}: + - ${{ parameters.steps }} diff --git a/eng/common/templates/steps/run-script-ifequalelse.yml b/eng/common/templates/steps/run-script-ifequalelse.yml new file mode 100644 index 0000000000..3d1242f558 --- /dev/null +++ b/eng/common/templates/steps/run-script-ifequalelse.yml @@ -0,0 +1,33 @@ +parameters: + # if parameter1 equals parameter 2, run 'ifScript' command, else run 'elsescript' command + parameter1: '' + parameter2: '' + ifScript: '' + elseScript: '' + + # name of script step + name: Script + + # display name of script step + displayName: If-Equal-Else Script + + # environment + env: {} + + # conditional expression for step execution + condition: '' + +steps: +- ${{ if and(ne(parameters.ifScript, ''), eq(parameters.parameter1, parameters.parameter2)) }}: + - script: ${{ parameters.ifScript }} + name: ${{ parameters.name }} + displayName: ${{ parameters.displayName }} + env: ${{ parameters.env }} + condition: ${{ parameters.condition }} + +- ${{ if and(ne(parameters.elseScript, ''), ne(parameters.parameter1, parameters.parameter2)) }}: + - script: ${{ parameters.elseScript }} + name: ${{ parameters.name }} + displayName: ${{ parameters.displayName }} + env: ${{ parameters.env }} + condition: ${{ parameters.condition }} \ No newline at end of file diff --git a/eng/common/templates/steps/send-to-helix.yml b/eng/common/templates/steps/send-to-helix.yml new file mode 100644 index 0000000000..d1ce577db5 --- /dev/null +++ b/eng/common/templates/steps/send-to-helix.yml @@ -0,0 +1,88 @@ +# Please remember to update the documentation if you make changes to these parameters! +parameters: + HelixSource: 'pr/default' # required -- sources must start with pr/, official/, prodcon/, or agent/ + HelixType: 'tests/default/' # required -- Helix telemetry which identifies what type of data this is; should include "test" for clarity and must end in '/' + HelixBuild: $(Build.BuildNumber) # required -- the build number Helix will use to identify this -- automatically set to the AzDO build number + HelixTargetQueues: '' # required -- semicolon delimited list of Helix queues to test on; see https://helix.dot.net/ for a list of queues + HelixAccessToken: '' # required -- access token to make Helix API requests; should be provided by the appropriate variable group + HelixPreCommands: '' # optional -- commands to run before Helix work item execution + HelixPostCommands: '' # optional -- commands to run after Helix work item execution + WorkItemDirectory: '' # optional -- a payload directory to zip up and send to Helix; requires WorkItemCommand; incompatible with XUnitProjects + WorkItemCommand: '' # optional -- a command to execute on the payload; requires WorkItemDirectory; incompatible with XUnitProjects + WorkItemTimeout: '' # optional -- a timeout in seconds for the work item command; requires WorkItemDirectory; incompatible with XUnitProjects + CorrelationPayloadDirectory: '' # optional -- a directory to zip up and send to Helix as a correlation payload + XUnitProjects: '' # optional -- semicolon delimited list of XUnitProjects to parse and send to Helix; requires XUnitRuntimeTargetFramework, XUnitPublishTargetFramework, XUnitRunnerVersion, and IncludeDotNetCli=true + XUnitWorkItemTimeout: '' # optional -- the workitem timeout in seconds for all workitems created from the xUnit projects specified by XUnitProjects + XUnitPublishTargetFramework: '' # optional -- framework to use to publish your xUnit projects + XUnitRuntimeTargetFramework: '' # optional -- framework to use for the xUnit console runner + XUnitRunnerVersion: '' # optional -- version of the xUnit nuget package you wish to use on Helix; required for XUnitProjects + IncludeDotNetCli: false # optional -- true will download a version of the .NET CLI onto the Helix machine as a correlation payload; requires DotNetCliPackageType and DotNetCliVersion + DotNetCliPackageType: '' # optional -- either 'sdk' or 'runtime'; determines whether the sdk or runtime will be sent to Helix; see https://raw.githubusercontent.com/dotnet/core/master/release-notes/releases.json + DotNetCliVersion: '' # optional -- version of the CLI to send to Helix; based on this: https://raw.githubusercontent.com/dotnet/core/master/release-notes/releases.json + EnableXUnitReporter: false # optional -- true enables XUnit result reporting to Mission Control + WaitForWorkItemCompletion: true # optional -- true will make the task wait until work items have been completed and fail the build if work items fail. False is "fire and forget." + IsExternal: false # [DEPRECATED] -- doesn't do anything, jobs are external if HelixAccessToken is empty and Creator is set + Creator: '' # optional -- if the build is external, use this to specify who is sending the job + DisplayNamePrefix: 'Run Tests' # optional -- rename the beginning of the displayName of the steps in AzDO + condition: succeeded() # optional -- condition for step to execute; defaults to succeeded() + continueOnError: false # optional -- determines whether to continue the build if the step errors; defaults to false + +steps: + - powershell: 'powershell "$env:BUILD_SOURCESDIRECTORY\eng\common\msbuild.ps1 $env:BUILD_SOURCESDIRECTORY\eng\common\helixpublish.proj /restore /t:Test /bl:$env:BUILD_SOURCESDIRECTORY\artifacts\log\$env:BuildConfig\SendToHelix.binlog"' + displayName: ${{ parameters.DisplayNamePrefix }} (Windows) + env: + BuildConfig: $(_BuildConfig) + HelixSource: ${{ parameters.HelixSource }} + HelixType: ${{ parameters.HelixType }} + HelixBuild: ${{ parameters.HelixBuild }} + HelixTargetQueues: ${{ parameters.HelixTargetQueues }} + HelixAccessToken: ${{ parameters.HelixAccessToken }} + HelixPreCommands: ${{ parameters.HelixPreCommands }} + HelixPostCommands: ${{ parameters.HelixPostCommands }} + WorkItemDirectory: ${{ parameters.WorkItemDirectory }} + WorkItemCommand: ${{ parameters.WorkItemCommand }} + WorkItemTimeout: ${{ parameters.WorkItemTimeout }} + CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} + XUnitProjects: ${{ parameters.XUnitProjects }} + XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} + XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} + XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} + XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} + IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} + DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} + DotNetCliVersion: ${{ parameters.DotNetCliVersion }} + EnableXUnitReporter: ${{ parameters.EnableXUnitReporter }} + WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} + Creator: ${{ parameters.Creator }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + condition: and(${{ parameters.condition }}, eq(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} + - script: $BUILD_SOURCESDIRECTORY/eng/common/msbuild.sh $BUILD_SOURCESDIRECTORY/eng/common/helixpublish.proj /restore /t:Test /bl:$BUILD_SOURCESDIRECTORY/artifacts/log/$BuildConfig/SendToHelix.binlog + displayName: ${{ parameters.DisplayNamePrefix }} (Unix) + env: + BuildConfig: $(_BuildConfig) + HelixSource: ${{ parameters.HelixSource }} + HelixType: ${{ parameters.HelixType }} + HelixBuild: ${{ parameters.HelixBuild }} + HelixTargetQueues: ${{ parameters.HelixTargetQueues }} + HelixAccessToken: ${{ parameters.HelixAccessToken }} + HelixPreCommands: ${{ parameters.HelixPreCommands }} + HelixPostCommands: ${{ parameters.HelixPostCommands }} + WorkItemDirectory: ${{ parameters.WorkItemDirectory }} + WorkItemCommand: ${{ parameters.WorkItemCommand }} + WorkItemTimeout: ${{ parameters.WorkItemTimeout }} + CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} + XUnitProjects: ${{ parameters.XUnitProjects }} + XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} + XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} + XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} + XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} + IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} + DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} + DotNetCliVersion: ${{ parameters.DotNetCliVersion }} + EnableXUnitReporter: ${{ parameters.EnableXUnitReporter }} + WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} + Creator: ${{ parameters.Creator }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + condition: and(${{ parameters.condition }}, ne(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} diff --git a/eng/common/templates/steps/telemetry-end.yml b/eng/common/templates/steps/telemetry-end.yml new file mode 100644 index 0000000000..fadc04ca1b --- /dev/null +++ b/eng/common/templates/steps/telemetry-end.yml @@ -0,0 +1,102 @@ +parameters: + maxRetries: 5 + retryDelay: 10 # in seconds + +steps: +- bash: | + if [ "$AGENT_JOBSTATUS" = "Succeeded" ] || [ "$AGENT_JOBSTATUS" = "PartiallySucceeded" ]; then + errorCount=0 + else + errorCount=1 + fi + warningCount=0 + + curlStatus=1 + retryCount=0 + # retry loop to harden against spotty telemetry connections + # we don't retry successes and 4xx client errors + until [[ $curlStatus -eq 0 || ( $curlStatus -ge 400 && $curlStatus -le 499 ) || $retryCount -ge $MaxRetries ]] + do + if [ $retryCount -gt 0 ]; then + echo "Failed to send telemetry to Helix; waiting $RetryDelay seconds before retrying..." + sleep $RetryDelay + fi + + # create a temporary file for curl output + res=`mktemp` + + curlResult=` + curl --verbose --output $res --write-out "%{http_code}"\ + -H 'Content-Type: application/json' \ + -H "X-Helix-Job-Token: $Helix_JobToken" \ + -H 'Content-Length: 0' \ + -X POST -G "https://helix.dot.net/api/2018-03-14/telemetry/job/build/$Helix_WorkItemId/finish" \ + --data-urlencode "errorCount=$errorCount" \ + --data-urlencode "warningCount=$warningCount"` + curlStatus=$? + + if [ $curlStatus -eq 0 ]; then + if [ $curlResult -gt 299 ] || [ $curlResult -lt 200 ]; then + curlStatus=$curlResult + fi + fi + + let retryCount++ + done + + if [ $curlStatus -ne 0 ]; then + echo "Failed to Send Build Finish information after $retryCount retries" + vstsLogOutput="vso[task.logissue type=error;sourcepath=templates/steps/telemetry-end.yml;code=1;]Failed to Send Build Finish information: $curlStatus" + echo "##$vstsLogOutput" + exit 1 + fi + displayName: Send Unix Build End Telemetry + env: + # defined via VSTS variables in start-job.sh + Helix_JobToken: $(Helix_JobToken) + Helix_WorkItemId: $(Helix_WorkItemId) + MaxRetries: ${{ parameters.maxRetries }} + RetryDelay: ${{ parameters.retryDelay }} + condition: and(always(), ne(variables['Agent.Os'], 'Windows_NT')) +- powershell: | + if (($env:Agent_JobStatus -eq 'Succeeded') -or ($env:Agent_JobStatus -eq 'PartiallySucceeded')) { + $ErrorCount = 0 + } else { + $ErrorCount = 1 + } + $WarningCount = 0 + + # Basic retry loop to harden against server flakiness + $retryCount = 0 + while ($retryCount -lt $env:MaxRetries) { + try { + Invoke-RestMethod -Uri "https://helix.dot.net/api/2018-03-14/telemetry/job/build/$env:Helix_WorkItemId/finish?errorCount=$ErrorCount&warningCount=$WarningCount" -Method Post -ContentType "application/json" -Body "" ` + -Headers @{ 'X-Helix-Job-Token'=$env:Helix_JobToken } + break + } + catch { + $statusCode = $_.Exception.Response.StatusCode.value__ + if ($statusCode -ge 400 -and $statusCode -le 499) { + Write-Host "##vso[task.logissue]error Failed to send telemetry to Helix (status code $statusCode); not retrying (4xx client error)" + Write-Host "##vso[task.logissue]error ", $_.Exception.GetType().FullName, $_.Exception.Message + exit 1 + } + Write-Host "Failed to send telemetry to Helix (status code $statusCode); waiting $env:RetryDelay seconds before retrying..." + $retryCount++ + sleep $env:RetryDelay + continue + } + } + + if ($retryCount -ge $env:MaxRetries) { + Write-Host "##vso[task.logissue]error Failed to send telemetry to Helix after $retryCount retries." + exit 1 + } + displayName: Send Windows Build End Telemetry + env: + # defined via VSTS variables in start-job.ps1 + Helix_JobToken: $(Helix_JobToken) + Helix_WorkItemId: $(Helix_WorkItemId) + MaxRetries: ${{ parameters.maxRetries }} + RetryDelay: ${{ parameters.retryDelay }} + condition: and(always(),eq(variables['Agent.Os'], 'Windows_NT')) diff --git a/eng/common/templates/steps/telemetry-start.yml b/eng/common/templates/steps/telemetry-start.yml new file mode 100644 index 0000000000..32c01ef0b5 --- /dev/null +++ b/eng/common/templates/steps/telemetry-start.yml @@ -0,0 +1,241 @@ +parameters: + helixSource: 'undefined_defaulted_in_telemetry.yml' + helixType: 'undefined_defaulted_in_telemetry.yml' + buildConfig: '' + runAsPublic: false + maxRetries: 5 + retryDelay: 10 # in seconds + +steps: +- ${{ if and(eq(parameters.runAsPublic, 'false'), not(eq(variables['System.TeamProject'], 'public'))) }}: + - task: AzureKeyVault@1 + inputs: + azureSubscription: 'HelixProd_KeyVault' + KeyVaultName: HelixProdKV + SecretsFilter: 'HelixApiAccessToken' + condition: always() +- bash: | + # create a temporary file + jobInfo=`mktemp` + + # write job info content to temporary file + cat > $jobInfo <' | Set-Content $proj + + MSBuild-Core $proj $bl /t:__WriteToolsetLocation /clp:ErrorsOnly`;NoSummary /p:__ToolsetLocationOutputFile=$toolsetLocationFile + + $path = Get-Content $toolsetLocationFile -TotalCount 1 + if (!(Test-Path $path)) { + throw "Invalid toolset path: $path" + } + + return $global:_ToolsetBuildProj = $path +} + +function ExitWithExitCode([int] $exitCode) { + if ($ci -and $prepareMachine) { + Stop-Processes + } + exit $exitCode +} + +function Stop-Processes() { + Write-Host "Killing running build processes..." + foreach ($processName in $processesToStopOnExit) { + Get-Process -Name $processName -ErrorAction SilentlyContinue | Stop-Process + } +} + +# +# Executes msbuild (or 'dotnet msbuild') with arguments passed to the function. +# The arguments are automatically quoted. +# Terminates the script if the build fails. +# +function MSBuild() { + if ($pipelinesLog) { + $buildTool = InitializeBuildTool + $toolsetBuildProject = InitializeToolset + $path = Split-Path -parent $toolsetBuildProject + $path = Join-Path $path (Join-Path $buildTool.Framework "Microsoft.DotNet.Arcade.Sdk.dll") + $args += "/logger:$path" + } + + MSBuild-Core @args +} + +# +# Executes msbuild (or 'dotnet msbuild') with arguments passed to the function. +# The arguments are automatically quoted. +# Terminates the script if the build fails. +# +function MSBuild-Core() { + if ($ci) { + if (!$binaryLog) { + throw "Binary log must be enabled in CI build." + } + + if ($nodeReuse) { + throw "Node reuse must be disabled in CI build." + } + } + + $buildTool = InitializeBuildTool + + $cmdArgs = "$($buildTool.Command) /m /nologo /clp:Summary /v:$verbosity /nr:$nodeReuse /p:ContinuousIntegrationBuild=$ci" + + if ($warnAsError) { + $cmdArgs += " /warnaserror /p:TreatWarningsAsErrors=true" + } + + foreach ($arg in $args) { + if ($arg -ne $null -and $arg.Trim() -ne "") { + $cmdArgs += " `"$arg`"" + } + } + + $exitCode = Exec-Process $buildTool.Path $cmdArgs + + if ($exitCode -ne 0) { + Write-Host "Build failed." -ForegroundColor Red + + $buildLog = GetMSBuildBinaryLogCommandLineArgument $args + if ($buildLog -ne $null) { + Write-Host "See log: $buildLog" -ForegroundColor DarkGray + } + + ExitWithExitCode $exitCode + } +} + +function GetMSBuildBinaryLogCommandLineArgument($arguments) { + foreach ($argument in $arguments) { + if ($argument -ne $null) { + $arg = $argument.Trim() + if ($arg.StartsWith("/bl:", "OrdinalIgnoreCase")) { + return $arg.Substring("/bl:".Length) + } + + if ($arg.StartsWith("/binaryLogger:", "OrdinalIgnoreCase")) { + return $arg.Substring("/binaryLogger:".Length) + } + } + } + + return $null +} + +$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..") +$EngRoot = Resolve-Path (Join-Path $PSScriptRoot "..") +$ArtifactsDir = Join-Path $RepoRoot "artifacts" +$ToolsetDir = Join-Path $ArtifactsDir "toolset" +$ToolsDir = Join-Path $RepoRoot ".tools" +$LogDir = Join-Path (Join-Path $ArtifactsDir "log") $configuration +$TempDir = Join-Path (Join-Path $ArtifactsDir "tmp") $configuration +$GlobalJson = Get-Content -Raw -Path (Join-Path $RepoRoot "global.json") | ConvertFrom-Json +# true if global.json contains a "runtimes" section +$globalJsonHasRuntimes = if ($GlobalJson.tools.PSObject.Properties.Name -Match 'runtimes') { $true } else { $false } + +Create-Directory $ToolsetDir +Create-Directory $TempDir +Create-Directory $LogDir + +if ($ci) { + Write-Host "##vso[task.setvariable variable=Artifacts]$ArtifactsDir" + Write-Host "##vso[task.setvariable variable=Artifacts.Toolset]$ToolsetDir" + Write-Host "##vso[task.setvariable variable=Artifacts.Log]$LogDir" + + $env:TEMP = $TempDir + $env:TMP = $TempDir +} diff --git a/eng/common/tools.sh b/eng/common/tools.sh new file mode 100755 index 0000000000..df3eb8bce0 --- /dev/null +++ b/eng/common/tools.sh @@ -0,0 +1,365 @@ +# Initialize variables if they aren't already defined. + +# CI mode - set to true on CI server for PR validation build or official build. +ci=${ci:-false} + +# Build configuration. Common values include 'Debug' and 'Release', but the repository may use other names. +configuration=${configuration:-'Debug'} + +# Set to true to output binary log from msbuild. Note that emitting binary log slows down the build. +# Binary log must be enabled on CI. +binary_log=${binary_log:-$ci} + +# Turns on machine preparation/clean up code that changes the machine state (e.g. kills build processes). +prepare_machine=${prepare_machine:-false} + +# True to restore toolsets and dependencies. +restore=${restore:-true} + +# Adjusts msbuild verbosity level. +verbosity=${verbosity:-'minimal'} + +# Set to true to reuse msbuild nodes. Recommended to not reuse on CI. +if [[ "$ci" == true ]]; then + node_reuse=${node_reuse:-false} +else + node_reuse=${node_reuse:-true} +fi + +# Configures warning treatment in msbuild. +warn_as_error=${warn_as_error:-true} + +# True to attempt using .NET Core already that meets requirements specified in global.json +# installed on the machine instead of downloading one. +use_installed_dotnet_cli=${use_installed_dotnet_cli:-true} + +# True to use global NuGet cache instead of restoring packages to repository-local directory. +if [[ "$ci" == true ]]; then + use_global_nuget_cache=${use_global_nuget_cache:-false} +else + use_global_nuget_cache=${use_global_nuget_cache:-true} +fi + +# Resolve any symlinks in the given path. +function ResolvePath { + local path=$1 + + while [[ -h $path ]]; do + local dir="$( cd -P "$( dirname "$path" )" && pwd )" + path="$(readlink "$path")" + + # if $path was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $path != /* ]] && path="$dir/$path" + done + + # return value + _ResolvePath="$path" +} + +# ReadVersionFromJson [json key] +function ReadGlobalVersion { + local key=$1 + + local line=`grep -m 1 "$key" "$global_json_file"` + local pattern="\"$key\" *: *\"(.*)\"" + + if [[ ! $line =~ $pattern ]]; then + echo "Error: Cannot find \"$key\" in $global_json_file" >&2 + ExitWithExitCode 1 + fi + + # return value + _ReadGlobalVersion=${BASH_REMATCH[1]} +} + +function InitializeDotNetCli { + if [[ -n "${_InitializeDotNetCli:-}" ]]; then + return + fi + + local install=$1 + + # Don't resolve runtime, shared framework, or SDK from other locations to ensure build determinism + export DOTNET_MULTILEVEL_LOOKUP=0 + + # Disable first run since we want to control all package sources + export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 + + # Disable telemetry on CI + if [[ $ci == true ]]; then + export DOTNET_CLI_TELEMETRY_OPTOUT=1 + fi + + # LTTNG is the logging infrastructure used by Core CLR. Need this variable set + # so it doesn't output warnings to the console. + export LTTNG_HOME="$HOME" + + # Source Build uses DotNetCoreSdkDir variable + if [[ -n "${DotNetCoreSdkDir:-}" ]]; then + export DOTNET_INSTALL_DIR="$DotNetCoreSdkDir" + fi + + # Find the first path on $PATH that contains the dotnet.exe + if [[ "$use_installed_dotnet_cli" == true && $global_json_has_runtimes == false && -z "${DOTNET_INSTALL_DIR:-}" ]]; then + local dotnet_path=`command -v dotnet` + if [[ -n "$dotnet_path" ]]; then + ResolvePath "$dotnet_path" + export DOTNET_INSTALL_DIR=`dirname "$_ResolvePath"` + fi + fi + + ReadGlobalVersion "dotnet" + local dotnet_sdk_version=$_ReadGlobalVersion + local dotnet_root="" + + # Use dotnet installation specified in DOTNET_INSTALL_DIR if it contains the required SDK version, + # otherwise install the dotnet CLI and SDK to repo local .dotnet directory to avoid potential permission issues. + if [[ $global_json_has_runtimes == false && -n "${DOTNET_INSTALL_DIR:-}" && -d "$DOTNET_INSTALL_DIR/sdk/$dotnet_sdk_version" ]]; then + dotnet_root="$DOTNET_INSTALL_DIR" + else + dotnet_root="$repo_root/.dotnet" + + export DOTNET_INSTALL_DIR="$dotnet_root" + + if [[ ! -d "$DOTNET_INSTALL_DIR/sdk/$dotnet_sdk_version" ]]; then + if [[ "$install" == true ]]; then + InstallDotNetSdk "$dotnet_root" "$dotnet_sdk_version" + else + echo "Unable to find dotnet with SDK version '$dotnet_sdk_version'" >&2 + ExitWithExitCode 1 + fi + fi + fi + + # Add dotnet to PATH. This prevents any bare invocation of dotnet in custom + # build steps from using anything other than what we've downloaded. + export PATH="$dotnet_root:$PATH" + + if [[ $ci == true ]]; then + # Make Sure that our bootstrapped dotnet cli is avaliable in future steps of the Azure Pipelines build + echo "##vso[task.prependpath]$dotnet_root" + echo "##vso[task.setvariable variable=DOTNET_MULTILEVEL_LOOKUP]0" + echo "##vso[task.setvariable variable=DOTNET_SKIP_FIRST_TIME_EXPERIENCE]1" + fi + + # return value + _InitializeDotNetCli="$dotnet_root" +} + +function InstallDotNetSdk { + local root=$1 + local version=$2 + local architecture="" + if [[ $# == 3 ]]; then + architecture=$3 + fi + InstallDotNet "$root" "$version" $architecture +} + +function InstallDotNet { + local root=$1 + local version=$2 + + GetDotNetInstallScript "$root" + local install_script=$_GetDotNetInstallScript + + local archArg='' + if [[ -n "${3:-}" ]]; then + archArg="--architecture $3" + fi + local runtimeArg='' + if [[ -n "${4:-}" ]]; then + runtimeArg="--runtime $4" + fi + + local skipNonVersionedFilesArg="" + if [[ "$#" -ge "5" ]]; then + skipNonVersionedFilesArg="--skip-non-versioned-files" + fi + bash "$install_script" --version $version --install-dir "$root" $archArg $runtimeArg $skipNonVersionedFilesArg || { + local exit_code=$? + echo "Failed to install dotnet SDK (exit code '$exit_code')." >&2 + ExitWithExitCode $exit_code + } +} + +function GetDotNetInstallScript { + local root=$1 + local install_script="$root/dotnet-install.sh" + local install_script_url="https://dot.net/v1/dotnet-install.sh" + + if [[ ! -a "$install_script" ]]; then + mkdir -p "$root" + + echo "Downloading '$install_script_url'" + + # Use curl if available, otherwise use wget + if command -v curl > /dev/null; then + curl "$install_script_url" -sSL --retry 10 --create-dirs -o "$install_script" + else + wget -q -O "$install_script" "$install_script_url" + fi + fi + + # return value + _GetDotNetInstallScript="$install_script" +} + +function InitializeBuildTool { + if [[ -n "${_InitializeBuildTool:-}" ]]; then + return + fi + + InitializeDotNetCli $restore + + # return values + _InitializeBuildTool="$_InitializeDotNetCli/dotnet" + _InitializeBuildToolCommand="msbuild" +} + +function GetNuGetPackageCachePath { + if [[ -z ${NUGET_PACKAGES:-} ]]; then + if [[ "$use_global_nuget_cache" == true ]]; then + export NUGET_PACKAGES="$HOME/.nuget/packages" + else + export NUGET_PACKAGES="$repo_root/.packages" + fi + fi + + # return value + _GetNuGetPackageCachePath=$NUGET_PACKAGES +} + +function InitializeNativeTools() { + if grep -Fq "native-tools" $global_json_file + then + local nativeArgs="" + if [[ "$ci" == true ]]; then + nativeArgs="-InstallDirectory $tools_dir" + fi + "$_script_dir/init-tools-native.sh" $nativeArgs + fi +} + +function InitializeToolset { + if [[ -n "${_InitializeToolset:-}" ]]; then + return + fi + + GetNuGetPackageCachePath + + ReadGlobalVersion "Microsoft.DotNet.Arcade.Sdk" + + local toolset_version=$_ReadGlobalVersion + local toolset_location_file="$toolset_dir/$toolset_version.txt" + + if [[ -a "$toolset_location_file" ]]; then + local path=`cat "$toolset_location_file"` + if [[ -a "$path" ]]; then + # return value + _InitializeToolset="$path" + return + fi + fi + + if [[ "$restore" != true ]]; then + echo "Toolset version $toolsetVersion has not been restored." >&2 + ExitWithExitCode 2 + fi + + local proj="$toolset_dir/restore.proj" + + local bl="" + if [[ "$binary_log" == true ]]; then + bl="/bl:$log_dir/ToolsetRestore.binlog" + fi + + echo '' > "$proj" + MSBuild "$proj" $bl /t:__WriteToolsetLocation /clp:ErrorsOnly\;NoSummary /p:__ToolsetLocationOutputFile="$toolset_location_file" + + local toolset_build_proj=`cat "$toolset_location_file"` + + if [[ ! -a "$toolset_build_proj" ]]; then + echo "Invalid toolset path: $toolset_build_proj" >&2 + ExitWithExitCode 3 + fi + + # return value + _InitializeToolset="$toolset_build_proj" +} + +function ExitWithExitCode { + if [[ "$ci" == true && "$prepare_machine" == true ]]; then + StopProcesses + fi + exit $1 +} + +function StopProcesses { + echo "Killing running build processes..." + pkill -9 "dotnet" || true + pkill -9 "vbcscompiler" || true + return 0 +} + +function MSBuild { + if [[ "$ci" == true ]]; then + if [[ "$binary_log" != true ]]; then + echo "Binary log must be enabled in CI build." >&2 + ExitWithExitCode 1 + fi + + if [[ "$node_reuse" == true ]]; then + echo "Node reuse must be disabled in CI build." >&2 + ExitWithExitCode 1 + fi + fi + + InitializeBuildTool + + local warnaserror_switch="" + if [[ $warn_as_error == true ]]; then + warnaserror_switch="/warnaserror" + fi + + "$_InitializeBuildTool" "$_InitializeBuildToolCommand" /m /nologo /clp:Summary /v:$verbosity /nr:$node_reuse $warnaserror_switch /p:TreatWarningsAsErrors=$warn_as_error /p:ContinuousIntegrationBuild=$ci "$@" || { + local exit_code=$? + echo "Build failed (exit code '$exit_code')." >&2 + ExitWithExitCode $exit_code + } +} + +ResolvePath "${BASH_SOURCE[0]}" +_script_dir=`dirname "$_ResolvePath"` + +eng_root=`cd -P "$_script_dir/.." && pwd` +repo_root=`cd -P "$_script_dir/../.." && pwd` +artifacts_dir="$repo_root/artifacts" +toolset_dir="$artifacts_dir/toolset" +tools_dir="$repo_root/.tools" +log_dir="$artifacts_dir/log/$configuration" +temp_dir="$artifacts_dir/tmp/$configuration" + +global_json_file="$repo_root/global.json" +# determine if global.json contains a "runtimes" entry +global_json_has_runtimes=false +dotnetlocal_key=`grep -m 1 "runtimes" "$global_json_file"` || true +if [[ -n "$dotnetlocal_key" ]]; then + global_json_has_runtimes=true +fi + +# HOME may not be defined in some scenarios, but it is required by NuGet +if [[ -z $HOME ]]; then + export HOME="$repo_root/artifacts/.home/" + mkdir -p "$HOME" +fi + +mkdir -p "$toolset_dir" +mkdir -p "$temp_dir" +mkdir -p "$log_dir" + +if [[ $ci == true ]]; then + export TEMP="$temp_dir" + export TMP="$temp_dir" +fi \ No newline at end of file diff --git a/eng/helix/content/mssql/InstallSqlServerLocalDB.ps1 b/eng/helix/content/mssql/InstallSqlServerLocalDB.ps1 index 5af8fc5dac..8621112ac6 100644 --- a/eng/helix/content/mssql/InstallSqlServerLocalDB.ps1 +++ b/eng/helix/content/mssql/InstallSqlServerLocalDB.ps1 @@ -3,6 +3,8 @@ Installs SQL Server 2016 Express LocalDB on a machine. .DESCRIPTION This script installs Microsoft SQL Server 2016 Express LocalDB on a machine. +.PARAMETER Force + Force the script to run the MSI, even it it appears LocalDB is installed. .LINK https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb?view=sql-server-2016 https://docs.microsoft.com/en-us/sql/database-engine/install-windows/install-sql-server-from-the-command-prompt?view=sql-server-2016 diff --git a/eng/helix/content/runtests.cmd b/eng/helix/content/runtests.cmd index 8a8a158739..8657c61ec1 100644 --- a/eng/helix/content/runtests.cmd +++ b/eng/helix/content/runtests.cmd @@ -52,7 +52,7 @@ if errorlevel 1 ( set FLAKY_FILTER="Flaky:All=true|Flaky:Helix:All=true|Flaky:Helix:Queue:All=true|Flaky:Helix:Queue:%HELIX%=true" echo Running known-flaky tests. -%DOTNET_ROOT%\dotnet vstest %target% --logger:trx --TestCaseFilter:%FLAKY_FILTER% +%DOTNET_ROOT%\dotnet vstest %target% --TestCaseFilter:%FLAKY_FILTER% if errorlevel 1 ( echo Failure in flaky test 1>&2 REM DO NOT EXIT and DO NOT SET EXIT_CODE to 1 diff --git a/eng/helix/content/runtests.sh b/eng/helix/content/runtests.sh index ce34df6222..b3a786ee05 100644 --- a/eng/helix/content/runtests.sh +++ b/eng/helix/content/runtests.sh @@ -1,9 +1,46 @@ #!/usr/bin/env bash + +test_binary_path="$1" +dotnet_sdk_version="$2" +dotnet_runtime_version="$3" +helix_queue_name="$4" + +RESET="\033[0m" +RED="\033[0;31m" +YELLOW="\033[0;33m" +MAGENTA="\033[0;95m" +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Ensures every invocation of dotnet apps uses the same dotnet.exe +# Add $random to path to ensure tests don't expect dotnet to be in a particular path +export DOTNET_ROOT="$DIR/.dotnet$RANDOM" + +# Ensure dotnet comes first on PATH +export PATH="$DOTNET_ROOT:$PATH" + +# Prevent fallback to global .NET locations. This ensures our tests use the shared frameworks we specify and don't rollforward to something else that might be installed on the machine +export DOTNET_MULTILEVEL_LOOKUP=0 + +# Avoid contaminating userprofiles +# Add $random to path to ensure tests don't expect home to be in a particular path +export DOTNET_CLI_HOME="$DIR/.home$RANDOM" + +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 + +# Used by SkipOnHelix attribute +export helix="$helix_queue_name" + + +RESET="\033[0m" +RED="\033[0;31m" +YELLOW="\033[0;33m" +MAGENTA="\033[0;95m" + curl -o dotnet-install.sh -sSL https://dot.net/v1/dotnet-install.sh if [ $? -ne 0 ]; then download_retries=3 while [ $download_retries -gt 0 ]; do - curl -sSL https://dot.net/v1/dotnet-install.sh + curl -o dotnet-install.sh -sSL https://dot.net/v1/dotnet-install.sh if [ $? -ne 0 ]; then let download_retries=download_retries-1 echo -e "${YELLOW}Failed to download dotnet-install.sh. Retries left: $download_retries.${RESET}" @@ -16,11 +53,11 @@ fi # Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs) chmod +x "dotnet-install.sh"; sync -./dotnet-install.sh --version $2 --install-dir $HELIX_CORRELATION_PAYLOAD/sdk +./dotnet-install.sh --version $dotnet_sdk_version --install-dir "$DOTNET_ROOT" if [ $? -ne 0 ]; then sdk_retries=3 while [ $sdk_retries -gt 0 ]; do - ./dotnet-install.sh --version $2 --install-dir $HELIX_CORRELATION_PAYLOAD/sdk + ./dotnet-install.sh --version $dotnet_sdk_version --install-dir "$DOTNET_ROOT" if [ $? -ne 0 ]; then let sdk_retries=sdk_retries-1 echo -e "${YELLOW}Failed to install .NET Core SDK $version. Retries left: $sdk_retries.${RESET}" @@ -30,11 +67,11 @@ if [ $? -ne 0 ]; then done fi -./dotnet-install.sh --runtime dotnet --version $3 --install-dir $HELIX_CORRELATION_PAYLOAD/sdk +./dotnet-install.sh --runtime dotnet --version $dotnet_runtime_version --install-dir "$DOTNET_ROOT" if [ $? -ne 0 ]; then runtime_retries=3 while [ $runtime_retries -gt 0 ]; do - ./dotnet-install.sh --runtime dotnet --version $3 --install-dir $HELIX_CORRELATION_PAYLOAD/sdk + ./dotnet-install.sh --runtime dotnet --version $dotnet_runtime_version --install-dir "$DOTNET_ROOT" if [ $? -ne 0 ]; then let runtime_retries=runtime_retries-1 echo -e "${YELLOW}Failed to install .NET Core runtime $version. Retries left: $runtime_retries.${RESET}" @@ -44,23 +81,7 @@ if [ $? -ne 0 ]; then done fi -export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 - -# Ensures every invocation of dotnet apps uses the same dotnet.exe -export DOTNET_ROOT="$HELIX_CORRELATION_PAYLOAD/sdk" - -# Ensure dotnet comes first on PATH -export PATH="$DOTNET_ROOT:$PATH" - -# Prevent fallback to global .NET locations. This ensures our tests use the shared frameworks we specify and don't rollforward to something else that might be installed on the machine -export DOTNET_MULTILEVEL_LOOKUP=0 - -# Avoid contaminating userprofiles -export DOTNET_CLI_HOME="$HELIX_CORRELATION_PAYLOAD/home" - -export helix="$4" - -$DOTNET_ROOT/dotnet vstest $1 -lt >discovered.txt +$DOTNET_ROOT/dotnet vstest $test_binary_path -lt >discovered.txt if grep -q "Exception thrown" discovered.txt; then echo -e "${RED}Exception thrown during test discovery${RESET}". cat discovered.txt @@ -71,17 +92,18 @@ fi # We need to specify all possible Flaky filters that apply to this environment, because the flaky attribute # only puts the explicit filter traits the user provided in the flaky attribute # Filter syntax: https://github.com/Microsoft/vstest-docs/blob/master/docs/filter.md -NONFLAKY_FILTER="Flaky:All!=true&Flaky:Helix:All!=true&Flaky:Helix:Queue:All!=true&Flaky:Helix:Queue:$HELIX!=true" +NONFLAKY_FILTER="Flaky:All!=true&Flaky:Helix:All!=true&Flaky:Helix:Queue:All!=true&Flaky:Helix:Queue:$helix_queue_name!=true" echo "Running non-flaky tests." -$DOTNET_ROOT/dotnet vstest $1 --logger:trx --TestCaseFilter:"$NONFLAKY_FILTER" +$DOTNET_ROOT/dotnet vstest $test_binary_path --logger:trx --TestCaseFilter:"$NONFLAKY_FILTER" nonflaky_exitcode=$? if [ $nonflaky_exitcode != 0 ]; then echo "Non-flaky tests failed!" 1>&2 # DO NOT EXIT fi -FLAKY_FILTER="Flaky:All=true|Flaky:Helix:All=true|Flaky:Helix:Queue:All=true|Flaky:Helix:Queue:$HELIX=true" + +FLAKY_FILTER="Flaky:All=true|Flaky:Helix:All=true|Flaky:Helix:Queue:All=true|Flaky:Helix:Queue:$helix_queue_name=true" echo "Running known-flaky tests." -$DOTNET_ROOT/dotnet vstest $1 --logger:trx --TestCaseFilter:"$FLAKY_FILTER" +$DOTNET_ROOT/dotnet vstest $test_binary_path --TestCaseFilter:"$FLAKY_FILTER" if [ $? != 0 ]; then echo "Flaky tests failed!" 1>&2 # DO NOT EXIT diff --git a/eng/scripts/InstallJdk.ps1 b/eng/scripts/InstallJdk.ps1 index e56e768f4f..3602b6e734 100644 --- a/eng/scripts/InstallJdk.ps1 +++ b/eng/scripts/InstallJdk.ps1 @@ -1,27 +1,52 @@ - +<# +.SYNOPSIS + Installs JDK into a folder in this repo. +.DESCRIPTION + This script downloads an extracts the JDK. +.PARAMETER JdkVersion + The version of the JDK to install. If not set, the default value is read from global.json +.PARAMETER Force + Overwrite the existing installation +#> param( - [Parameter(Mandatory = $true)] - $JdkVersion + [string]$JdkVersion, + [switch]$Force ) - $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # Workaround PowerShell/PowerShell#2138 Set-StrictMode -Version 1 -if (-not $env:JAVA_HOME) { - throw 'You must set the JAVA_HOME environment variable to the destination of the JDK.' +$repoRoot = Resolve-Path "$PSScriptRoot\..\.." +$installDir = "$repoRoot\.tools\jdk\win-x64\" +$tempDir = "$repoRoot\obj" +if (-not $JdkVersion) { + $globalJson = Get-Content "$repoRoot\global.json" | ConvertFrom-Json + $JdkVersion = $globalJson.tools.jdk } -$repoRoot = Resolve-Path "$PSScriptRoot/../.." -$tempDir = "$repoRoot/obj" +if (Test-Path $installDir) { + if ($Force) { + Remove-Item -Force -Recurse $installDir + } + else { + Write-Host "The JDK already installed to $installDir. Exiting without action. Call this script again with -Force to overwrite." + exit 0 + } +} + +Remove-Item -Force -Recurse $tempDir -ErrorAction Ignore | out-null mkdir $tempDir -ea Ignore | out-null +mkdir $installDir -ea Ignore | out-null Write-Host "Starting download of JDK ${JdkVersion}" Invoke-WebRequest -UseBasicParsing -Uri "https://netcorenativeassets.blob.core.windows.net/resource-packages/external/windows/java/jdk-${JdkVersion}_windows-x64_bin.zip" -Out "$tempDir/jdk.zip" Write-Host "Done downloading JDK ${JdkVersion}" Expand-Archive "$tempDir/jdk.zip" -d "$tempDir/jdk/" Write-Host "Expanded JDK to $tempDir" -mkdir (split-path -parent $env:JAVA_HOME) -ea ignore | out-null -Write-Host "Installing JDK to $env:JAVA_HOME" -Move-Item "$tempDir/jdk/jdk-${jdkVersion}" $env:JAVA_HOME -Write-Host "Done installing JDK to $env:JAVA_HOME" +Write-Host "Installing JDK to $installDir" +Move-Item "$tempDir/jdk/jdk-${JdkVersion}/*" $installDir +Write-Host "Done installing JDK to $installDir" + +if ($env:TF_BUILD) { + Write-Host "##vso[task.prependpath]$installDir\bin" +} diff --git a/eng/targets/Helix.Common.props b/eng/targets/Helix.Common.props index 76b2d615a9..f4cd02ece0 100644 --- a/eng/targets/Helix.Common.props +++ b/eng/targets/Helix.Common.props @@ -23,7 +23,7 @@ - + diff --git a/eng/targets/Helix.targets b/eng/targets/Helix.targets index 2ec7b469ea..901f9f75e4 100644 --- a/eng/targets/Helix.targets +++ b/eng/targets/Helix.targets @@ -60,6 +60,12 @@ Usage: dotnet build /t:Helix src/MyTestProject.csproj + + + <_HelixFriendlyNameTargetQueue>$(HelixTargetQueue) + <_HelixFriendlyNameTargetQueue Condition="$(HelixTargetQueue.Contains('@'))">$(HelixTargetQueue.Substring(1, $([MSBuild]::Subtract($(HelixTargetQueue.LastIndexOf(')')), 1)))) + + @@ -72,8 +78,8 @@ Usage: dotnet build /t:Helix src/MyTestProject.csproj $(TargetFileName) @(HelixPreCommand) @(HelixPostCommand) - call runtests.cmd $(TargetFileName) $(TargetFrameworkIdentifier) $(NETCoreSdkVersion) $(MicrosoftNETCoreAppPackageVersion) $(HelixTargetQueue) - ./runtests.sh $(TargetFileName) $(NETCoreSdkVersion) $(MicrosoftNETCoreAppPackageVersion) $(HelixTargetQueue) + call runtests.cmd $(TargetFileName) $(TargetFrameworkIdentifier) $(NETCoreSdkVersion) $(MicrosoftNETCoreAppPackageVersion) $(_HelixFriendlyNameTargetQueue) + ./runtests.sh $(TargetFileName) $(NETCoreSdkVersion) $(MicrosoftNETCoreAppPackageVersion) $(_HelixFriendlyNameTargetQueue) $(HelixTimeout) diff --git a/global.json b/global.json index 445956802e..30db777b99 100644 --- a/global.json +++ b/global.json @@ -2,8 +2,11 @@ "sdk": { "version": "3.0.100-preview5-011568" }, + "tools": { + "jdk": "11.0.3" + }, "msbuild-sdks": { "Yarn.MSBuild": "1.15.2", - "Microsoft.DotNet.Helix.Sdk": "2.0.0-beta.19262.1" + "Microsoft.DotNet.Helix.Sdk": "2.0.0-beta.19270.1" } } diff --git a/src/Analyzers/Analyzers/test/AnalyzerTestBase.cs b/src/Analyzers/Analyzers/test/AnalyzerTestBase.cs index 989d79d012..dee9dd3227 100644 --- a/src/Analyzers/Analyzers/test/AnalyzerTestBase.cs +++ b/src/Analyzers/Analyzers/test/AnalyzerTestBase.cs @@ -51,7 +51,11 @@ namespace Microsoft.AspNetCore.Analyzers return AppContext.BaseDirectory; } +// This test code needs to be updated to support distributed testing. +// See https://github.com/aspnet/AspNetCore/issues/10422 +#pragma warning disable 0618 var solutionDirectory = TestPathUtilities.GetSolutionRootDirectory("Analyzers"); +#pragma warning restore 0618 var projectDirectory = Path.Combine(solutionDirectory, "Analyzers", "test"); return projectDirectory; } diff --git a/src/Analyzers/Analyzers/test/Microsoft.AspNetCore.Analyzers.Test.csproj b/src/Analyzers/Analyzers/test/Microsoft.AspNetCore.Analyzers.Test.csproj index f80356e535..258da09738 100644 --- a/src/Analyzers/Analyzers/test/Microsoft.AspNetCore.Analyzers.Test.csproj +++ b/src/Analyzers/Analyzers/test/Microsoft.AspNetCore.Analyzers.Test.csproj @@ -6,7 +6,7 @@ Microsoft.AspNetCore.Analyzers - + false diff --git a/src/Components/Blazor/Blazor/test/WebAssemblyUriHelperTest.cs b/src/Components/Blazor/Blazor/test/WebAssemblyUriHelperTest.cs index 45c94a71d2..2903d7fcd3 100644 --- a/src/Components/Blazor/Blazor/test/WebAssemblyUriHelperTest.cs +++ b/src/Components/Blazor/Blazor/test/WebAssemblyUriHelperTest.cs @@ -29,6 +29,9 @@ namespace Microsoft.AspNetCore.Blazor.Services.Test [InlineData("scheme://host/path/", "scheme://host/path/", "")] [InlineData("scheme://host/path/", "scheme://host/path/more", "more")] [InlineData("scheme://host/path/", "scheme://host/path", "")] + [InlineData("scheme://host/path/", "scheme://host/path#hash", "#hash")] + [InlineData("scheme://host/path/", "scheme://host/path/#hash", "#hash")] + [InlineData("scheme://host/path/", "scheme://host/path/more#hash", "more#hash")] public void ComputesCorrectValidBaseRelativePaths(string baseUri, string absoluteUri, string expectedResult) { var actualResult = _uriHelper.ToBaseRelativePath(baseUri, absoluteUri); diff --git a/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.props b/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.props index 682dbeed28..03f70748ff 100644 --- a/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.props +++ b/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.props @@ -6,7 +6,7 @@ none - --verbose --strip-security true --exclude-feature com --exclude-feature sre -v false -c link -u link -b true + --disable-opt unreachablebodies --verbose --strip-security true --exclude-feature com --exclude-feature sre -v false -c link -u link -b true dist/ $(BaseBlazorDistPath)_content/ $(BaseBlazorDistPath)_framework/ diff --git a/src/Components/Blazor/Build/test/RuntimeDependenciesResolverTest.cs b/src/Components/Blazor/Build/test/RuntimeDependenciesResolverTest.cs index 53597ad893..1817a4c302 100644 --- a/src/Components/Blazor/Build/test/RuntimeDependenciesResolverTest.cs +++ b/src/Components/Blazor/Build/test/RuntimeDependenciesResolverTest.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test } [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/6549 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/10426")] public void FindsReferenceAssemblyGraph_ForStandaloneApp() { // Arrange diff --git a/src/Components/Blazor/Server/src/MonoDebugProxy/UpdateSources.cmd b/src/Components/Blazor/Server/src/MonoDebugProxy/UpdateSources.cmd deleted file mode 100644 index 591f2f9950..0000000000 --- a/src/Components/Blazor/Server/src/MonoDebugProxy/UpdateSources.cmd +++ /dev/null @@ -1,20 +0,0 @@ -@echo off -echo |---- -echo | Copying the ws-proxy sources here is a temporary step until ws-proxy is -echo | distributed as a NuGet package. -echo | ... -echo | Instead of dealing with Git submodules, this script simply fetches the -echo | latest sources so they can be built directly inside this project (hence -echo | we don't have to publish our own separate package for this). -echo | ... -echo | When updating, you'll need to re-apply any patches we've made manually. -echo |---- -@echo on - -cd /D "%~dp0" -rmdir /s /q ws-proxy -git clone https://github.com/kumpera/ws-proxy.git -rmdir /s /q ws-proxy\.git -del ws-proxy\*.csproj -del ws-proxy\*.sln -del ws-proxy\Program.cs diff --git a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs index 9d29cc494a..ee28bd94b7 100644 --- a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs +++ b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs @@ -6,6 +6,9 @@ using Mono.Cecil.Cil; using System.Linq; using Newtonsoft.Json.Linq; using System.Net.Http; +using Mono.Cecil.Pdb; +using Newtonsoft.Json; +using System.Text.RegularExpressions; namespace WsProxy { internal class BreakPointRequest { @@ -18,17 +21,29 @@ namespace WsProxy { return $"BreakPointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}"; } - public static BreakPointRequest Parse (JObject args) + public static BreakPointRequest Parse (JObject args, DebugStore store) { if (args == null) return null; var url = args? ["url"]?.Value (); - if (!url.StartsWith ("dotnet://", StringComparison.InvariantCulture)) + if (url == null) { + var urlRegex = args?["urlRegex"].Value(); + var sourceFile = store.GetFileByUrlRegex (urlRegex); + + url = sourceFile?.DotNetUrl; + } + + if (url != null && !url.StartsWith ("dotnet://", StringComparison.InvariantCulture)) { + var sourceFile = store.GetFileByUrl (url); + url = sourceFile?.DotNetUrl; + } + + if (url == null) return null; - var parts = url.Substring ("dotnet://".Length).Split ('/'); - if (parts.Length != 2) + var parts = ParseDocumentUrl (url); + if (parts.Assembly == null) return null; var line = args? ["lineNumber"]?.Value (); @@ -37,12 +52,24 @@ namespace WsProxy { return null; return new BreakPointRequest () { - Assembly = parts [0], - File = parts [1], + Assembly = parts.Assembly, + File = parts.DocumentPath, Line = line.Value, Column = column.Value }; } + + static (string Assembly, string DocumentPath) ParseDocumentUrl (string url) + { + if (Uri.TryCreate (url, UriKind.Absolute, out var docUri) && docUri.Scheme == "dotnet") { + return ( + docUri.Host, + docUri.PathAndQuery.Substring (1) + ); + } else { + return (null, null); + } + } } @@ -101,7 +128,7 @@ namespace WsProxy { public SourceLocation (MethodInfo mi, SequencePoint sp) { this.id = mi.SourceId; - this.line = sp.StartLine; + this.line = sp.StartLine - 1; this.column = sp.StartColumn - 1; this.cliLoc = new CliLocation (mi, sp.Offset); } @@ -260,13 +287,13 @@ namespace WsProxy { } } - internal class AssemblyInfo { static int next_id; ModuleDefinition image; readonly int id; Dictionary methods = new Dictionary (); - readonly List sources = new List (); + Dictionary sourceLinkMappings = new Dictionary(); + readonly List sources = new List(); public AssemblyInfo (byte[] assembly, byte[] pdb) { @@ -274,16 +301,35 @@ namespace WsProxy { this.id = ++next_id; } - ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/); - if (pdb != null) { - rp.ReadSymbols = true; - rp.SymbolReaderProvider = new PortablePdbReaderProvider (); - rp.SymbolStream = new MemoryStream (pdb); + try { + ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/); + if (pdb != null) { + rp.ReadSymbols = true; + rp.SymbolReaderProvider = new PortablePdbReaderProvider (); + rp.SymbolStream = new MemoryStream (pdb); + } + + rp.ReadingMode = ReadingMode.Immediate; + rp.InMemory = true; + + this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp); + } catch (BadImageFormatException ex) { + Console.WriteLine ($"Failed to read assembly as portable PDB: {ex.Message}"); } - rp.InMemory = true; + if (this.image == null) { + ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/); + if (pdb != null) { + rp.ReadSymbols = true; + rp.SymbolReaderProvider = new NativePdbReaderProvider (); + rp.SymbolStream = new MemoryStream (pdb); + } - this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp); + rp.ReadingMode = ReadingMode.Immediate; + rp.InMemory = true; + + this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp); + } Populate (); } @@ -294,6 +340,8 @@ namespace WsProxy { void Populate () { + ProcessSourceLink(); + var d2s = new Dictionary (); Func get_src = (doc) => { @@ -301,33 +349,88 @@ namespace WsProxy { return null; if (d2s.ContainsKey (doc)) return d2s [doc]; - var src = new SourceFile (this, sources.Count, doc); + var src = new SourceFile (this, sources.Count, doc, GetSourceLinkUrl (doc.Url)); sources.Add (src); d2s [doc] = src; return src; }; - foreach (var m in image.GetTypes ().SelectMany (t => t.Methods)) { + foreach (var m in image.GetTypes().SelectMany(t => t.Methods)) { Document first_doc = null; foreach (var sp in m.DebugInformation.SequencePoints) { - if (first_doc == null) { + if (first_doc == null && !sp.Document.Url.EndsWith (".g.cs")) { first_doc = sp.Document; - } else if (first_doc != sp.Document) { - //FIXME this is needed for (c)ctors in corlib - throw new Exception ($"Cant handle multi-doc methods in {m}"); } + // else if (first_doc != sp.Document) { + // //FIXME this is needed for (c)ctors in corlib + // throw new Exception ($"Cant handle multi-doc methods in {m}"); + //} } - var src = get_src (first_doc); - var mi = new MethodInfo (this, m, src); - int mt = (int)m.MetadataToken.RID; - this.methods [mt] = mi; - if (src != null) - src.AddMethod (mi); + if (first_doc == null) { + // all generated files + first_doc = m.DebugInformation.SequencePoints.FirstOrDefault ()?.Document; + } + if (first_doc != null) { + var src = get_src (first_doc); + var mi = new MethodInfo (this, m, src); + int mt = (int)m.MetadataToken.RID; + this.methods [mt] = mi; + if (src != null) + src.AddMethod (mi); + } } } + private void ProcessSourceLink () + { + var sourceLinkDebugInfo = image.CustomDebugInformations.FirstOrDefault (i => i.Kind == CustomDebugInformationKind.SourceLink); + + if (sourceLinkDebugInfo != null) { + var sourceLinkContent = ((SourceLinkDebugInformation)sourceLinkDebugInfo).Content; + + if (sourceLinkContent != null) { + var jObject = JObject.Parse (sourceLinkContent) ["documents"]; + sourceLinkMappings = JsonConvert.DeserializeObject> (jObject.ToString ()); + } + } + } + + private Uri GetSourceLinkUrl (string document) + { + if (sourceLinkMappings.TryGetValue (document, out string url)) { + return new Uri (url); + } + + foreach (var sourceLinkDocument in sourceLinkMappings) { + string key = sourceLinkDocument.Key; + + if (Path.GetFileName (key) != "*") { + continue; + } + + var keyTrim = key.TrimEnd ('*'); + + if (document.StartsWith(keyTrim, StringComparison.OrdinalIgnoreCase)) { + var docUrlPart = document.Replace (keyTrim, ""); + return new Uri (sourceLinkDocument.Value.TrimEnd ('*') + docUrlPart); + } + } + + return null; + } + + private string GetRelativePath (string relativeTo, string path) + { + var uri = new Uri (relativeTo, UriKind.RelativeOrAbsolute); + var rel = Uri.UnescapeDataString (uri.MakeRelativeUri (new Uri (path, UriKind.RelativeOrAbsolute)).ToString ()).Replace (Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + if (rel.Contains (Path.DirectorySeparatorChar.ToString ()) == false) { + rel = $".{ Path.DirectorySeparatorChar }{ rel }"; + } + return rel; + } + public IEnumerable Sources { get { return this.sources; } } @@ -342,9 +445,9 @@ namespace WsProxy { public MethodInfo GetMethodByToken (int token) { - return methods [token]; + methods.TryGetValue (token, out var value); + return value; } - } internal class SourceFile { @@ -353,23 +456,36 @@ namespace WsProxy { int id; Document doc; - internal SourceFile (AssemblyInfo assembly, int id, Document doc) + internal SourceFile (AssemblyInfo assembly, int id, Document doc, Uri sourceLinkUri) { this.methods = new HashSet (); + this.SourceLinkUri = sourceLinkUri; this.assembly = assembly; this.id = id; this.doc = doc; + this.DebuggerFileName = doc.Url.Replace ("\\", "/").Replace (":", ""); + + this.SourceUri = new Uri ((Path.IsPathRooted (doc.Url) ? "file://" : "") + doc.Url, UriKind.RelativeOrAbsolute); + if (SourceUri.IsFile && File.Exists (SourceUri.LocalPath)) { + this.Url = this.SourceUri.ToString (); + } else { + this.Url = DotNetUrl; + } + } internal void AddMethod (MethodInfo mi) { this.methods.Add (mi); } - public string FileName => Path.GetFileName (doc.Url); - public string Url => $"dotnet://{assembly.Name}/{FileName}"; + public string DebuggerFileName { get; } + public string Url { get; } + public string AssemblyName => assembly.Name; + public string DotNetUrl => $"dotnet://{assembly.Name}/{DebuggerFileName}"; public string DocHashCode => "abcdee" + id; public SourceId SourceId => new SourceId (assembly.Id, this.id); - public string LocalPath => doc.Url; + public Uri SourceLinkUri { get; } + public Uri SourceUri { get; } public IEnumerable Methods => this.methods; } @@ -377,17 +493,18 @@ namespace WsProxy { internal class DebugStore { List assemblies = new List (); - public DebugStore (string[] loaded_files) + public DebugStore (string [] loaded_files) { - bool MatchPdb (string asm, string pdb) { + bool MatchPdb (string asm, string pdb) + { return Path.ChangeExtension (asm, "pdb") == pdb; } var asm_files = new List (); var pdb_files = new List (); foreach (var f in loaded_files) { - var file_name = f.ToLower (); - if (file_name.EndsWith (".pdb", StringComparison.Ordinal)) + var file_name = f; + if (file_name.EndsWith (".pdb", StringComparison.OrdinalIgnoreCase)) pdb_files.Add (file_name); else asm_files.Add (file_name); @@ -395,14 +512,18 @@ namespace WsProxy { //FIXME make this parallel foreach (var p in asm_files) { - var pdb = pdb_files.FirstOrDefault (n => MatchPdb (p, n)); - HttpClient h = new HttpClient (); - var assembly_bytes = h.GetByteArrayAsync (p).Result; - byte[] pdb_bytes = null; - if (pdb != null) - pdb_bytes = h.GetByteArrayAsync (pdb).Result; + try { + var pdb = pdb_files.FirstOrDefault (n => MatchPdb (p, n)); + HttpClient h = new HttpClient (); + var assembly_bytes = h.GetByteArrayAsync (p).Result; + byte [] pdb_bytes = null; + if (pdb != null) + pdb_bytes = h.GetByteArrayAsync (pdb).Result; - this.assemblies.Add (new AssemblyInfo (assembly_bytes, pdb_bytes)); + this.assemblies.Add (new AssemblyInfo (assembly_bytes, pdb_bytes)); + } catch (Exception e) { + Console.WriteLine ($"Failed to read {p} ({e.Message})"); + } } } @@ -412,7 +533,6 @@ namespace WsProxy { foreach (var s in a.Sources) yield return s; } - } public SourceFile GetFileById (SourceId id) @@ -426,26 +546,23 @@ namespace WsProxy { } /* - Matching logic here is hilarious and it goes like this: - We inject one line at the top of all sources to make it easy to identify them [1]. V8 uses zero based indexing for both line and column. PPDBs uses one based indexing for both line and column. - Which means that: - - for lines, values are already adjusted (v8 numbers come +1 due to the injected line) - - for columns, we need to +1 the v8 numbers - [1] It's so we can deal with the Runtime.compileScript ide cmd */ static bool Match (SequencePoint sp, SourceLocation start, SourceLocation end) { - if (start.Line > sp.StartLine) + var spStart = (Line: sp.StartLine - 1, Column: sp.StartColumn - 1); + var spEnd = (Line: sp.EndLine - 1, Column: sp.EndColumn - 1); + + if (start.Line > spStart.Line) return false; - if ((start.Column + 1) > sp.StartColumn && start.Line == sp.StartLine) + if (start.Column > spStart.Column && start.Line == sp.StartLine) return false; - if (end.Line < sp.EndLine) + if (end.Line < spEnd.Line) return false; - if ((end.Column + 1) < sp.EndColumn && end.Line == sp.EndLine) + if (end.Column < spEnd.Column && end.Line == spEnd.Line) return false; return true; @@ -477,28 +594,24 @@ namespace WsProxy { } /* - Matching logic here is hilarious and it goes like this: - We inject one line at the top of all sources to make it easy to identify them [1]. V8 uses zero based indexing for both line and column. PPDBs uses one based indexing for both line and column. - Which means that: - - for lines, values are already adjusted (v8 numbers come + 1 due to the injected line) - - for columns, we need to +1 the v8 numbers - [1] It's so we can deal with the Runtime.compileScript ide cmd */ static bool Match (SequencePoint sp, int line, int column) { - if (sp.StartLine > line || sp.EndLine < line) + var bp = (line: line + 1, column: column + 1); + + if (sp.StartLine > bp.line || sp.EndLine < bp.line) return false; //Chrome sends a zero column even if getPossibleBreakpoints say something else if (column == 0) return true; - if (sp.StartColumn > (column + 1) && sp.StartLine == line) + if (sp.StartColumn > bp.column && sp.StartLine == bp.line) return false; - if (sp.EndColumn < (column + 1) && sp.EndLine == line) + if (sp.EndColumn < bp.column && sp.EndLine == bp.line) return false; return true; @@ -506,8 +619,11 @@ namespace WsProxy { public SourceLocation FindBestBreakpoint (BreakPointRequest req) { - var asm = this.assemblies.FirstOrDefault (a => a.Name == req.Assembly); - var src = asm.Sources.FirstOrDefault (s => s.FileName == req.File); + var asm = assemblies.FirstOrDefault (a => a.Name.Equals (req.Assembly, StringComparison.OrdinalIgnoreCase)); + var src = asm?.Sources?.FirstOrDefault (s => s.DebuggerFileName.Equals (req.File, StringComparison.OrdinalIgnoreCase)); + + if (src == null) + return null; foreach (var m in src.Methods) { foreach (var sp in m.methodDef.DebugInformation.SequencePoints) { @@ -521,8 +637,15 @@ namespace WsProxy { } public string ToUrl (SourceLocation location) + => location != null ? GetFileById (location.Id).Url : ""; + + public SourceFile GetFileByUrlRegex (string urlRegex) { - return GetFileById (location.Id).Url; + var regex = new Regex (urlRegex); + return AllSources ().FirstOrDefault (file => regex.IsMatch (file.Url.ToString())); } + + public SourceFile GetFileByUrl (string url) + => AllSources ().FirstOrDefault (file => file.Url.ToString() == url); } } diff --git a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs index ae6343bca4..6ff776bffe 100644 --- a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs +++ b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs @@ -8,6 +8,7 @@ using System.Threading; using System.IO; using System.Text; using System.Collections.Generic; +using System.Net; namespace WsProxy { @@ -20,6 +21,8 @@ namespace WsProxy { public const string REMOVE_BREAK_POINT = "MONO.mono_wasm_remove_breakpoint({0})"; public const string GET_LOADED_FILES = "MONO.mono_wasm_get_loaded_files()"; public const string CLEAR_ALL_BREAKPOINTS = "MONO.mono_wasm_clear_all_breakpoints()"; + public const string GET_OBJECT_PROPERTIES = "MONO.mono_wasm_get_object_properties({0})"; + public const string GET_ARRAY_VALUES = "MONO.mono_wasm_get_array_values({0})"; } internal enum MonoErrorCodes { @@ -128,7 +131,7 @@ namespace WsProxy { case "Debugger.getScriptSource": { var script_id = args? ["scriptId"]?.Value (); if (script_id.StartsWith ("dotnet://", StringComparison.InvariantCultureIgnoreCase)) { - OnGetScriptSource (id, script_id, token); + await OnGetScriptSource (id, script_id, token); return true; } @@ -154,7 +157,7 @@ namespace WsProxy { case "Debugger.setBreakpointByUrl": { Info ($"BP req {args}"); - var bp_req = BreakPointRequest.Parse (args); + var bp_req = BreakPointRequest.Parse (args, store); if (bp_req != null) { await SetBreakPoint (id, bp_req, token); return true; @@ -200,7 +203,14 @@ namespace WsProxy { await GetScopeProperties (id, int.Parse (objId.Substring ("dotnet:scope:".Length)), token); return true; } - + if (objId.StartsWith("dotnet:", StringComparison.InvariantCulture)) + { + if (objId.StartsWith("dotnet:object:", StringComparison.InvariantCulture)) + await GetDetails(id, int.Parse(objId.Substring("dotnet:object:".Length)), token, MonoCommands.GET_OBJECT_PROPERTIES); + if (objId.StartsWith("dotnet:array:", StringComparison.InvariantCulture)) + await GetDetails(id, int.Parse(objId.Substring("dotnet:array:".Length)), token, MonoCommands.GET_ARRAY_VALUES); + return true; + } break; } } @@ -213,6 +223,7 @@ namespace WsProxy { Info ("RUNTIME READY, PARTY TIME"); await RuntimeReady (token); await SendCommand ("Debugger.resume", new JObject (), token); + SendEvent ("Mono.runtimeReady", new JObject (), token); } async Task OnBreakPointHit (JObject args, CancellationToken token) @@ -257,9 +268,9 @@ namespace WsProxy { var src = bp == null ? null : store.GetFileById (bp.Location.Id); var callFrames = new List (); - foreach (var f in orig_callframes) { - var function_name = f ["functionName"]?.Value (); - var url = f ["url"]?.Value (); + foreach (var frame in orig_callframes) { + var function_name = frame ["functionName"]?.Value (); + var url = frame ["url"]?.Value (); if ("mono_wasm_fire_bp" == function_name || "_mono_wasm_fire_bp" == function_name) { var frames = new List (); int frame_id = 0; @@ -271,14 +282,19 @@ namespace WsProxy { var asm = store.GetAssemblyByName (assembly_name); var method = asm.GetMethodByToken (method_token); - var location = method.GetLocationByIl (il_pos); + + if (method == null) { + Info ($"Unable to find il offset: {il_pos} in method token: {method_token} assembly name: {assembly_name}"); + continue; + } + + var location = method?.GetLocationByIl (il_pos); // When hitting a breakpoint on the "IncrementCount" method in the standard // Blazor project template, one of the stack frames is inside mscorlib.dll // and we get location==null for it. It will trigger a NullReferenceException // if we don't skip over that stack frame. - if (location == null) - { + if (location == null) { continue; } @@ -288,7 +304,7 @@ namespace WsProxy { callFrames.Add (JObject.FromObject (new { functionName = method.Name, - + callFrameId = $"dotnet:scope:{frame_id}", functionLocation = method.StartLocation.ToJObject (), location = location.ToJObject (), @@ -300,25 +316,24 @@ namespace WsProxy { type = "local", @object = new { @type = "object", - className = "Object", + className = "Object", description = "Object", - objectId = $"dotnet:scope:{frame_id}" + objectId = $"dotnet:scope:{frame_id}", }, name = method.Name, startLocation = method.StartLocation.ToJObject (), endLocation = method.EndLocation.ToJObject (), - } - }, - - @this = new { - } + }}, + @this = new { } })); ++frame_id; this.current_callstack = frames; + } - } else if (!url.StartsWith ("wasm://wasm/", StringComparison.InvariantCulture)) { - callFrames.Add (f); + } else if (!(function_name.StartsWith ("wasm-function", StringComparison.InvariantCulture) + || url.StartsWith ("wasm://wasm/", StringComparison.InvariantCulture))) { + callFrames.Add (frame); } } @@ -393,6 +408,57 @@ namespace WsProxy { await SendCommand ("Debugger.resume", new JObject (), token); } + async Task GetDetails(int msg_id, int object_id, CancellationToken token, string command) + { + var o = JObject.FromObject(new + { + expression = string.Format(command, object_id), + objectGroup = "mono_debugger", + includeCommandLineAPI = false, + silent = false, + returnByValue = true, + }); + + var res = await SendCommand("Runtime.evaluate", o, token); + + //if we fail we just buble that to the IDE (and let it panic over it) + if (res.IsErr) + { + SendResponse(msg_id, res, token); + return; + } + + var values = res.Value?["result"]?["value"]?.Values().ToArray(); + + var var_list = new List(); + + // Trying to inspect the stack frame for DotNetDispatcher::InvokeSynchronously + // results in a "Memory access out of bounds", causing 'values' to be null, + // so skip returning variable values in that case. + for (int i = 0; i < values.Length; i+=2) + { + string fieldName = (string)values[i]["name"]; + if (fieldName.Contains("k__BackingField")){ + fieldName = fieldName.Replace("k__BackingField", ""); + fieldName = fieldName.Replace("<", ""); + fieldName = fieldName.Replace(">", ""); + } + var_list.Add(JObject.FromObject(new + { + name = fieldName, + value = values[i+1]["value"] + })); + + } + o = JObject.FromObject(new + { + result = var_list + }); + + SendResponse(msg_id, Result.Ok(o), token); + } + + async Task GetScopeProperties (int msg_id, int scope_id, CancellationToken token) { var scope = this.current_callstack.FirstOrDefault (s => s.Id == scope_id); @@ -425,6 +491,10 @@ namespace WsProxy { // results in a "Memory access out of bounds", causing 'values' to be null, // so skip returning variable values in that case. for (int i = 0; values != null && i < vars.Length; ++i) { + var value = values [i] ["value"]; + if (((string)value ["description"]) == null) + value ["description"] = value ["value"]?.ToString(); + var_list.Add (JObject.FromObject (new { name = vars [i].Name, value = values [i] ["value"] @@ -485,7 +555,8 @@ namespace WsProxy { url = s.Url, executionContextId = this.ctx_id, hash = s.DocHashCode, - executionContextAuxData = this.aux_ctx_data + executionContextAuxData = this.aux_ctx_data, + dotNetUrl = s.DotNetUrl }); //Debug ($"\tsending {s.Url}"); SendEvent ("Debugger.scriptParsed", ok, token); @@ -640,23 +711,51 @@ namespace WsProxy { } - void OnGetScriptSource (int msg_id, string script_id, CancellationToken token) + async Task OnGetScriptSource (int msg_id, string script_id, CancellationToken token) { var id = new SourceId (script_id); var src_file = store.GetFileById (id); var res = new StringWriter (); - res.WriteLine ($"//dotnet:{id}"); + //res.WriteLine ($"//{id}"); - using (var f = new StreamReader (File.Open (src_file.LocalPath, FileMode.Open))) { - res.Write (f.ReadToEnd ()); + try { + var uri = new Uri (src_file.Url); + if (uri.IsFile && File.Exists(uri.LocalPath)) { + using (var f = new StreamReader (File.Open (src_file.SourceUri.LocalPath, FileMode.Open))) { + await res.WriteAsync (await f.ReadToEndAsync ()); + } + + var o = JObject.FromObject (new { + scriptSource = res.ToString () + }); + + SendResponse (msg_id, Result.Ok (o), token); + } else if(src_file.SourceLinkUri != null) { + var doc = await new WebClient ().DownloadStringTaskAsync (src_file.SourceLinkUri); + await res.WriteAsync (doc); + + var o = JObject.FromObject (new { + scriptSource = res.ToString () + }); + + SendResponse (msg_id, Result.Ok (o), token); + } else { + var o = JObject.FromObject (new { + scriptSource = $"// Unable to find document {src_file.SourceUri}" + }); + + SendResponse (msg_id, Result.Ok (o), token); + } + } catch (Exception e) { + var o = JObject.FromObject (new { + scriptSource = $"// Unable to read document ({e.Message})\n" + + $"Local path: {src_file?.SourceUri}\n" + + $"SourceLink path: {src_file?.SourceLinkUri}\n" + }); + + SendResponse (msg_id, Result.Ok (o), token); } - - var o = JObject.FromObject (new { - scriptSource = res.ToString () - }); - - SendResponse (msg_id, Result.Ok (o), token); } } } diff --git a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/WsProxy.cs b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/WsProxy.cs index 0629491680..e575e0a244 100644 --- a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/WsProxy.cs +++ b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/WsProxy.cs @@ -144,7 +144,7 @@ namespace WsProxy { void Send (WebSocket to, JObject o, CancellationToken token) { - var bytes = Encoding.UTF8.GetBytes (o.ToString ()); + var bytes = Encoding.UTF8.GetBytes (o.ToString ()); var queue = GetQueueForSocket (to); var task = queue.Send (bytes, token); @@ -256,7 +256,7 @@ namespace WsProxy { } // , HttpContext context) - public async Task Run (Uri browserUri, WebSocket ideSocket) + public async Task Run (Uri browserUri, WebSocket ideSocket) { Debug ("wsproxy start"); using (this.ide = ideSocket) { @@ -276,7 +276,7 @@ namespace WsProxy { try { while (!x.IsCancellationRequested) { - var task = await Task.WhenAny (pending_ops); + var task = await Task.WhenAny (pending_ops.ToArray ()); //Console.WriteLine ("pump {0} {1}", task, pending_ops.IndexOf (task)); if (task == pending_ops [0]) { var msg = ((Task)task).Result; diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs index 19c9249e47..72db875d48 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs @@ -49,6 +49,32 @@ namespace Microsoft.AspNetCore.Components.RenderTree // Built-in components: https://github.com/aspnet/AspNetCore/issues/8825 namespace Microsoft.AspNetCore.Components { + public partial class AuthorizeView : Microsoft.AspNetCore.Components.ComponentBase + { + public AuthorizeView() { } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment Authorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + [System.Diagnostics.DebuggerStepThroughAttribute] + protected override System.Threading.Tasks.Task OnParametersSetAsync() { throw null; } + } + + public partial class CascadingAuthenticationState : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable + { + public CascadingAuthenticationState() { } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + protected override void OnInit() { } + void System.IDisposable.Dispose() { } + } + public partial class CascadingValue : Microsoft.AspNetCore.Components.IComponent { public CascadingValue() { } diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs index 1729a18ed1..a1b3d1e2f8 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs @@ -16,21 +16,6 @@ namespace Microsoft.AspNetCore.Components public abstract System.Threading.Tasks.Task GetAuthenticationStateAsync(); protected void NotifyAuthenticationStateChanged(System.Threading.Tasks.Task task) { } } - public partial class AuthorizeView : Microsoft.AspNetCore.Components.ComponentBase - { - public AuthorizeView() { } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment Authorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } - [System.Diagnostics.DebuggerStepThroughAttribute] - protected override System.Threading.Tasks.Task OnParametersSetAsync() { throw null; } - } [Microsoft.AspNetCore.Components.BindElementAttribute("select", null, "value", "onchange")] [Microsoft.AspNetCore.Components.BindElementAttribute("textarea", null, "value", "onchange")] [Microsoft.AspNetCore.Components.BindInputElementAttribute("checkbox", null, "checked", "onchange")] @@ -85,15 +70,6 @@ namespace Microsoft.AspNetCore.Components public static System.Action SetValueHandler(System.Action setter, string existingValue) { throw null; } public static System.Action SetValueHandler(System.Action setter, T existingValue) { throw null; } } - public partial class CascadingAuthenticationState : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable - { - public CascadingAuthenticationState() { } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } - protected override void OnInit() { } - void System.IDisposable.Dispose() { } - } [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false, Inherited=false)] public sealed partial class CascadingParameterAttribute : System.Attribute { diff --git a/src/Components/Components/src/Auth/AuthenticationState.cs b/src/Components/Components/src/Auth/AuthenticationState.cs index 2a145d44f3..d90090c7c8 100644 --- a/src/Components/Components/src/Auth/AuthenticationState.cs +++ b/src/Components/Components/src/Auth/AuthenticationState.cs @@ -11,11 +11,6 @@ namespace Microsoft.AspNetCore.Components /// public class AuthenticationState { - /// - /// Gets a that describes the current user. - /// - public ClaimsPrincipal User { get; } - /// /// Constructs an instance of . /// @@ -24,5 +19,10 @@ namespace Microsoft.AspNetCore.Components { User = user ?? throw new ArgumentNullException(nameof(user)); } + + /// + /// Gets a that describes the current user. + /// + public ClaimsPrincipal User { get; } } } diff --git a/src/Components/Components/src/Auth/AuthenticationStateProvider.cs b/src/Components/Components/src/Auth/AuthenticationStateProvider.cs index ffd2fc252a..e9e3e3c772 100644 --- a/src/Components/Components/src/Auth/AuthenticationStateProvider.cs +++ b/src/Components/Components/src/Auth/AuthenticationStateProvider.cs @@ -12,19 +12,16 @@ namespace Microsoft.AspNetCore.Components public abstract class AuthenticationStateProvider { /// - /// Gets an instance that describes - /// the current user. + /// Asynchronously gets an that describes the current user. /// - /// An instance that describes the current user. + /// A task that, when resolved, gives an instance that describes the current user. public abstract Task GetAuthenticationStateAsync(); /// /// An event that provides notification when the /// has changed. For example, this event may be raised if a user logs in or out. /// -#pragma warning disable 0067 // "Never used" (it's only raised by subclasses) public event AuthenticationStateChangedHandler AuthenticationStateChanged; -#pragma warning restore 0067 /// /// Raises the event. diff --git a/src/Components/Components/src/UriHelperBase.cs b/src/Components/Components/src/UriHelperBase.cs index 41af519eca..3b8f091045 100644 --- a/src/Components/Components/src/UriHelperBase.cs +++ b/src/Components/Components/src/UriHelperBase.cs @@ -154,7 +154,10 @@ namespace Microsoft.AspNetCore.Components // baseUri ends with a slash), and from that we return "something" return locationAbsolute.Substring(baseUri.Length); } - else if ($"{locationAbsolute}/".Equals(baseUri, StringComparison.Ordinal)) + + var hashIndex = locationAbsolute.IndexOf('#'); + var locationAbsoluteNoHash = hashIndex < 0 ? locationAbsolute : locationAbsolute.Substring(0, hashIndex); + if ($"{locationAbsoluteNoHash}/".Equals(baseUri, StringComparison.Ordinal)) { // Special case: for the base URI "/something/", if you're at // "/something" then treat it as if you were at "/something/" (i.e., @@ -162,7 +165,7 @@ namespace Microsoft.AspNetCore.Components // whether the server would return the same page whether or not the // slash is present, but ASP.NET Core at least does by default when // using PathBase. - return string.Empty; + return locationAbsolute.Substring(baseUri.Length - 1); } var message = $"The URI '{locationAbsolute}' is not contained by the base URI '{baseUri}'."; diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index c15d1d9bac..be4994a83b 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -2577,7 +2577,7 @@ namespace Microsoft.AspNetCore.Components.Test } [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/7487 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/7487")] public async Task CanTriggerEventHandlerDisposedInEarlierPendingBatchAsync() { // This represents the scenario where the same event handler is being triggered diff --git a/src/Components/test/E2ETest/Infrastructure/BasicTestAppTestBase.cs b/src/Components/test/E2ETest/Infrastructure/BasicTestAppTestBase.cs index d4933b858e..62d62f0d68 100644 --- a/src/Components/test/E2ETest/Infrastructure/BasicTestAppTestBase.cs +++ b/src/Components/test/E2ETest/Infrastructure/BasicTestAppTestBase.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure public class BasicTestAppTestBase : ServerTestBase> { public string ServerPathBase - => "/subdir" + (_serverFixture.UsingAspNetHost ? "#server" : ""); + => "/subdir" + (_serverFixture.ExecutionMode == ExecutionMode.Server ? "#server" : ""); public BasicTestAppTestBase( BrowserFixture browserFixture, diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/ToggleExecutionModeServerFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/ToggleExecutionModeServerFixture.cs index 9fb9863f60..759b871852 100644 --- a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/ToggleExecutionModeServerFixture.cs +++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/ToggleExecutionModeServerFixture.cs @@ -9,7 +9,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures : ServerFixture { public string PathBase { get; set; } - public bool UsingAspNetHost { get; private set; } + + public ExecutionMode ExecutionMode { get; set; } = ExecutionMode.Client; private AspNetSiteServerFixture.BuildWebHost _buildWebHostMethod; private IDisposable _serverToDispose; @@ -18,7 +19,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures { _buildWebHostMethod = buildWebHostMethod ?? throw new ArgumentNullException(nameof(buildWebHostMethod)); - UsingAspNetHost = true; } protected override string StartAndGetRootUri() @@ -46,4 +46,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures _serverToDispose?.Dispose(); } } + + public enum ExecutionMode { Client, Server } } diff --git a/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs index be5e996196..b7b6d361c8 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs @@ -8,7 +8,7 @@ using OpenQA.Selenium; using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.Components.E2ETests.ServerExecutionTests +namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { public class PrerenderingTest : ServerTestBase { diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ServerExecutionTestExtensions.cs b/src/Components/test/E2ETest/ServerExecutionTests/ServerExecutionTestExtensions.cs index e867e03a9b..c8e34f36cd 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/ServerExecutionTestExtensions.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/ServerExecutionTestExtensions.cs @@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests public static ToggleExecutionModeServerFixture WithServerExecution(this ToggleExecutionModeServerFixture serverFixture) { serverFixture.UseAspNetHost(TestServer.Program.BuildWebHost); + serverFixture.ExecutionMode = ExecutionMode.Server; return serverFixture; } } diff --git a/src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs b/src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs index 9265718d54..1f16095af5 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs @@ -81,4 +81,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { } } + + public class ServerAuthTest : AuthTest + { + public ServerAuthTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture serverFixture, ITestOutputHelper output) + : base(browserFixture, serverFixture.WithServerExecution(), output) + { + } + } } diff --git a/src/Components/test/E2ETest/Tests/AuthTest.cs b/src/Components/test/E2ETest/Tests/AuthTest.cs new file mode 100644 index 0000000000..cc3139b6ab --- /dev/null +++ b/src/Components/test/E2ETest/Tests/AuthTest.cs @@ -0,0 +1,92 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using BasicTestApp; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETest.Tests +{ + public class AuthTest : BasicTestAppTestBase + { + // These strings correspond to the links in BasicTestApp\AuthTest\Links.razor + const string CascadingAuthenticationStateLink = "Cascading authentication state"; + const string AuthorizeViewCases = "AuthorizeView cases"; + + public AuthTest( + BrowserFixture browserFixture, + ToggleExecutionModeServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + // Normally, the E2E tests use the Blazor dev server if they are testing + // client-side execution. But for the auth tests, we always have to run + // in "hosted on ASP.NET Core" mode, because we get the auth state from it. + serverFixture.UseAspNetHost(TestServer.Program.BuildWebHost); + } + + [Fact] + public void CascadingAuthenticationState_Unauthenticated() + { + SignInAs(null); + + var appElement = MountAndNavigateToAuthTest(CascadingAuthenticationStateLink); + + Browser.Equal("False", () => appElement.FindElement(By.Id("identity-authenticated")).Text); + Browser.Equal(string.Empty, () => appElement.FindElement(By.Id("identity-name")).Text); + Browser.Equal("(none)", () => appElement.FindElement(By.Id("test-claim")).Text); + } + + [Fact] + public void CascadingAuthenticationState_Authenticated() + { + SignInAs("someone cool"); + + var appElement = MountAndNavigateToAuthTest(CascadingAuthenticationStateLink); + + Browser.Equal("True", () => appElement.FindElement(By.Id("identity-authenticated")).Text); + Browser.Equal("someone cool", () => appElement.FindElement(By.Id("identity-name")).Text); + Browser.Equal("Test claim value", () => appElement.FindElement(By.Id("test-claim")).Text); + } + + [Fact] + public void AuthorizeViewCases_NoAuthorizationRule_Unauthenticated() + { + SignInAs(null); + MountAndNavigateToAuthTest(AuthorizeViewCases); + WaitUntilExists(By.CssSelector("#no-authorization-rule .not-authorized")); + } + + [Fact] + public void AuthorizeViewCases_NoAuthorizationRule_Authenticated() + { + SignInAs("Some User"); + var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); + Browser.Equal("Welcome, Some User!", () => + appElement.FindElement(By.CssSelector("#no-authorization-rule .authorized")).Text); + } + + IWebElement MountAndNavigateToAuthTest(string authLinkText) + { + Navigate(ServerPathBase); + var appElement = MountTestComponent(); + WaitUntilExists(By.Id("auth-links")); + appElement.FindElement(By.LinkText(authLinkText)).Click(); + return appElement; + } + + void SignInAs(string usernameOrNull) + { + const string authenticationPageUrl = "/Authentication"; + var baseRelativeUri = usernameOrNull == null + ? $"{authenticationPageUrl}?signout=true" + : $"{authenticationPageUrl}?username={usernameOrNull}"; + Navigate(baseRelativeUri); + WaitUntilExists(By.CssSelector("h1#authentication")); + } + } +} diff --git a/src/Components/test/E2ETest/Tests/BindTest.cs b/src/Components/test/E2ETest/Tests/BindTest.cs index b08a315441..51fcc9ea9d 100644 --- a/src/Components/test/E2ETest/Tests/BindTest.cs +++ b/src/Components/test/E2ETest/Tests/BindTest.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests protected override void InitializeAsyncCore() { // On WebAssembly, page reloads are expensive so skip if possible - Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost); + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); MountTestComponent(); WaitUntilExists(By.Id("bind-cases")); } diff --git a/src/Components/test/E2ETest/Tests/CascadingValueTest.cs b/src/Components/test/E2ETest/Tests/CascadingValueTest.cs index 8102be1e58..e4642fa98c 100644 --- a/src/Components/test/E2ETest/Tests/CascadingValueTest.cs +++ b/src/Components/test/E2ETest/Tests/CascadingValueTest.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests protected override void InitializeAsyncCore() { - Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost); + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); MountTestComponent(); } diff --git a/src/Components/test/E2ETest/Tests/ComponentRenderingTest.cs b/src/Components/test/E2ETest/Tests/ComponentRenderingTest.cs index 5c8e194cac..c88d2997c6 100644 --- a/src/Components/test/E2ETest/Tests/ComponentRenderingTest.cs +++ b/src/Components/test/E2ETest/Tests/ComponentRenderingTest.cs @@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests protected override void InitializeAsyncCore() { - Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost); + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); } [Fact] diff --git a/src/Components/test/E2ETest/Tests/EventBubblingTest.cs b/src/Components/test/E2ETest/Tests/EventBubblingTest.cs index a15d1b31d4..f45e3d106a 100644 --- a/src/Components/test/E2ETest/Tests/EventBubblingTest.cs +++ b/src/Components/test/E2ETest/Tests/EventBubblingTest.cs @@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests protected override void InitializeAsyncCore() { - Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost); + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); MountTestComponent(); WaitUntilExists(By.Id("event-bubbling")); } diff --git a/src/Components/test/E2ETest/Tests/EventCallbackTest.cs b/src/Components/test/E2ETest/Tests/EventCallbackTest.cs index 4e5472a88a..01d53d6b20 100644 --- a/src/Components/test/E2ETest/Tests/EventCallbackTest.cs +++ b/src/Components/test/E2ETest/Tests/EventCallbackTest.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests protected override void InitializeAsyncCore() { // On WebAssembly, page reloads are expensive so skip if possible - Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost); + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); MountTestComponent(); } diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs index 88f2a0d9fb..9f1083af20 100644 --- a/src/Components/test/E2ETest/Tests/FormsTest.cs +++ b/src/Components/test/E2ETest/Tests/FormsTest.cs @@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests protected override void InitializeAsyncCore() { // On WebAssembly, page reloads are expensive so skip if possible - Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost); + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); } [Fact] diff --git a/src/Components/test/E2ETest/Tests/InteropTest.cs b/src/Components/test/E2ETest/Tests/InteropTest.cs index 4b82c98096..7ab262c465 100644 --- a/src/Components/test/E2ETest/Tests/InteropTest.cs +++ b/src/Components/test/E2ETest/Tests/InteropTest.cs @@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests // Include the sync assertions only when running under WebAssembly var expectedValues = expectedAsyncValues; - if (!_serverFixture.UsingAspNetHost) + if (_serverFixture.ExecutionMode == ExecutionMode.Client) { foreach (var kvp in expectedSyncValues) { diff --git a/src/Components/test/E2ETest/Tests/KeyTest.cs b/src/Components/test/E2ETest/Tests/KeyTest.cs index b7a5519694..2cb0e58d18 100644 --- a/src/Components/test/E2ETest/Tests/KeyTest.cs +++ b/src/Components/test/E2ETest/Tests/KeyTest.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests protected override void InitializeAsyncCore() { // On WebAssembly, page reloads are expensive so skip if possible - Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost); + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); } [Fact] diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/AuthHome.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthHome.razor new file mode 100644 index 0000000000..863f865a98 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthHome.razor @@ -0,0 +1,3 @@ +@page "/AuthHome" + +Select an auth test below. diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor new file mode 100644 index 0000000000..299a5beb87 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor @@ -0,0 +1,30 @@ +@using Microsoft.AspNetCore.Components.Routing +@inject IUriHelper UriHelper + +@* + This router is independent of any other router that may exist within the same project. + It exists so that (1) we can easily have multiple test cases that depend on the + CascadingAuthenticationState, and (2) we can test the integration between the router + and @page authorization rules. +*@ + + + + + +
+ + +@functions { + protected override void OnInit() + { + // Start at AuthHome, not at any other component in the same app that happens to + // register itself for the route "" + var absoluteUriPath = new Uri(UriHelper.GetAbsoluteUri()).GetLeftPart(UriPartial.Path); + var relativeUri = UriHelper.ToBaseRelativePath(UriHelper.GetBaseUri(), absoluteUriPath); + if (relativeUri == string.Empty) + { + UriHelper.NavigateTo("AuthHome"); + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/AuthorizeViewCases.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthorizeViewCases.razor new file mode 100644 index 0000000000..d78c70f72a --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthorizeViewCases.razor @@ -0,0 +1,17 @@ +@page "/AuthorizeViewCases" + +
+

Scenario: No authorization rule

+ + + +

Authorizing...

+
+ +

Welcome, @context.User.Identity.Name!

+
+ +

You're not logged in.

+
+
+
diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/CascadingAuthenticationStateConsumer.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/CascadingAuthenticationStateConsumer.razor new file mode 100644 index 0000000000..8113e1080f --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/CascadingAuthenticationStateConsumer.razor @@ -0,0 +1,47 @@ +@page "/CascadingAuthenticationStateConsumer" +@using System.Security.Claims + +

Cascading authentication state

+ +@if (user == null) +{ + Requesting authentication state... +} +else +{ +

+ Authenticated: + @user.Identity.IsAuthenticated +

+ +

+ Name: + @user.Identity.Name +

+ +

+ Test claim: + @if (user.HasClaim(TestClaimPredicate) == true) + { + @user.Claims.Single(c => TestClaimPredicate(c)).Value + } + else + { + (none) + } +

+} + +@functions +{ + static Predicate TestClaimPredicate = c => c.Type == "test-claim"; + + ClaimsPrincipal user; + + [CascadingParameter] Task AuthenticationStateTask { get; set; } + + protected override async Task OnParametersSetAsync() + { + user = (await AuthenticationStateTask).User; + } +} diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/ClientSideAuthenticationStateData.cs b/src/Components/test/testassets/BasicTestApp/AuthTest/ClientSideAuthenticationStateData.cs new file mode 100644 index 0000000000..15178b5d82 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/ClientSideAuthenticationStateData.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace BasicTestApp.AuthTest +{ + // DTO shared between server and client + public class ClientSideAuthenticationStateData + { + public bool IsAuthenticated { get; set; } + + public string UserName { get; set; } + + public Dictionary ExposedClaims { get; set; } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor new file mode 100644 index 0000000000..70f524a763 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor @@ -0,0 +1,8 @@ +@using Microsoft.AspNetCore.Components.Routing + + +

To change the underlying authentication state, go here.

diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/ServerAuthenticationStateProvider.cs b/src/Components/test/testassets/BasicTestApp/AuthTest/ServerAuthenticationStateProvider.cs new file mode 100644 index 0000000000..08639f9254 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/ServerAuthenticationStateProvider.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; + +namespace BasicTestApp.AuthTest +{ + // This is intended to be similar to the authentication stateprovider included by default + // with the client-side Blazor "Hosted in ASP.NET Core" template + public class ServerAuthenticationStateProvider : AuthenticationStateProvider + { + private readonly HttpClient _httpClient; + + public ServerAuthenticationStateProvider(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public override async Task GetAuthenticationStateAsync() + { + var uri = new Uri(_httpClient.BaseAddress, "/api/User"); + var data = await _httpClient.GetJsonAsync(uri.AbsoluteUri); + ClaimsIdentity identity; + if (data.IsAuthenticated) + { + var claims = new[] { new Claim(ClaimTypes.Name, data.UserName) } + .Concat(data.ExposedClaims.Select(c => new Claim(c.Key, c.Value))); + identity = new ClaimsIdentity(claims, "Server authentication"); + } + else + { + identity = new ClaimsIdentity(); + } + + return new AuthenticationState(new ClaimsPrincipal(identity)); + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index 7348a5626f..7570d92e99 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -54,6 +54,8 @@ + + @if (SelectedComponentType != null) diff --git a/src/Components/test/testassets/BasicTestApp/Startup.cs b/src/Components/test/testassets/BasicTestApp/Startup.cs index 0f509b98e8..73084e0260 100644 --- a/src/Components/test/testassets/BasicTestApp/Startup.cs +++ b/src/Components/test/testassets/BasicTestApp/Startup.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Runtime.InteropServices; +using BasicTestApp.AuthTest; using Microsoft.AspNetCore.Blazor.Http; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Builder; using Microsoft.Extensions.DependencyInjection; @@ -12,6 +14,7 @@ namespace BasicTestApp { public void ConfigureServices(IServiceCollection services) { + services.AddSingleton(); } public void Configure(IComponentsApplicationBuilder app) diff --git a/src/Components/test/testassets/TestServer/Components.TestServer.csproj b/src/Components/test/testassets/TestServer/Components.TestServer.csproj index 3566f065b3..bf0a093d11 100644 --- a/src/Components/test/testassets/TestServer/Components.TestServer.csproj +++ b/src/Components/test/testassets/TestServer/Components.TestServer.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -6,6 +6,7 @@ + diff --git a/src/Components/test/testassets/TestServer/Controllers/UserController.cs b/src/Components/test/testassets/TestServer/Controllers/UserController.cs new file mode 100644 index 0000000000..16764e4080 --- /dev/null +++ b/src/Components/test/testassets/TestServer/Controllers/UserController.cs @@ -0,0 +1,28 @@ +using System.Linq; +using BasicTestApp.AuthTest; +using Microsoft.AspNetCore.Mvc; + +namespace Components.TestServer.Controllers +{ + [Route("api/[controller]")] + public class UserController : Controller + { + // GET api/user + [HttpGet] + public ClientSideAuthenticationStateData Get() + { + // Servers are not expected to expose everything from the server-side ClaimsPrincipal + // to the client. It's up to the developer to choose what kind of authentication state + // data is needed on the client so it can display suitable options in the UI. + + return new ClientSideAuthenticationStateData + { + IsAuthenticated = User.Identity.IsAuthenticated, + UserName = User.Identity.Name, + ExposedClaims = User.Claims + .Where(c => c.Type == "test-claim") + .ToDictionary(c => c.Type, c => c.Value) + }; + } + } +} diff --git a/src/Components/test/testassets/TestServer/Pages/Authentication.cshtml b/src/Components/test/testassets/TestServer/Pages/Authentication.cshtml new file mode 100644 index 0000000000..3f6cbdefd5 --- /dev/null +++ b/src/Components/test/testassets/TestServer/Pages/Authentication.cshtml @@ -0,0 +1,72 @@ +@page +@using Microsoft.AspNetCore.Authentication +@using System.Security.Claims + + + + Authentication + + +

Authentication

+

+ This is a completely fake login mechanism for automated test purposes. + It accepts any username, with no password. +

+

+ Obviously you should not do this in real applications. + See also: documentation on configuring a real login system. +

+ +
+

Sign in

+ + @* Do not use method="get" for real login forms. This is just to simplify E2E tests. *@ +
+

+ User name: + +

+

+ +

+
+
+ +
+

Status

+

+ Authenticated: @User.Identity.IsAuthenticated + Username: @User.Identity.Name +

+ Sign out +
+ + + +@functions { + public async Task OnGet() + { + if (Request.Query["signout"] == "true") + { + await HttpContext.SignOutAsync(); + return Redirect("Authentication"); + } + + var username = Request.Query["username"]; + if (!string.IsNullOrEmpty(username)) + { + var claims = new List + { + new Claim(ClaimTypes.Name, username), + new Claim("test-claim", "Test claim value"), + }; + + await HttpContext.SignInAsync( + new ClaimsPrincipal(new ClaimsIdentity(claims, "FakeAuthenticationType"))); + + return Redirect("Authentication"); + } + + return Page(); + } +} diff --git a/src/Components/test/testassets/TestServer/Startup.cs b/src/Components/test/testassets/TestServer/Startup.cs index cf7ea69d32..26da1af8e0 100644 --- a/src/Components/test/testassets/TestServer/Startup.cs +++ b/src/Components/test/testassets/TestServer/Startup.cs @@ -1,4 +1,5 @@ using BasicTestApp; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Hosting; @@ -28,6 +29,7 @@ namespace TestServer options.AddPolicy("AllowAll", _ => { /* Controlled below */ }); }); services.AddServerSideBlazor(); + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -50,6 +52,7 @@ namespace TestServer .AllowCredentials(); }); + app.UseAuthentication(); // Mount the server-side Blazor app on /subdir app.Map("/subdir", subdirApp => @@ -70,6 +73,7 @@ namespace TestServer app.UseEndpoints(endpoints => { endpoints.MapControllers(); + endpoints.MapRazorPages(); }); // Separately, mount a prerendered server-side Blazor app on /prerendered diff --git a/src/DataProtection/Extensions/test/DataProtectionProviderTests.cs b/src/DataProtection/Extensions/test/DataProtectionProviderTests.cs index 9006c778c7..65c542f499 100644 --- a/src/DataProtection/Extensions/test/DataProtectionProviderTests.cs +++ b/src/DataProtection/Extensions/test/DataProtectionProviderTests.cs @@ -117,7 +117,7 @@ namespace Microsoft.AspNetCore.DataProtection [ConditionalFact] [Flaky("https://github.com/aspnet/AspNetCore-Internal/issues/2177", FlakyOn.AzP.Windows)] [X509StoreIsAvailable(StoreName.My, StoreLocation.CurrentUser)] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/6720 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/6720")] public void System_UsesProvidedDirectoryAndCertificate() { var filePath = Path.Combine(GetTestFilesPath(), "TestCert.pfx"); @@ -167,7 +167,7 @@ namespace Microsoft.AspNetCore.DataProtection [ConditionalFact] [X509StoreIsAvailable(StoreName.My, StoreLocation.CurrentUser)] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/6720 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/6720")] public void System_UsesProvidedCertificateNotFromStore() { using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/Microsoft.AspNetCore.FunctionalTests.csproj b/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/Microsoft.AspNetCore.FunctionalTests.csproj index 416f78e75c..6da74488a4 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/Microsoft.AspNetCore.FunctionalTests.csproj +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/Microsoft.AspNetCore.FunctionalTests.csproj @@ -4,7 +4,7 @@ netcoreapp3.0 - + false diff --git a/src/Features/JsonPatch/ref/Microsoft.AspNetCore.JsonPatch.csproj b/src/Features/JsonPatch/ref/Microsoft.AspNetCore.JsonPatch.csproj index 1a9fa78e0f..c2eba6030a 100644 --- a/src/Features/JsonPatch/ref/Microsoft.AspNetCore.JsonPatch.csproj +++ b/src/Features/JsonPatch/ref/Microsoft.AspNetCore.JsonPatch.csproj @@ -1,10 +1,10 @@ - netcoreapp3.0 + netstandard2.0 - - + + diff --git a/src/Features/JsonPatch/ref/Microsoft.AspNetCore.JsonPatch.netcoreapp3.0.cs b/src/Features/JsonPatch/ref/Microsoft.AspNetCore.JsonPatch.netstandard2.0.cs similarity index 100% rename from src/Features/JsonPatch/ref/Microsoft.AspNetCore.JsonPatch.netcoreapp3.0.cs rename to src/Features/JsonPatch/ref/Microsoft.AspNetCore.JsonPatch.netstandard2.0.cs diff --git a/src/Features/JsonPatch/src/Microsoft.AspNetCore.JsonPatch.csproj b/src/Features/JsonPatch/src/Microsoft.AspNetCore.JsonPatch.csproj index 63fec7add9..5865069fdd 100644 --- a/src/Features/JsonPatch/src/Microsoft.AspNetCore.JsonPatch.csproj +++ b/src/Features/JsonPatch/src/Microsoft.AspNetCore.JsonPatch.csproj @@ -2,7 +2,7 @@ ASP.NET Core support for JSON PATCH. - netcoreapp3.0 + netstandard2.0 $(NoWarn);CS1591 true aspnetcore;json;jsonpatch diff --git a/src/Hosting/Hosting/test/WebHostTests.cs b/src/Hosting/Hosting/test/WebHostTests.cs index 5d3e7139c7..3d4a67b076 100644 --- a/src/Hosting/Hosting/test/WebHostTests.cs +++ b/src/Hosting/Hosting/test/WebHostTests.cs @@ -199,7 +199,7 @@ namespace Microsoft.AspNetCore.Hosting } [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/7291 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/7291")] public async Task WebHostStopAsyncUsesDefaultTimeoutIfGivenTokenDoesNotFire() { var data = new Dictionary @@ -315,7 +315,7 @@ namespace Microsoft.AspNetCore.Hosting } [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/7291 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/7291")] public void WebHostApplicationLifetimeEventsOrderedCorrectlyDuringShutdown() { using (var host = CreateBuilder() diff --git a/src/Hosting/test/FunctionalTests/Microsoft.AspNetCore.Hosting.FunctionalTests.csproj b/src/Hosting/test/FunctionalTests/Microsoft.AspNetCore.Hosting.FunctionalTests.csproj index 1c92dddbaa..7a273f333d 100644 --- a/src/Hosting/test/FunctionalTests/Microsoft.AspNetCore.Hosting.FunctionalTests.csproj +++ b/src/Hosting/test/FunctionalTests/Microsoft.AspNetCore.Hosting.FunctionalTests.csproj @@ -4,7 +4,7 @@ netcoreapp3.0 - + false diff --git a/src/Hosting/test/FunctionalTests/ShutdownTests.cs b/src/Hosting/test/FunctionalTests/ShutdownTests.cs index 0d7210cd0d..7e6124e146 100644 --- a/src/Hosting/test/FunctionalTests/ShutdownTests.cs +++ b/src/Hosting/test/FunctionalTests/ShutdownTests.cs @@ -49,8 +49,11 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests { var logger = loggerFactory.CreateLogger(testName); +// https://github.com/aspnet/AspNetCore/issues/8247 +#pragma warning disable 0618 var applicationPath = Path.Combine(TestPathUtilities.GetSolutionRootDirectory("Hosting"), "test", "testassets", "Microsoft.AspNetCore.Hosting.TestSites"); +#pragma warning restore 0618 var deploymentParameters = new DeploymentParameters( applicationPath, diff --git a/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs b/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs index 88988a779e..216cf1cff9 100644 --- a/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs +++ b/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs @@ -28,7 +28,10 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests { var logger = loggerFactory.CreateLogger(nameof(InjectedStartup_DefaultApplicationNameIsEntryAssembly)); +// https://github.com/aspnet/AspNetCore/issues/8247 +#pragma warning disable 0618 var applicationPath = Path.Combine(TestPathUtilities.GetSolutionRootDirectory("Hosting"), "test", "testassets", "IStartupInjectionAssemblyName"); +#pragma warning restore 0618 var deploymentParameters = new DeploymentParameters(variant) { diff --git a/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp3.0.cs b/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp3.0.cs index 6a70682b1a..cae69dd66c 100644 --- a/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp3.0.cs +++ b/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp3.0.cs @@ -1,18 +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. -namespace Microsoft.AspNetCore.Authorization -{ - public partial interface IAllowAnonymous - { - } - public partial interface IAuthorizeData - { - string AuthenticationSchemes { get; set; } - string Policy { get; set; } - string Roles { get; set; } - } -} namespace Microsoft.AspNetCore.Builder { public abstract partial class EndpointBuilder @@ -385,6 +373,13 @@ namespace Microsoft.AspNetCore.Http public string ToUriComponent() { throw null; } } public delegate System.Threading.Tasks.Task RequestDelegate(Microsoft.AspNetCore.Http.HttpContext context); + public static partial class RequestTrailerExtensions + { + public static bool CheckTrailersAvailable(this Microsoft.AspNetCore.Http.HttpRequest request) { throw null; } + public static Microsoft.Extensions.Primitives.StringValues GetDeclaredTrailers(this Microsoft.AspNetCore.Http.HttpRequest request) { throw null; } + public static Microsoft.Extensions.Primitives.StringValues GetTrailer(this Microsoft.AspNetCore.Http.HttpRequest request, string trailerName) { throw null; } + public static bool SupportsTrailers(this Microsoft.AspNetCore.Http.HttpRequest request) { throw null; } + } public static partial class ResponseTrailerExtensions { public static void AppendTrailer(this Microsoft.AspNetCore.Http.HttpResponse response, string trailerName, Microsoft.Extensions.Primitives.StringValues trailerValues) { } diff --git a/src/Http/Http.Abstractions/src/Extensions/RequestTrailerExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/RequestTrailerExtensions.cs new file mode 100644 index 0000000000..6ffeb2eebc --- /dev/null +++ b/src/Http/Http.Abstractions/src/Extensions/RequestTrailerExtensions.cs @@ -0,0 +1,65 @@ +// 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.Http.Features; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// HttpRequest extensions for working with request trailing headers. + /// + public static class RequestTrailerExtensions + { + /// + /// Gets the request "Trailer" header that lists which trailers to expect after the body. + /// + /// + /// + public static StringValues GetDeclaredTrailers(this HttpRequest request) + { + return request.Headers.GetCommaSeparatedValues(HeaderNames.Trailer); + } + + /// + /// Indicates if the request supports receiving trailer headers. + /// + /// + /// + public static bool SupportsTrailers(this HttpRequest request) + { + return request.HttpContext.Features.Get() != null; + } + + /// + /// Checks if the request supports trailers and they are available to be read now. + /// This does not mean that there are any trailers to read. + /// + /// + /// + public static bool CheckTrailersAvailable(this HttpRequest request) + { + return request.HttpContext.Features.Get()?.Available == true; + } + + /// + /// Gets the requested trailing header from the response. Check + /// or a NotSupportedException may be thrown. + /// Check or an InvalidOperationException may be thrown. + /// + /// + /// + public static StringValues GetTrailer(this HttpRequest request, string trailerName) + { + var feature = request.HttpContext.Features.Get(); + if (feature == null) + { + throw new NotSupportedException("This request does not support trailers."); + } + + return feature.Trailers[trailerName]; + } + } +} diff --git a/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netstandard2.0.cs b/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netstandard2.0.cs index 48f381bb96..8c0f10b5dc 100644 --- a/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netstandard2.0.cs +++ b/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netstandard2.0.cs @@ -195,6 +195,11 @@ namespace Microsoft.AspNetCore.Http.Features System.Threading.CancellationToken RequestAborted { get; set; } void Abort(); } + public partial interface IHttpRequestTrailersFeature + { + bool Available { get; } + Microsoft.AspNetCore.Http.IHeaderDictionary Trailers { get; } + } public partial interface IHttpResponseFeature { System.IO.Stream Body { get; set; } diff --git a/src/Http/Http.Features/src/IHttpRequestTrailersFeature.cs b/src/Http/Http.Features/src/IHttpRequestTrailersFeature.cs new file mode 100644 index 0000000000..19706e9e4e --- /dev/null +++ b/src/Http/Http.Features/src/IHttpRequestTrailersFeature.cs @@ -0,0 +1,26 @@ +// 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.Http.Features +{ + /// + /// This feature exposes HTTP request trailer headers, either for HTTP/1.1 chunked bodies or HTTP/2 trailing headers. + /// + public interface IHttpRequestTrailersFeature + { + /// + /// Indicates if the are available yet. They may not be available until the + /// request body is fully read. + /// + bool Available { get; } + + /// + /// The trailing headers received. This will throw if + /// returns false. They may not be available until the request body is fully read. If there are no trailers this will + /// return an empty collection. + /// + IHeaderDictionary Trailers { get; } + } +} diff --git a/src/Http/Metadata/ref/Microsoft.AspNetCore.Metadata.csproj b/src/Http/Metadata/ref/Microsoft.AspNetCore.Metadata.csproj new file mode 100644 index 0000000000..5bd3e643f1 --- /dev/null +++ b/src/Http/Metadata/ref/Microsoft.AspNetCore.Metadata.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.0 + + + + + + diff --git a/src/Http/Metadata/ref/Microsoft.AspNetCore.Metadata.netstandard2.0.cs b/src/Http/Metadata/ref/Microsoft.AspNetCore.Metadata.netstandard2.0.cs new file mode 100644 index 0000000000..effddb3203 --- /dev/null +++ b/src/Http/Metadata/ref/Microsoft.AspNetCore.Metadata.netstandard2.0.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authorization +{ + public partial interface IAllowAnonymous + { + } + public partial interface IAuthorizeData + { + string AuthenticationSchemes { get; set; } + string Policy { get; set; } + string Roles { get; set; } + } +} diff --git a/src/Http/Http.Abstractions/src/IAllowAnonymous.cs b/src/Http/Metadata/src/IAllowAnonymous.cs similarity index 100% rename from src/Http/Http.Abstractions/src/IAllowAnonymous.cs rename to src/Http/Metadata/src/IAllowAnonymous.cs diff --git a/src/Http/Http.Abstractions/src/IAuthorizeData.cs b/src/Http/Metadata/src/IAuthorizeData.cs similarity index 100% rename from src/Http/Http.Abstractions/src/IAuthorizeData.cs rename to src/Http/Metadata/src/IAuthorizeData.cs diff --git a/src/Http/Metadata/src/Microsoft.AspNetCore.Metadata.csproj b/src/Http/Metadata/src/Microsoft.AspNetCore.Metadata.csproj new file mode 100644 index 0000000000..4c2b8fed22 --- /dev/null +++ b/src/Http/Metadata/src/Microsoft.AspNetCore.Metadata.csproj @@ -0,0 +1,12 @@ + + + + ASP.NET Core metadata. + netstandard2.0 + true + $(NoWarn);CS1591 + true + aspnetcore + + + diff --git a/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.csproj index 4771b24dcf..a68d86a042 100644 --- a/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.csproj @@ -5,6 +5,7 @@ + diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index b78218944f..70f62b2cfe 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -26,6 +26,7 @@ Microsoft.AspNetCore.Routing.RouteCollection + diff --git a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs index 40a22ff1a7..0fb9d68e00 100644 --- a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs +++ b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/ConfigureSigningCredentialsTests.cs @@ -22,8 +22,8 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer UnsafeEphemeralKeySet : (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? X509KeyStorageFlags.PersistKeySet : X509KeyStorageFlags.DefaultKeySet); - [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/6720 + [ConditionalFact] + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/6720")] [FrameworkSkipCondition(RuntimeFrameworks.CLR)] public void Configure_AddsDevelopmentKeyFromConfiguration() { @@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer } [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/6720 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/6720")] public void Configure_LoadsPfxCertificateCredentialFromConfiguration() { // Arrange @@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer } [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/6720 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/6720")] public void Configure_LoadsCertificateStoreCertificateCredentialFromConfiguration() { try diff --git a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs index 8551f9ccae..20bd91c88f 100644 --- a/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs +++ b/src/Identity/ApiAuthorization.IdentityServer/test/Configuration/SigningKeysLoaderTests.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration [Fact] public void LoadFromFile_ThrowsIfFileDoesNotExist() { - // Arrange, Act & Assert + // Arrange, Act & Assert var exception = Assert.Throws(() => SigningKeysLoader.LoadFromFile("./nonexisting.pfx", "", DefaultFlags)); Assert.Equal($"There was an error loading the certificate. The file './nonexisting.pfx' was not found.", exception.Message); } @@ -58,8 +58,8 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration Assert.Equal("Couldn't find a valid certificate with subject 'Invalid' on the 'CurrentUser\\My'", exception.Message); } - [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/6720 + [ConditionalFact] + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/6720")] public static void LoadFromStoreCert_SkipsCertificatesNotYetValid() { try @@ -81,8 +81,8 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration } } - [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/6720 + [ConditionalFact] + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/6720")] public static void LoadFromStoreCert_PrefersCertificatesCloserToExpirationDate() { try @@ -104,8 +104,8 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Configuration } } - [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/6720 + [ConditionalFact] + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/6720")] public static void LoadFromStoreCert_SkipsExpiredCertificates() { try diff --git a/src/Identity/UI/src/wwwroot/V4/Identity/css/site.css b/src/Identity/UI/src/wwwroot/V4/Identity/css/site.css index e1ca50bc9f..52889ec4d5 100644 --- a/src/Identity/UI/src/wwwroot/V4/Identity/css/site.css +++ b/src/Identity/UI/src/wwwroot/V4/Identity/css/site.css @@ -7,6 +7,23 @@ a.navbar-brand { word-break: break-all; } +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + /* Sticky footer styles -------------------------------------------------- */ html { @@ -59,7 +76,5 @@ body { width: 100%; overflow: scroll; white-space: nowrap; - /* Set the fixed height of the footer here */ - height: 60px; line-height: 60px; /* Vertically center the text there */ } diff --git a/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs b/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs index 14ae5b2db4..f3915ed44e 100644 --- a/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs +++ b/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs @@ -50,8 +50,7 @@ namespace Identity.DefaultUI.WebSite .AddRoles() .AddEntityFrameworkStores(); - services.AddMvc() - .AddNewtonsoftJson(); + services.AddMvc(); services.AddSingleton(); } diff --git a/src/Installers/Windows/SharedFrameworkBundle/Bundle.wxs b/src/Installers/Windows/SharedFrameworkBundle/Bundle.wxs index 4880a599b7..ecdb8d7264 100644 --- a/src/Installers/Windows/SharedFrameworkBundle/Bundle.wxs +++ b/src/Installers/Windows/SharedFrameworkBundle/Bundle.wxs @@ -26,7 +26,7 @@ - + diff --git a/src/Installers/Windows/WindowsHostingBundle/Bundle.wxs b/src/Installers/Windows/WindowsHostingBundle/Bundle.wxs index 95897534b7..8fd846a61a 100644 --- a/src/Installers/Windows/WindowsHostingBundle/Bundle.wxs +++ b/src/Installers/Windows/WindowsHostingBundle/Bundle.wxs @@ -21,7 +21,7 @@ - + diff --git a/src/Middleware/CORS/test/FunctionalTests/CORS.FunctionalTests.csproj b/src/Middleware/CORS/test/FunctionalTests/CORS.FunctionalTests.csproj index 8bb04bbf54..1c0e5a12a1 100644 --- a/src/Middleware/CORS/test/FunctionalTests/CORS.FunctionalTests.csproj +++ b/src/Middleware/CORS/test/FunctionalTests/CORS.FunctionalTests.csproj @@ -6,7 +6,7 @@ $(DefaultItemExcludes);node_modules\**\* - + false diff --git a/src/Middleware/CORS/test/FunctionalTests/CorsMiddlewareFunctionalTest.cs b/src/Middleware/CORS/test/FunctionalTests/CorsMiddlewareFunctionalTest.cs index 717f67a07f..2990bd53a9 100644 --- a/src/Middleware/CORS/test/FunctionalTests/CorsMiddlewareFunctionalTest.cs +++ b/src/Middleware/CORS/test/FunctionalTests/CorsMiddlewareFunctionalTest.cs @@ -68,7 +68,10 @@ namespace FunctionalTests private static async Task CreateDeployments(ILoggerFactory loggerFactory, string startup) { + // https://github.com/aspnet/AspNetCore/issues/7990 +#pragma warning disable 0618 var solutionPath = TestPathUtilities.GetSolutionRootDirectory("Middleware"); +#pragma warning restore 0618 var configuration = #if RELEASE diff --git a/src/Middleware/Middleware.sln b/src/Middleware/Middleware.sln index 5f16259900..a02c8c40a8 100644 --- a/src/Middleware/Middleware.sln +++ b/src/Middleware/Middleware.sln @@ -283,6 +283,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HeaderPropagationSample", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IIS", "..\Servers\IIS\IIS\src\Microsoft.AspNetCore.Server.IIS.csproj", "{B9BE1823-B555-4AAB-AEBC-C8C3F48C8861}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RequestThrottling", "RequestThrottling", "{8C9AA8A2-9D1F-4450-9F8D-56BAB6F3D343}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RequestThrottlingSample", "RequestThrottling\sample\RequestThrottlingSample.csproj", "{6720919C-0DEA-49E1-90DC-F1883F7919CD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestThrottling", "RequestThrottling\src\Microsoft.AspNetCore.RequestThrottling.csproj", "{4CE2384D-6B88-4824-ADD1-4183D180FEFF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestThrottling.Tests", "RequestThrottling\test\Microsoft.AspNetCore.RequestThrottling.Tests.csproj", "{353AA2B0-1013-486C-B5BD-9379385CA403}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1541,6 +1549,42 @@ Global {B9BE1823-B555-4AAB-AEBC-C8C3F48C8861}.Release|x64.Build.0 = Release|Any CPU {B9BE1823-B555-4AAB-AEBC-C8C3F48C8861}.Release|x86.ActiveCfg = Release|Any CPU {B9BE1823-B555-4AAB-AEBC-C8C3F48C8861}.Release|x86.Build.0 = Release|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Debug|x64.Build.0 = Debug|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Debug|x86.ActiveCfg = Debug|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Debug|x86.Build.0 = Debug|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Release|Any CPU.Build.0 = Release|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Release|x64.ActiveCfg = Release|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Release|x64.Build.0 = Release|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Release|x86.ActiveCfg = Release|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Release|x86.Build.0 = Release|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Debug|x64.ActiveCfg = Debug|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Debug|x64.Build.0 = Debug|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Debug|x86.ActiveCfg = Debug|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Debug|x86.Build.0 = Debug|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Release|Any CPU.Build.0 = Release|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Release|x64.ActiveCfg = Release|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Release|x64.Build.0 = Release|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Release|x86.ActiveCfg = Release|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Release|x86.Build.0 = Release|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Debug|Any CPU.Build.0 = Debug|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Debug|x64.ActiveCfg = Debug|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Debug|x64.Build.0 = Debug|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Debug|x86.ActiveCfg = Debug|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Debug|x86.Build.0 = Debug|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Release|Any CPU.ActiveCfg = Release|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Release|Any CPU.Build.0 = Release|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Release|x64.ActiveCfg = Release|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Release|x64.Build.0 = Release|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Release|x86.ActiveCfg = Release|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1663,6 +1707,9 @@ Global {179A159B-87EA-4353-BE92-4FB6CC05BC7D} = {0437D207-864E-429C-92B4-9D08D290188C} {CDE2E736-A034-4748-98C4-0DEDAAC8063D} = {179A159B-87EA-4353-BE92-4FB6CC05BC7D} {B9BE1823-B555-4AAB-AEBC-C8C3F48C8861} = {ACA6DDB9-7592-47CE-A740-D15BF307E9E0} + {6720919C-0DEA-49E1-90DC-F1883F7919CD} = {8C9AA8A2-9D1F-4450-9F8D-56BAB6F3D343} + {4CE2384D-6B88-4824-ADD1-4183D180FEFF} = {8C9AA8A2-9D1F-4450-9F8D-56BAB6F3D343} + {353AA2B0-1013-486C-B5BD-9379385CA403} = {8C9AA8A2-9D1F-4450-9F8D-56BAB6F3D343} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {83786312-A93B-4BB4-AB06-7C6913A59AFA} diff --git a/src/Middleware/RequestThrottling/RequestThrottling.slnf b/src/Middleware/RequestThrottling/RequestThrottling.slnf new file mode 100644 index 0000000000..d434fbc862 --- /dev/null +++ b/src/Middleware/RequestThrottling/RequestThrottling.slnf @@ -0,0 +1,25 @@ +{ + "solution": { + "path": "..\\Middleware.sln", + "projects": [ + "..\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", + "..\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj", + "..\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj", + "..\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", + "..\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", + "..\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", + "..\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", + "..\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj", + "..\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj", + "..\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", + "..\\Servers\\Kestrel\\Transport.Abstractions\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj", + "..\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj", + "..\\http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj", + "..\\http\\http\\src\\Microsoft.AspNetCore.Http.csproj", + "HttpsPolicy\\src\\Microsoft.AspNetCore.HttpsPolicy.csproj", + "RequestThrottling\\sample\\RequestThrottlingSample.csproj", + "RequestThrottling\\src\\Microsoft.AspNetCore.RequestThrottling.csproj", + "RequestThrottling\\test\\Microsoft.AspNetCore.RequestThrottling.Tests.csproj" + ] + } +} \ No newline at end of file diff --git a/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.csproj b/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.csproj new file mode 100644 index 0000000000..0a1bcdd0b9 --- /dev/null +++ b/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.csproj @@ -0,0 +1,10 @@ + + + + netcoreapp3.0 + + + + + + diff --git a/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs b/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs new file mode 100644 index 0000000000..618082bc4a --- /dev/null +++ b/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs @@ -0,0 +1,3 @@ +// 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. + diff --git a/src/Middleware/RequestThrottling/sample/RequestThrottlingSample.csproj b/src/Middleware/RequestThrottling/sample/RequestThrottlingSample.csproj new file mode 100644 index 0000000000..0f80e6516a --- /dev/null +++ b/src/Middleware/RequestThrottling/sample/RequestThrottlingSample.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.0 + + + + + + + + + diff --git a/src/Middleware/RequestThrottling/sample/Startup.cs b/src/Middleware/RequestThrottling/sample/Startup.cs new file mode 100644 index 0000000000..95a94be56d --- /dev/null +++ b/src/Middleware/RequestThrottling/sample/Startup.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace RequestThrottlingSample +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + { + app.Run(async context => + { + await context.Response.WriteAsync("Hello world!"); + }); + } + + // Entry point for the application. + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) // for the cert file + .ConfigureLogging(factory => + { + factory.SetMinimumLevel(LogLevel.Debug); + factory.AddConsole(); + }) + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj b/src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj new file mode 100644 index 0000000000..5014e9cec5 --- /dev/null +++ b/src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj @@ -0,0 +1,10 @@ + + + + ASP.NET Core middleware for queuing incoming HTTP requests, to avoid threadpool starvation. + netcoreapp3.0 + true + aspnetcore;queue;queuing + + + diff --git a/src/Middleware/RequestThrottling/src/Properties/AssemblyInfo.cs b/src/Middleware/RequestThrottling/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..1dcaedfaa6 --- /dev/null +++ b/src/Middleware/RequestThrottling/src/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.RequestThrottling.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Middleware/RequestThrottling/src/SemaphoreWrapper.cs b/src/Middleware/RequestThrottling/src/SemaphoreWrapper.cs new file mode 100644 index 0000000000..4c79b94777 --- /dev/null +++ b/src/Middleware/RequestThrottling/src/SemaphoreWrapper.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.RequestThrottling +{ + internal class SemaphoreWrapper : IDisposable + { + private SemaphoreSlim _semaphore; + + public SemaphoreWrapper(int queueLength) + { + _semaphore = new SemaphoreSlim(queueLength); + } + + public Task EnterQueue() + { + return _semaphore.WaitAsync(); + } + + public void LeaveQueue() + { + _semaphore.Release(); + } + + public int Count + { + get => _semaphore.CurrentCount; + } + + public void Dispose() + { + _semaphore.Dispose(); + } + } +} diff --git a/src/Middleware/RequestThrottling/test/Microsoft.AspNetCore.RequestThrottling.Tests.csproj b/src/Middleware/RequestThrottling/test/Microsoft.AspNetCore.RequestThrottling.Tests.csproj new file mode 100644 index 0000000000..8c0dd8e989 --- /dev/null +++ b/src/Middleware/RequestThrottling/test/Microsoft.AspNetCore.RequestThrottling.Tests.csproj @@ -0,0 +1,10 @@ + + + + netcoreapp3.0 + + + + + + diff --git a/src/Middleware/RequestThrottling/test/SemaphoreWrapperTests.cs b/src/Middleware/RequestThrottling/test/SemaphoreWrapperTests.cs new file mode 100644 index 0000000000..b5cdfce18f --- /dev/null +++ b/src/Middleware/RequestThrottling/test/SemaphoreWrapperTests.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 Xunit; +using System.Threading; +using System.Threading.Tasks; +using System; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Testing; + +namespace Microsoft.AspNetCore.RequestThrottling.Tests +{ + public class SemaphoreWrapperTests + { + [Fact] + public async Task TracksQueueLength() + { + using var s = new SemaphoreWrapper(1); + Assert.Equal(1, s.Count); + + await s.EnterQueue().OrTimeout(); + Assert.Equal(0, s.Count); + + s.LeaveQueue(); + Assert.Equal(1, s.Count); + } + + [Fact] + public void DoesNotWaitIfSpaceAvailible() + { + using var s = new SemaphoreWrapper(2); + + var t1 = s.EnterQueue(); + Assert.True(t1.IsCompleted); + + var t2 = s.EnterQueue(); + Assert.True(t2.IsCompleted); + + var t3 = s.EnterQueue(); + Assert.False(t3.IsCompleted); + } + + [Fact] + public async Task WaitsIfNoSpaceAvailible() + { + using var s = new SemaphoreWrapper(1); + await s.EnterQueue().OrTimeout(); + + var waitingTask = s.EnterQueue(); + Assert.False(waitingTask.IsCompleted); + + s.LeaveQueue(); + await waitingTask.OrTimeout(); + } + + [Fact] + public async Task IsEncapsulated() + { + using var s1 = new SemaphoreWrapper(1); + using var s2 = new SemaphoreWrapper(1); + + await s1.EnterQueue().OrTimeout(); + await s2.EnterQueue().OrTimeout(); + } + } +} diff --git a/src/Middleware/RequestThrottling/test/TaskExtensions.cs b/src/Middleware/RequestThrottling/test/TaskExtensions.cs new file mode 100644 index 0000000000..52ec0c4303 --- /dev/null +++ b/src/Middleware/RequestThrottling/test/TaskExtensions.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.Diagnostics; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Testing; + +namespace System.Threading.Tasks +{ +#if TESTUTILS + public +#else + internal +#endif + static class TaskExtensions + { + private const int DefaultTimeout = 30 * 1000; + + public static Task OrTimeout(this Task task, int milliseconds = DefaultTimeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) + { + return OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds), memberName, filePath, lineNumber); + } + + public static Task OrTimeout(this Task task, TimeSpan timeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) + { + return task.TimeoutAfter(timeout, filePath, lineNumber ?? 0); + } + + public static Task OrTimeout(this ValueTask task, int milliseconds = DefaultTimeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) => + OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds), memberName, filePath, lineNumber); + + public static Task OrTimeout(this ValueTask task, TimeSpan timeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) => + task.AsTask().OrTimeout(timeout, memberName, filePath, lineNumber); + + public static Task OrTimeout(this Task task, int milliseconds = DefaultTimeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) + { + return OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds), memberName, filePath, lineNumber); + } + + public static Task OrTimeout(this Task task, TimeSpan timeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) + { + return task.TimeoutAfter(timeout, filePath, lineNumber ?? 0); + } + + public static async Task OrThrowIfOtherFails(this Task task, Task otherTask) + { + var completed = await Task.WhenAny(task, otherTask); + if (completed == otherTask && otherTask.IsFaulted) + { + // Manifest the exception + otherTask.GetAwaiter().GetResult(); + throw new Exception("Unreachable code"); + } + else + { + // Await the task we were asked to await. Either it's finished, or the otherTask finished successfully, and it's not our job to check that + await task; + } + } + + public static async Task OrThrowIfOtherFails(this Task task, Task otherTask) + { + await OrThrowIfOtherFails((Task)task, otherTask); + + // If we get here, 'task' is finished and succeeded. + return task.GetAwaiter().GetResult(); + } + } +} diff --git a/src/Middleware/build.cmd b/src/Middleware/build.cmd new file mode 100644 index 0000000000..033fe6f614 --- /dev/null +++ b/src/Middleware/build.cmd @@ -0,0 +1,3 @@ +@ECHO OFF +SET RepoRoot=%~dp0..\.. +%RepoRoot%\build.cmd -projects %~dp0\**\*.*proj %* diff --git a/src/MusicStore/test/MusicStore.E2ETests/Common/Helpers.cs b/src/MusicStore/test/MusicStore.E2ETests/Common/Helpers.cs index 954cb9dea5..eb761e21a5 100644 --- a/src/MusicStore/test/MusicStore.E2ETests/Common/Helpers.cs +++ b/src/MusicStore/test/MusicStore.E2ETests/Common/Helpers.cs @@ -9,7 +9,10 @@ namespace E2ETests { public static string GetApplicationPath() { + // https://github.com/aspnet/AspNetCore/issues/8343 +#pragma warning disable 0618 var solutionDirectory = TestPathUtilities.GetSolutionRootDirectory("MusicStore"); +#pragma warning restore 0618 return Path.GetFullPath(Path.Combine(solutionDirectory, "samples", "MusicStore")); } diff --git a/src/MusicStore/test/MusicStore.E2ETests/MusicStore.E2ETests.csproj b/src/MusicStore/test/MusicStore.E2ETests/MusicStore.E2ETests.csproj index 5a2d81e394..affbb20ef3 100644 --- a/src/MusicStore/test/MusicStore.E2ETests/MusicStore.E2ETests.csproj +++ b/src/MusicStore/test/MusicStore.E2ETests/MusicStore.E2ETests.csproj @@ -9,7 +9,7 @@ $(WarningsNotAsErrors);xUnit1004 $(NoWarn);NU1605 - + false false false diff --git a/src/Mvc/Mvc.Analyzers/src/AttributesShouldNotBeAppliedToPageModelAnalyzer.cs b/src/Mvc/Mvc.Analyzers/src/AttributesShouldNotBeAppliedToPageModelAnalyzer.cs index 4dc7b6c85d..ee91bde64e 100644 --- a/src/Mvc/Mvc.Analyzers/src/AttributesShouldNotBeAppliedToPageModelAnalyzer.cs +++ b/src/Mvc/Mvc.Analyzers/src/AttributesShouldNotBeAppliedToPageModelAnalyzer.cs @@ -1,7 +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; using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; @@ -122,7 +121,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers } } - private static AttributeData GetAttribute(ISymbol symbol, INamedTypeSymbol attributeType) + private static AttributeData? GetAttribute(ISymbol symbol, INamedTypeSymbol attributeType) { foreach (var attribute in symbol.GetAttributes()) { diff --git a/src/Mvc/Mvc.Analyzers/src/CodeAnalysisExtensions.cs b/src/Mvc/Mvc.Analyzers/src/CodeAnalysisExtensions.cs index 72da1b1f4c..50508ab63d 100644 --- a/src/Mvc/Mvc.Analyzers/src/CodeAnalysisExtensions.cs +++ b/src/Mvc/Mvc.Analyzers/src/CodeAnalysisExtensions.cs @@ -31,9 +31,10 @@ namespace Microsoft.CodeAnalysis Debug.Assert(methodSymbol != null); Debug.Assert(attribute != null); - while (methodSymbol != null) + IMethodSymbol? current = methodSymbol; + while (current != null) { - foreach (var attributeData in GetAttributes(methodSymbol, attribute)) + foreach (var attributeData in GetAttributes(current, attribute)) { yield return attributeData; } @@ -43,7 +44,7 @@ namespace Microsoft.CodeAnalysis break; } - methodSymbol = methodSymbol.IsOverride ? methodSymbol.OverriddenMethod : null; + current = current.IsOverride ? current.OverriddenMethod : null; } } @@ -76,14 +77,15 @@ namespace Microsoft.CodeAnalysis return HasAttribute(propertySymbol, attribute); } - while (propertySymbol != null) + IPropertySymbol? current = propertySymbol; + while (current != null) { - if (propertySymbol.HasAttribute(attribute)) + if (current.HasAttribute(attribute)) { return true; } - propertySymbol = propertySymbol.IsOverride ? propertySymbol.OverriddenProperty : null; + current = current.IsOverride ? current.OverriddenProperty : null; } return false; diff --git a/src/Mvc/Mvc.Analyzers/src/Microsoft.AspNetCore.Mvc.Analyzers.csproj b/src/Mvc/Mvc.Analyzers/src/Microsoft.AspNetCore.Mvc.Analyzers.csproj index 1e92917e80..d3422ae192 100644 --- a/src/Mvc/Mvc.Analyzers/src/Microsoft.AspNetCore.Mvc.Analyzers.csproj +++ b/src/Mvc/Mvc.Analyzers/src/Microsoft.AspNetCore.Mvc.Analyzers.csproj @@ -9,6 +9,8 @@ false false $(MSBuildProjectName).nuspec + enable + enable
diff --git a/src/Mvc/Mvc.Analyzers/src/TopLevelParameterNameAnalyzer.cs b/src/Mvc/Mvc.Analyzers/src/TopLevelParameterNameAnalyzer.cs index ad5aeeaeab..c20466bfcc 100644 --- a/src/Mvc/Mvc.Analyzers/src/TopLevelParameterNameAnalyzer.cs +++ b/src/Mvc/Mvc.Analyzers/src/TopLevelParameterNameAnalyzer.cs @@ -3,7 +3,6 @@ using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; @@ -24,8 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers context.RegisterCompilationStartAction(compilationStartAnalysisContext => { - var typeCache = new SymbolCache(compilationStartAnalysisContext.Compilation); - if (typeCache.ControllerAttribute == null || typeCache.ControllerAttribute.TypeKind == TypeKind.Error) + if (!SymbolCache.TryCreate(compilationStartAnalysisContext.Compilation, out var typeCache)) { // No-op if we can't find types we care about. return; @@ -185,20 +183,100 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers internal readonly struct SymbolCache { - public SymbolCache(Compilation compilation) + public SymbolCache( + INamedTypeSymbol bindAttribute, + INamedTypeSymbol controllerAttribute, + INamedTypeSymbol fromBodyAttribute, + INamedTypeSymbol apiBehaviorMetadata, + INamedTypeSymbol binderTypeProviderMetadata, + INamedTypeSymbol modelNameProvider, + INamedTypeSymbol nonControllerAttribute, + INamedTypeSymbol nonActionAttribute, + IMethodSymbol disposableDispose) { - BindAttribute = compilation.GetTypeByMetadataName(SymbolNames.BindAttribute); - ControllerAttribute = compilation.GetTypeByMetadataName(SymbolNames.ControllerAttribute); - FromBodyAttribute = compilation.GetTypeByMetadataName(SymbolNames.FromBodyAttribute); - IApiBehaviorMetadata = compilation.GetTypeByMetadataName(SymbolNames.IApiBehaviorMetadata); - IBinderTypeProviderMetadata = compilation.GetTypeByMetadataName(SymbolNames.IBinderTypeProviderMetadata); - IModelNameProvider = compilation.GetTypeByMetadataName(SymbolNames.IModelNameProvider); - NonControllerAttribute = compilation.GetTypeByMetadataName(SymbolNames.NonControllerAttribute); - NonActionAttribute = compilation.GetTypeByMetadataName(SymbolNames.NonActionAttribute); + BindAttribute = bindAttribute; + ControllerAttribute = controllerAttribute; + FromBodyAttribute = fromBodyAttribute; + IApiBehaviorMetadata = apiBehaviorMetadata; + IBinderTypeProviderMetadata = binderTypeProviderMetadata; + IModelNameProvider = modelNameProvider; + NonControllerAttribute = nonControllerAttribute; + NonActionAttribute = nonActionAttribute; + IDisposableDispose = disposableDispose; + } + + public static bool TryCreate(Compilation compilation, out SymbolCache symbolCache) + { + symbolCache = default; + + if (!TryGetType(SymbolNames.BindAttribute, out var bindAttribute)) + { + return false; + } + + + if (!TryGetType(SymbolNames.ControllerAttribute, out var controllerAttribute)) + { + return false; + } + + + if (!TryGetType(SymbolNames.FromBodyAttribute, out var fromBodyAttribute)) + { + return false; + } + + if (!TryGetType(SymbolNames.IApiBehaviorMetadata, out var apiBehaviorMetadata)) + { + return false; + } + + if (!TryGetType(SymbolNames.IBinderTypeProviderMetadata, out var iBinderTypeProviderMetadata)) + { + return false; + } + + if (!TryGetType(SymbolNames.IModelNameProvider, out var iModelNameProvider)) + { + return false; + } + + if (!TryGetType(SymbolNames.NonControllerAttribute, out var nonControllerAttribute)) + { + return false; + } + + if (!TryGetType(SymbolNames.NonActionAttribute, out var nonActionAttribute)) + { + return false; + } var disposable = compilation.GetSpecialType(SpecialType.System_IDisposable); - var members = disposable.GetMembers(nameof(IDisposable.Dispose)); - IDisposableDispose = members.Length == 1 ? (IMethodSymbol)members[0] : null; + var members = disposable?.GetMembers(nameof(IDisposable.Dispose)); + var idisposableDispose = (IMethodSymbol?)members?[0]; + if (idisposableDispose == null) + { + return false; + } + + symbolCache = new SymbolCache( + bindAttribute, + controllerAttribute, + fromBodyAttribute, + apiBehaviorMetadata, + iBinderTypeProviderMetadata, + iModelNameProvider, + nonControllerAttribute, + nonActionAttribute, + idisposableDispose); + + return true; + + bool TryGetType(string typeName, out INamedTypeSymbol typeSymbol) + { + typeSymbol = compilation.GetTypeByMetadataName(typeName); + return typeSymbol != null && typeSymbol.TypeKind != TypeKind.Error; + } } public INamedTypeSymbol BindAttribute { get; } diff --git a/src/Mvc/Mvc.Analyzers/test/Infrastructure/MvcTestSource.cs b/src/Mvc/Mvc.Analyzers/test/Infrastructure/MvcTestSource.cs index 89f141ef95..8b304a0f94 100644 --- a/src/Mvc/Mvc.Analyzers/test/Infrastructure/MvcTestSource.cs +++ b/src/Mvc/Mvc.Analyzers/test/Infrastructure/MvcTestSource.cs @@ -27,13 +27,16 @@ namespace Microsoft.AspNetCore.Mvc private static string GetProjectDirectory() { - // On helix we use the published test files + // On helix we use the published test files if (SkipOnHelixAttribute.OnHelix()) { return AppContext.BaseDirectory; } +// https://github.com/aspnet/AspNetCore/issues/9431 +#pragma warning disable 0618 var solutionDirectory = TestPathUtilities.GetSolutionRootDirectory("Mvc"); +#pragma warning restore 0618 var projectDirectory = Path.Combine(solutionDirectory, "Mvc.Analyzers", "test"); return projectDirectory; } diff --git a/src/Mvc/Mvc.Analyzers/test/Mvc.Analyzers.Test.csproj b/src/Mvc/Mvc.Analyzers/test/Mvc.Analyzers.Test.csproj index df8a4a4cb0..ab2cabb576 100644 --- a/src/Mvc/Mvc.Analyzers/test/Mvc.Analyzers.Test.csproj +++ b/src/Mvc/Mvc.Analyzers/test/Mvc.Analyzers.Test.csproj @@ -6,7 +6,7 @@ Microsoft.AspNetCore.Mvc.Analyzers - + false @@ -21,7 +21,7 @@ - + diff --git a/src/Mvc/Mvc.Analyzers/test/TopLevelParameterNameAnalyzerTest.cs b/src/Mvc/Mvc.Analyzers/test/TopLevelParameterNameAnalyzerTest.cs index 7329ae9d13..361f6d3cb5 100644 --- a/src/Mvc/Mvc.Analyzers/test/TopLevelParameterNameAnalyzerTest.cs +++ b/src/Mvc/Mvc.Analyzers/test/TopLevelParameterNameAnalyzerTest.cs @@ -134,7 +134,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var method = (IMethodSymbol)modelType.GetMembers("ActionMethod").First(); var parameter = method.Parameters[0]; - var symbolCache = new TopLevelParameterNameAnalyzer.SymbolCache(compilation); + Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache)); var result = TopLevelParameterNameAnalyzer.IsProblematicParameter(symbolCache, parameter); return result; @@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { var methodName = nameof(GetNameTests.SingleAttribute); var compilation = await GetCompilationForGetName(); - var symbolCache = new TopLevelParameterNameAnalyzer.SymbolCache(compilation); + Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache)); var type = compilation.GetTypeByMetadataName(typeof(GetNameTests).FullName); var method = (IMethodSymbol)type.GetMembers(methodName).First(); @@ -161,7 +161,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { var methodName = nameof(GetNameTests.NoAttribute); var compilation = await GetCompilationForGetName(); - var symbolCache = new TopLevelParameterNameAnalyzer.SymbolCache(compilation); + Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache)); var type = compilation.GetTypeByMetadataName(typeof(GetNameTests).FullName); var method = (IMethodSymbol)type.GetMembers(methodName).First(); @@ -177,7 +177,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { var methodName = nameof(GetNameTests.SingleAttributeWithoutName); var compilation = await GetCompilationForGetName(); - var symbolCache = new TopLevelParameterNameAnalyzer.SymbolCache(compilation); + Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache)); var type = compilation.GetTypeByMetadataName(typeof(GetNameTests).FullName); var method = (IMethodSymbol)type.GetMembers(methodName).First(); @@ -193,7 +193,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { var methodName = nameof(GetNameTests.MultipleAttributes); var compilation = await GetCompilationForGetName(); - var symbolCache = new TopLevelParameterNameAnalyzer.SymbolCache(compilation); + Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache)); var type = compilation.GetTypeByMetadataName(typeof(GetNameTests).FullName); var method = (IMethodSymbol)type.GetMembers(methodName).First(); @@ -221,7 +221,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source }); var compilation = await project.GetCompilationAsync(); - var symbolCache = new TopLevelParameterNameAnalyzer.SymbolCache(compilation); + Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache)); var type = compilation.GetTypeByMetadataName(typeof(SpecifiesModelTypeTests).FullName); var method = (IMethodSymbol)type.GetMembers(testMethod).First(); @@ -239,7 +239,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source }); var compilation = await project.GetCompilationAsync(); - var symbolCache = new TopLevelParameterNameAnalyzer.SymbolCache(compilation); + Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache)); var type = compilation.GetTypeByMetadataName(typeof(SpecifiesModelTypeTests).FullName); var method = (IMethodSymbol)type.GetMembers(testMethod).First(); @@ -257,7 +257,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source }); var compilation = await project.GetCompilationAsync(); - var symbolCache = new TopLevelParameterNameAnalyzer.SymbolCache(compilation); + Assert.True(TopLevelParameterNameAnalyzer.SymbolCache.TryCreate(compilation, out var symbolCache)); var type = compilation.GetTypeByMetadataName(typeof(SpecifiesModelTypeTests).FullName); var method = (IMethodSymbol)type.GetMembers(testMethod).First(); diff --git a/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadata.cs b/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadata.cs index 018967cb10..2fdc277a08 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadata.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadata.cs @@ -1,6 +1,7 @@ // 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.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -17,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers _statusCode = null; } - public ActualApiResponseMetadata(ReturnStatementSyntax returnStatement, int statusCode, ITypeSymbol returnType) + public ActualApiResponseMetadata(ReturnStatementSyntax returnStatement, int statusCode, ITypeSymbol? returnType) { ReturnStatement = returnStatement; _statusCode = statusCode; @@ -26,10 +27,10 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers public ReturnStatementSyntax ReturnStatement { get; } - public int StatusCode => _statusCode.Value; + public int StatusCode => _statusCode ?? throw new ArgumentException("Status code is not available when IsDefaultResponse is true"); public bool IsDefaultResponse => _statusCode == null; - public ITypeSymbol ReturnType { get; } + public ITypeSymbol? ReturnType { get; } } } diff --git a/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadataFactory.cs b/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadataFactory.cs index 83e1c78546..3580535601 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadataFactory.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadataFactory.cs @@ -87,13 +87,13 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers .FirstOrDefault(); var statusCode = GetDefaultStatusCode(defaultStatusCodeAttribute); - ITypeSymbol returnType = null; + ITypeSymbol? returnType = null; switch (returnExpression) { case InvocationExpressionSyntax invocation: { // Covers the 'return StatusCode(200)' case. - var result = InspectMethodArguments(symbolCache, semanticModel, invocation.Expression, invocation.ArgumentList, cancellationToken); + var result = InspectMethodArguments(semanticModel, invocation.Expression, invocation.ArgumentList, cancellationToken); statusCode = result.statusCode ?? statusCode; returnType = result.returnType; break; @@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers case ObjectCreationExpressionSyntax creation: { // Read values from 'return new StatusCodeResult(200) case. - var result = InspectMethodArguments(symbolCache, semanticModel, creation, creation.ArgumentList, cancellationToken); + var result = InspectMethodArguments(semanticModel, creation, creation.ArgumentList, cancellationToken); statusCode = result.statusCode ?? statusCode; returnType = result.returnType; @@ -123,14 +123,14 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers return new ActualApiResponseMetadata(returnStatementSyntax, statusCode.Value, returnType); } - private static (int? statusCode, ITypeSymbol returnType) InspectInitializers( + private static (int? statusCode, ITypeSymbol? returnType) InspectInitializers( in ApiControllerSymbolCache symbolCache, SemanticModel semanticModel, InitializerExpressionSyntax initializer, CancellationToken cancellationToken) { int? statusCode = null; - ITypeSymbol typeSymbol = null; + ITypeSymbol? typeSymbol = null; for (var i = 0; initializer != null && i < initializer.Expressions.Count; i++) { @@ -162,15 +162,14 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers return (statusCode, typeSymbol); } - private static (int? statusCode, ITypeSymbol returnType) InspectMethodArguments( - in ApiControllerSymbolCache symbolCache, + private static (int? statusCode, ITypeSymbol? returnType) InspectMethodArguments( SemanticModel semanticModel, ExpressionSyntax expression, BaseArgumentListSyntax argumentList, CancellationToken cancellationToken) { int? statusCode = null; - ITypeSymbol typeSymbol = null; + ITypeSymbol? typeSymbol = null; var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken); diff --git a/src/Mvc/Mvc.Api.Analyzers/src/AddResponseTypeAttributeCodeFixAction.cs b/src/Mvc/Mvc.Api.Analyzers/src/AddResponseTypeAttributeCodeFixAction.cs index 18b82c2ae1..0e23036cce 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/AddResponseTypeAttributeCodeFixAction.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/AddResponseTypeAttributeCodeFixAction.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -43,7 +44,14 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) { - var context = await CreateCodeActionContext(cancellationToken).ConfigureAwait(false); + var nullableContext = await CreateCodeActionContext(cancellationToken).ConfigureAwait(false); + if (nullableContext == null) + { + return _document; + } + + var context = nullableContext.Value; + var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(context.SymbolCache, context.Method); var errorResponseType = SymbolApiResponseMetadataProvider.GetErrorResponseType(context.SymbolCache, context.Method); @@ -56,8 +64,11 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var documentEditor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false); var addUsingDirective = false; - foreach (var (statusCode, returnType) in results.OrderBy(s => s.statusCode)) + foreach (var item in results.OrderBy(s => s.statusCode)) { + var statusCode = item.statusCode; + var returnType = item.typeSymbol; + AttributeSyntax attributeSyntax; bool addUsing; @@ -113,7 +124,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers return document.WithSyntaxRoot(root); } - private async Task CreateCodeActionContext(CancellationToken cancellationToken) + private async Task CreateCodeActionContext(CancellationToken cancellationToken) { var root = await _document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var semanticModel = await _document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); @@ -124,7 +135,10 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var statusCodesType = semanticModel.Compilation.GetTypeByMetadataName(ApiSymbolNames.HttpStatusCodes); var statusCodeConstants = GetStatusCodeConstants(statusCodesType); - var symbolCache = new ApiControllerSymbolCache(semanticModel.Compilation); + if (!ApiControllerSymbolCache.TryCreate(semanticModel.Compilation, out var symbolCache)) + { + return null; + } var codeActionContext = new CodeActionContext(semanticModel, symbolCache, method, methodSyntax, statusCodeConstants, cancellationToken); return codeActionContext; @@ -152,15 +166,15 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers return statusCodeConstants; } - private ICollection<(int statusCode, ITypeSymbol typeSymbol)> CalculateStatusCodesToApply(in CodeActionContext context, IList declaredResponseMetadata) + private ICollection<(int statusCode, ITypeSymbol? typeSymbol)> CalculateStatusCodesToApply(in CodeActionContext context, IList declaredResponseMetadata) { if (!ActualApiResponseMetadataFactory.TryGetActualResponseMetadata(context.SymbolCache, context.SemanticModel, context.MethodSyntax, context.CancellationToken, out var actualResponseMetadata)) { // If we cannot parse metadata correctly, don't offer fixes. - return Array.Empty<(int, ITypeSymbol)>(); + return Array.Empty<(int, ITypeSymbol?)>(); } - var statusCodes = new Dictionary(); + var statusCodes = new Dictionary(); foreach (var metadata in actualResponseMetadata) { if (DeclaredApiResponseMetadata.TryGetDeclaredMetadata(declaredResponseMetadata, metadata, result: out var declaredMetadata) && diff --git a/src/Mvc/Mvc.Api.Analyzers/src/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer.cs b/src/Mvc/Mvc.Api.Analyzers/src/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer.cs index 9a8dc1c714..274609128f 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer.cs @@ -23,8 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers context.RegisterCompilationStartAction(compilationStartAnalysisContext => { - var symbolCache = new ApiControllerSymbolCache(compilationStartAnalysisContext.Compilation); - if (symbolCache.ApiConventionTypeAttribute == null || symbolCache.ApiConventionTypeAttribute.TypeKind == TypeKind.Error) + if (!ApiControllerSymbolCache.TryCreate(compilationStartAnalysisContext.Compilation, out var symbolCache)) { // No-op if we can't find types we care about. return; @@ -55,12 +54,18 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers } var parent = ifOperation.Parent; - if (parent?.Kind == OperationKind.Block) + if (parent == null) { - parent = parent?.Parent; + // No parent, nothing to do + return; } - if (parent?.Kind != OperationKind.MethodBodyOperation) + if (parent.Kind == OperationKind.Block && parent.Parent != null) + { + parent = parent.Parent; + } + + if (parent.Kind != OperationKind.MethodBodyOperation) { // Only support top-level ModelState IsValid checks. return; diff --git a/src/Mvc/Mvc.Api.Analyzers/src/ApiControllerSymbolCache.cs b/src/Mvc/Mvc.Api.Analyzers/src/ApiControllerSymbolCache.cs index 37f6b26291..2a41802889 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/ApiControllerSymbolCache.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/ApiControllerSymbolCache.cs @@ -8,30 +8,164 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers { internal readonly struct ApiControllerSymbolCache { - public ApiControllerSymbolCache(Compilation compilation) + public static bool TryCreate(Compilation compilation, out ApiControllerSymbolCache symbolCache) { - ApiConventionMethodAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ApiConventionMethodAttribute); - ApiConventionNameMatchAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ApiConventionNameMatchAttribute); - ApiConventionTypeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ApiConventionTypeAttribute); - ApiConventionTypeMatchAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ApiConventionTypeMatchAttribute); - ControllerAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ControllerAttribute); - DefaultStatusCodeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.DefaultStatusCodeAttribute); - IActionResult = compilation.GetTypeByMetadataName(ApiSymbolNames.IActionResult); - IApiBehaviorMetadata = compilation.GetTypeByMetadataName(ApiSymbolNames.IApiBehaviorMetadata); - ModelStateDictionary = compilation.GetTypeByMetadataName(ApiSymbolNames.ModelStateDictionary); - NonActionAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.NonActionAttribute); - NonControllerAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.NonControllerAttribute); - ProblemDetails = compilation.GetTypeByMetadataName(ApiSymbolNames.ProblemDetails); - ProducesDefaultResponseTypeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ProducesDefaultResponseTypeAttribute); - ProducesErrorResponseTypeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ProducesErrorResponseTypeAttribute); - ProducesResponseTypeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ProducesResponseTypeAttribute); + symbolCache = default; + + if (!TryGetType(ApiSymbolNames.ApiConventionMethodAttribute, out var apiConventionMethodAttribute)) + { + return false; + } + + if (!TryGetType(ApiSymbolNames.ApiConventionNameMatchAttribute, out var apiConventionNameMatchAttribute)) + { + return false; + } + + if (!TryGetType(ApiSymbolNames.ApiConventionTypeAttribute, out var apiConventionTypeAttribute)) + { + return false; + } + + if (!TryGetType(ApiSymbolNames.ApiConventionTypeMatchAttribute, out var apiConventionTypeMatchAttribute)) + { + return false; + } + + if (!TryGetType(ApiSymbolNames.ControllerAttribute, out var controllerAttribute)) + { + return false; + } + + if (!TryGetType(ApiSymbolNames.DefaultStatusCodeAttribute, out var defaultStatusCodeAttribute)) + { + return false; + } + + if (!TryGetType(ApiSymbolNames.IActionResult, out var iActionResult)) + { + return false; + } + + if (!TryGetType(ApiSymbolNames.IApiBehaviorMetadata, out var iApiBehaviorMetadata)) + { + return false; + } + + if (!TryGetType(ApiSymbolNames.ModelStateDictionary, out var modelStateDictionary)) + { + return false; + } + + if (!TryGetType(ApiSymbolNames.NonActionAttribute, out var nonActionAttribute)) + { + return false; + } + + if (!TryGetType(ApiSymbolNames.NonControllerAttribute, out var nonControllerAttribute)) + { + return false; + } + + if (!TryGetType(ApiSymbolNames.ProblemDetails, out var problemDetails)) + { + return false; + } + + if (!TryGetType(ApiSymbolNames.ProducesDefaultResponseTypeAttribute, out var producesDefaultResponseTypeAttribute)) + { + return false; + } + + if (!TryGetType(ApiSymbolNames.ProducesErrorResponseTypeAttribute, out var producesErrorResponseTypeAttribute)) + { + return false; + } + + if (!TryGetType(ApiSymbolNames.ProducesResponseTypeAttribute, out var producesResponseTypeAttribute)) + { + return false; + } var statusCodeActionResult = compilation.GetTypeByMetadataName(ApiSymbolNames.IStatusCodeActionResult); - StatusCodeActionResultStatusProperty = (IPropertySymbol)statusCodeActionResult?.GetMembers("StatusCode")[0]; + var statusCodeActionResultStatusProperty = (IPropertySymbol?)statusCodeActionResult?.GetMembers("StatusCode")[0]; + if (statusCodeActionResultStatusProperty == null) + { + return false; + } var disposable = compilation.GetSpecialType(SpecialType.System_IDisposable); - var members = disposable.GetMembers(nameof(IDisposable.Dispose)); - IDisposableDispose = members.Length == 1 ? (IMethodSymbol)members[0] : null; + var members = disposable?.GetMembers(nameof(IDisposable.Dispose)); + var iDisposableDispose = (IMethodSymbol?)members?[0]; + if (iDisposableDispose == null) + { + return false; + } + + symbolCache = new ApiControllerSymbolCache( + apiConventionMethodAttribute, + apiConventionNameMatchAttribute, + apiConventionTypeAttribute, + apiConventionTypeMatchAttribute, + controllerAttribute, + defaultStatusCodeAttribute, + iActionResult, + iApiBehaviorMetadata, + iDisposableDispose, + statusCodeActionResultStatusProperty, + modelStateDictionary, + nonActionAttribute, + nonControllerAttribute, + problemDetails, + producesDefaultResponseTypeAttribute, + producesResponseTypeAttribute, + producesErrorResponseTypeAttribute); + + return true; + + bool TryGetType(string typeName, out INamedTypeSymbol typeSymbol) + { + typeSymbol = compilation.GetTypeByMetadataName(typeName); + return typeSymbol != null && typeSymbol.TypeKind != TypeKind.Error; + } + } + + private ApiControllerSymbolCache( + INamedTypeSymbol apiConventionMethodAttribute, + INamedTypeSymbol apiConventionNameMatchAttribute, + INamedTypeSymbol apiConventionTypeAttribute, + INamedTypeSymbol apiConventionTypeMatchAttribute, + INamedTypeSymbol controllerAttribute, + INamedTypeSymbol defaultStatusCodeAttribute, + INamedTypeSymbol actionResult, + INamedTypeSymbol apiBehaviorMetadata, + IMethodSymbol disposableDispose, + IPropertySymbol statusCodeActionResultStatusProperty, + ITypeSymbol modelStateDictionary, + INamedTypeSymbol nonActionAttribute, + INamedTypeSymbol nonControllerAttribute, + INamedTypeSymbol problemDetails, + INamedTypeSymbol producesDefaultResponseTypeAttribute, + INamedTypeSymbol producesResponseTypeAttribute, + INamedTypeSymbol producesErrorResponseTypeAttribute) + { + ApiConventionMethodAttribute = apiConventionMethodAttribute; + ApiConventionNameMatchAttribute = apiConventionNameMatchAttribute; + ApiConventionTypeAttribute = apiConventionTypeAttribute; + ApiConventionTypeMatchAttribute = apiConventionTypeMatchAttribute; + ControllerAttribute = controllerAttribute; + DefaultStatusCodeAttribute = defaultStatusCodeAttribute; + IActionResult = actionResult; + IApiBehaviorMetadata = apiBehaviorMetadata; + IDisposableDispose = disposableDispose; + StatusCodeActionResultStatusProperty = statusCodeActionResultStatusProperty; + ModelStateDictionary = modelStateDictionary; + NonActionAttribute = nonActionAttribute; + NonControllerAttribute = nonControllerAttribute; + ProblemDetails = problemDetails; + ProducesDefaultResponseTypeAttribute = producesDefaultResponseTypeAttribute; + ProducesResponseTypeAttribute = producesResponseTypeAttribute; + ProducesErrorResponseTypeAttribute = producesErrorResponseTypeAttribute; } public INamedTypeSymbol ApiConventionMethodAttribute { get; } diff --git a/src/Mvc/Mvc.Api.Analyzers/src/ApiConventionAnalyzer.cs b/src/Mvc/Mvc.Api.Analyzers/src/ApiConventionAnalyzer.cs index 6b92ee4c8f..5890981a10 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/ApiConventionAnalyzer.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/ApiConventionAnalyzer.cs @@ -25,8 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers context.RegisterCompilationStartAction(compilationStartAnalysisContext => { - var symbolCache = new ApiControllerSymbolCache(compilationStartAnalysisContext.Compilation); - if (symbolCache.ApiConventionTypeAttribute == null || symbolCache.ApiConventionTypeAttribute.TypeKind == TypeKind.Error) + if (!ApiControllerSymbolCache.TryCreate(compilationStartAnalysisContext.Compilation, out var symbolCache)) { // No-op if we can't find types we care about. return; diff --git a/src/Mvc/Mvc.Api.Analyzers/src/DeclaredApiResponseMetadata.cs b/src/Mvc/Mvc.Api.Analyzers/src/DeclaredApiResponseMetadata.cs index 30819931d0..c8f2a9c48f 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/DeclaredApiResponseMetadata.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/DeclaredApiResponseMetadata.cs @@ -23,8 +23,8 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers private DeclaredApiResponseMetadata( int statusCode, - AttributeData attributeData, - IMethodSymbol attributeSource, + AttributeData? attributeData, + IMethodSymbol? attributeSource, bool @implicit, bool @default) { @@ -37,9 +37,9 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers public int StatusCode { get; } - public AttributeData Attribute { get; } + public AttributeData? Attribute { get; } - public IMethodSymbol AttributeSource { get; } + public IMethodSymbol? AttributeSource { get; } /// /// True if this is the implicit 200 associated with an diff --git a/src/Mvc/Mvc.Api.Analyzers/src/Microsoft.AspNetCore.Mvc.Api.Analyzers.csproj b/src/Mvc/Mvc.Api.Analyzers/src/Microsoft.AspNetCore.Mvc.Api.Analyzers.csproj index f5efb230a2..d827140aad 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/Microsoft.AspNetCore.Mvc.Api.Analyzers.csproj +++ b/src/Mvc/Mvc.Api.Analyzers/src/Microsoft.AspNetCore.Mvc.Api.Analyzers.csproj @@ -9,6 +9,8 @@ false false $(MSBuildProjectName).nuspec + enable + enable diff --git a/src/Mvc/Mvc.Api.Analyzers/src/SymbolApiResponseMetadataProvider.cs b/src/Mvc/Mvc.Api.Analyzers/src/SymbolApiResponseMetadataProvider.cs index 9b6a1b759c..e8c85489e7 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/SymbolApiResponseMetadataProvider.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/SymbolApiResponseMetadataProvider.cs @@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers return Array.Empty(); } - private static IMethodSymbol GetMethodFromConventionMethodAttribute(in ApiControllerSymbolCache symbolCache, IMethodSymbol method) + private static IMethodSymbol? GetMethodFromConventionMethodAttribute(in ApiControllerSymbolCache symbolCache, IMethodSymbol method) { var attribute = method.GetAttributes(symbolCache.ApiConventionMethodAttribute, inherit: true) .FirstOrDefault(); @@ -113,7 +113,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers return (IMethodSymbol)conventionMethod; } - private static IMethodSymbol MatchConventionMethod( + private static IMethodSymbol? MatchConventionMethod( in ApiControllerSymbolCache symbolCache, IMethodSymbol method, IReadOnlyList conventionTypes) diff --git a/src/Mvc/Mvc.Api.Analyzers/test/ActualApiResponseMetadataFactoryTest.cs b/src/Mvc/Mvc.Api.Analyzers/test/ActualApiResponseMetadataFactoryTest.cs index bf4a12fd67..d9c15bddaa 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/ActualApiResponseMetadataFactoryTest.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/ActualApiResponseMetadataFactoryTest.cs @@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers }"; var project = DiagnosticProject.Create(GetType().Assembly, new[] { source }); var compilation = await project.GetCompilationAsync(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var returnType = compilation.GetTypeByMetadataName($"{Namespace}.TestController"); var syntaxTree = returnType.DeclaringSyntaxReferences[0].SyntaxTree; @@ -307,7 +307,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var type = compilation.GetTypeByMetadataName(typeName); var method = (IMethodSymbol)type.GetMembers(methodName).First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var syntaxTree = method.DeclaringSyntaxReferences[0].SyntaxTree; var methodSyntax = (MethodDeclarationSyntax)syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan); @@ -322,7 +322,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers { // Arrange var compilation = await GetCompilation("InspectReturnExpressionTests"); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var controllerType = compilation.GetTypeByMetadataName(typeof(TestFiles.InspectReturnExpressionTests.TestController).FullName); var syntaxTree = controllerType.DeclaringSyntaxReferences[0].SyntaxTree; @@ -342,7 +342,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers { var project = DiagnosticProject.Create(GetType().Assembly, new[] { source }); var compilation = await project.GetCompilationAsync(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var returnType = compilation.GetTypeByMetadataName($"{Namespace}.{test}"); var syntaxTree = returnType.DeclaringSyntaxReferences[0].SyntaxTree; diff --git a/src/Mvc/Mvc.Api.Analyzers/test/ApiControllerFactsTest.cs b/src/Mvc/Mvc.Api.Analyzers/test/ApiControllerFactsTest.cs index e0875d0f93..786aeeb9d7 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/ApiControllerFactsTest.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/ApiControllerFactsTest.cs @@ -37,7 +37,7 @@ namespace TestNamespace }"; var project = DiagnosticProject.Create(GetType().Assembly, new[] { source }); var compilation = await project.GetCompilationAsync(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var method = (IMethodSymbol)compilation.GetTypeByMetadataName("TestNamespace.TestController").GetMembers("Get").First(); // Act @@ -52,7 +52,7 @@ namespace TestNamespace { // Arrange var compilation = await GetCompilation(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var type = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerTest_IndexModel).FullName); var method = (IMethodSymbol)type.GetMembers(nameof(ApiConventionAnalyzerTest_IndexModel.OnGet)).First(); @@ -68,7 +68,7 @@ namespace TestNamespace { // Arrange var compilation = await GetCompilation(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var type = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerTest_NotApiController).FullName); var method = (IMethodSymbol)type.GetMembers(nameof(ApiConventionAnalyzerTest_NotApiController.Index)).First(); @@ -84,7 +84,7 @@ namespace TestNamespace { // Arrange var compilation = await GetCompilation(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var type = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerTest_NotAction).FullName); var method = (IMethodSymbol)type.GetMembers(nameof(ApiConventionAnalyzerTest_NotAction.Index)).First(); @@ -100,7 +100,7 @@ namespace TestNamespace { // Arrange var compilation = await GetCompilation(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var type = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerTest_Valid).FullName); var method = (IMethodSymbol)type.GetMembers(nameof(ApiConventionAnalyzerTest_Valid.Index)).First(); @@ -116,7 +116,7 @@ namespace TestNamespace { // Arrange var compilation = await GetCompilation(nameof(IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssembly)); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var type = compilation.GetTypeByMetadataName(typeof(IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssemblyController).FullName); var method = (IMethodSymbol)type.GetMembers(nameof(IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssemblyController.Action)).First(); diff --git a/src/Mvc/Mvc.Api.Analyzers/test/Infrastructure/MvcTestSource.cs b/src/Mvc/Mvc.Api.Analyzers/test/Infrastructure/MvcTestSource.cs index fb20bd8cbf..6548f4306b 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/Infrastructure/MvcTestSource.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/Infrastructure/MvcTestSource.cs @@ -27,13 +27,16 @@ namespace Microsoft.AspNetCore.Mvc private static string GetProjectDirectory() { - // On helix we use the published test files + // On helix we use the published test files if (SkipOnHelixAttribute.OnHelix()) { return AppContext.BaseDirectory; } +// https://github.com/aspnet/AspNetCore/issues/9431 +#pragma warning disable 0618 var solutionDirectory = TestPathUtilities.GetSolutionRootDirectory("Mvc"); +#pragma warning restore 0618 var projectDirectory = Path.Combine(solutionDirectory, "Mvc.Api.Analyzers", "test"); return projectDirectory; } diff --git a/src/Mvc/Mvc.Api.Analyzers/test/Mvc.Api.Analyzers.Test.csproj b/src/Mvc/Mvc.Api.Analyzers/test/Mvc.Api.Analyzers.Test.csproj index 370646fc2b..c81ef1db1d 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/Mvc.Api.Analyzers.Test.csproj +++ b/src/Mvc/Mvc.Api.Analyzers/test/Mvc.Api.Analyzers.Test.csproj @@ -5,7 +5,7 @@ Microsoft.AspNetCore.Mvc.Api.Analyzers - + false @@ -20,7 +20,7 @@ - + diff --git a/src/Mvc/Mvc.Api.Analyzers/test/SymbolApiConventionMatcherTest.cs b/src/Mvc/Mvc.Api.Analyzers/test/SymbolApiConventionMatcherTest.cs index 864a6b3791..1d0ab00c1b 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/SymbolApiConventionMatcherTest.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/SymbolApiConventionMatcherTest.cs @@ -431,7 +431,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers private async Task RunMatchTest(string methodName, string conventionMethodName, bool expected) { var compilation = await GetCompilationAsync(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var testController = compilation.GetTypeByMetadataName(TestControllerName); var testConvention = compilation.GetTypeByMetadataName(TestConventionName); @@ -451,7 +451,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers // Arrange var expected = SymbolApiConventionNameMatchBehavior.Exact; var compilation = await GetCompilationAsync(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var testConvention = compilation.GetTypeByMetadataName(TestConventionName); var method = testConvention.GetMembers(nameof(TestConvention.MethodWithoutMatchBehavior)).First(); @@ -469,7 +469,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers // Arrange var expected = SymbolApiConventionNameMatchBehavior.Exact; var compilation = await GetCompilationAsync(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var testConvention = compilation.GetTypeByMetadataName(TestConventionName); var method = testConvention.GetMembers(nameof(TestConvention.MethodWithRandomAttributes)).First(); @@ -487,7 +487,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers // Arrange var expected = SymbolApiConventionNameMatchBehavior.Prefix; var compilation = await GetCompilationAsync(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var testConvention = compilation.GetTypeByMetadataName(TestConventionName); var method = testConvention.GetMembers(nameof(TestConvention.Get)).First(); @@ -505,7 +505,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers // Arrange var expected = SymbolApiConventionTypeMatchBehavior.AssignableFrom; var compilation = await GetCompilationAsync(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var testConvention = compilation.GetTypeByMetadataName(TestConventionName); var method = (IMethodSymbol)testConvention.GetMembers(nameof(TestConvention.Get)).First(); @@ -524,7 +524,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers // Arrange var expected = SymbolApiConventionTypeMatchBehavior.AssignableFrom; var compilation = await GetCompilationAsync(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var testConvention = compilation.GetTypeByMetadataName(TestConventionName); var method = (IMethodSymbol)testConvention.GetMembers(nameof(TestConvention.MethodParameterWithRandomAttributes)).First(); @@ -543,7 +543,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers // Arrange var expected = SymbolApiConventionTypeMatchBehavior.Any; var compilation = await GetCompilationAsync(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); var testConvention = compilation.GetTypeByMetadataName(TestConventionName); var method = (IMethodSymbol)testConvention.GetMembers(nameof(TestConvention.MethodWithAnyTypeMatchBehaviorParameter)).First(); diff --git a/src/Mvc/Mvc.Api.Analyzers/test/SymbolApiResponseMetadataProviderTest.cs b/src/Mvc/Mvc.Api.Analyzers/test/SymbolApiResponseMetadataProviderTest.cs index d93a2ee158..f9adfa062a 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/SymbolApiResponseMetadataProviderTest.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/SymbolApiResponseMetadataProviderTest.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerWithoutConvention)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerWithoutConvention.GetPerson)).First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); @@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerWithoutConvention)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerWithoutConvention.PostPerson)).First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); @@ -57,7 +57,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesAttribute)).First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); @@ -75,7 +75,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeInConstructor)).First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); @@ -98,7 +98,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeAndTypeInConstructor)).First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); @@ -121,7 +121,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeInConstructorAndProperty)).First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); @@ -144,7 +144,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeAndTypeInConstructorAndProperty)).First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); @@ -167,7 +167,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomProducesResponseTypeAttributeWithArguments)).First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); @@ -189,7 +189,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.GetResponseMetadata_ReturnsValuesFromApiConventionMethodAttribute)).First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); @@ -220,7 +220,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.GetResponseMetadata_WithProducesResponseTypeAndApiConventionMethod)).First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); @@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomApiResponseMetadataProvider)).First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); @@ -275,7 +275,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{typeName}"); var method = (IMethodSymbol)controller.GetMembers(methodName).First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); @@ -298,7 +298,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName(type.FullName); var method = (IMethodSymbol)controller.GetMembers().First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); @@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName(type.FullName); var method = (IMethodSymbol)controller.GetMembers().First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); @@ -410,7 +410,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var type = compilation.GetTypeByMetadataName(typeof(GetErrorResponseType_ReturnsProblemDetails_IfNoAttributeIsDiscoveredController).FullName); var method = (IMethodSymbol)type.GetMembers("Action").First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetErrorResponseType(symbolCache, method); @@ -428,7 +428,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var type = compilation.GetTypeByMetadataName(typeof(GetErrorResponseType_ReturnsTypeDefinedAtAssemblyController).FullName); var method = (IMethodSymbol)type.GetMembers("Action").First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetErrorResponseType(symbolCache, method); @@ -446,7 +446,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var type = compilation.GetTypeByMetadataName(typeof(GetErrorResponseType_ReturnsTypeDefinedAtControllerController).FullName); var method = (IMethodSymbol)type.GetMembers("Action").First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetErrorResponseType(symbolCache, method); @@ -464,7 +464,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var type = compilation.GetTypeByMetadataName(typeof(GetErrorResponseType_ReturnsTypeDefinedAtActionController).FullName); var method = (IMethodSymbol)type.GetMembers("Action").First(); - var symbolCache = new ApiControllerSymbolCache(compilation); + Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache)); // Act var result = SymbolApiResponseMetadataProvider.GetErrorResponseType(symbolCache, method); diff --git a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.csproj index c77d73737f..d319a85c95 100644 --- a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.csproj @@ -15,7 +15,6 @@ - diff --git a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs index 136cf9e1e0..54a8699f3f 100644 --- a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs +++ b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs @@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute(202)] public partial class AcceptedAtActionResult : Microsoft.AspNetCore.Mvc.ObjectResult { - public AcceptedAtActionResult(string actionName, string controllerName, object routeValues, object value) : base (default(object)) { } + public AcceptedAtActionResult(string actionName, string controllerName, object routeValues, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) : base (default(object)) { } public string ActionName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public string ControllerName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.AspNetCore.Routing.RouteValueDictionary RouteValues { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } @@ -48,8 +48,8 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute(202)] public partial class AcceptedAtRouteResult : Microsoft.AspNetCore.Mvc.ObjectResult { - public AcceptedAtRouteResult(object routeValues, object value) : base (default(object)) { } - public AcceptedAtRouteResult(string routeName, object routeValues, object value) : base (default(object)) { } + public AcceptedAtRouteResult(object routeValues, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) : base (default(object)) { } + public AcceptedAtRouteResult(string routeName, object routeValues, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) : base (default(object)) { } public string RouteName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.AspNetCore.Routing.RouteValueDictionary RouteValues { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.AspNetCore.Mvc.IUrlHelper UrlHelper { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } @@ -59,8 +59,8 @@ namespace Microsoft.AspNetCore.Mvc public partial class AcceptedResult : Microsoft.AspNetCore.Mvc.ObjectResult { public AcceptedResult() : base (default(object)) { } - public AcceptedResult(string location, object value) : base (default(object)) { } - public AcceptedResult(System.Uri locationUri, object value) : base (default(object)) { } + public AcceptedResult(string location, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) : base (default(object)) { } + public AcceptedResult(System.Uri locationUri, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) : base (default(object)) { } public string Location { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public override void OnFormatting(Microsoft.AspNetCore.Mvc.ActionContext context) { } } @@ -151,8 +151,8 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute(400)] public partial class BadRequestObjectResult : Microsoft.AspNetCore.Mvc.ObjectResult { - public BadRequestObjectResult(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) : base (default(object)) { } - public BadRequestObjectResult(object error) : base (default(object)) { } + public BadRequestObjectResult([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) : base (default(object)) { } + public BadRequestObjectResult([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object error) : base (default(object)) { } } [Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute(400)] public partial class BadRequestResult : Microsoft.AspNetCore.Mvc.StatusCodeResult @@ -226,8 +226,8 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute(409)] public partial class ConflictObjectResult : Microsoft.AspNetCore.Mvc.ObjectResult { - public ConflictObjectResult(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) : base (default(object)) { } - public ConflictObjectResult(object error) : base (default(object)) { } + public ConflictObjectResult([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) : base (default(object)) { } + public ConflictObjectResult([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object error) : base (default(object)) { } } [Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute(409)] public partial class ConflictResult : Microsoft.AspNetCore.Mvc.StatusCodeResult @@ -278,43 +278,43 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.AcceptedResult Accepted() { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.AcceptedResult Accepted(object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.AcceptedResult Accepted([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.AcceptedResult Accepted(string uri) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.AcceptedResult Accepted(string uri, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.AcceptedResult Accepted(string uri, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.AcceptedResult Accepted(System.Uri uri) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.AcceptedResult Accepted(System.Uri uri, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.AcceptedResult Accepted(System.Uri uri, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.AcceptedAtActionResult AcceptedAtAction(string actionName) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.AcceptedAtActionResult AcceptedAtAction(string actionName, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.AcceptedAtActionResult AcceptedAtAction(string actionName, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.AcceptedAtActionResult AcceptedAtAction(string actionName, object routeValues, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.AcceptedAtActionResult AcceptedAtAction(string actionName, object routeValues, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.AcceptedAtActionResult AcceptedAtAction(string actionName, string controllerName) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.AcceptedAtActionResult AcceptedAtAction(string actionName, string controllerName, object routeValues) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.AcceptedAtActionResult AcceptedAtAction(string actionName, string controllerName, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object routeValues) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.AcceptedAtActionResult AcceptedAtAction(string actionName, string controllerName, object routeValues, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.AcceptedAtActionResult AcceptedAtAction(string actionName, string controllerName, object routeValues, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.AcceptedAtRouteResult AcceptedAtRoute(object routeValues) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.AcceptedAtRouteResult AcceptedAtRoute([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object routeValues) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.AcceptedAtRouteResult AcceptedAtRoute(object routeValues, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.AcceptedAtRouteResult AcceptedAtRoute(object routeValues, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.AcceptedAtRouteResult AcceptedAtRoute(string routeName) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.AcceptedAtRouteResult AcceptedAtRoute(string routeName, object routeValues) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.AcceptedAtRouteResult AcceptedAtRoute(string routeName, object routeValues, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.AcceptedAtRouteResult AcceptedAtRoute(string routeName, object routeValues, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.BadRequestResult BadRequest() { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.BadRequestObjectResult BadRequest(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.BadRequestObjectResult BadRequest([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.BadRequestObjectResult BadRequest(object error) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.BadRequestObjectResult BadRequest([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object error) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.ChallengeResult Challenge() { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] @@ -326,9 +326,9 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.ConflictResult Conflict() { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.ConflictObjectResult Conflict(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.ConflictObjectResult Conflict([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.ConflictObjectResult Conflict(object error) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.ConflictObjectResult Conflict([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object error) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.ContentResult Content(string content) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] @@ -338,21 +338,21 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.ContentResult Content(string content, string contentType, System.Text.Encoding contentEncoding) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.CreatedResult Created(string uri, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.CreatedResult Created(string uri, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.CreatedResult Created(System.Uri uri, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.CreatedResult Created(System.Uri uri, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.CreatedAtActionResult CreatedAtAction(string actionName, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.CreatedAtActionResult CreatedAtAction(string actionName, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.CreatedAtActionResult CreatedAtAction(string actionName, object routeValues, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.CreatedAtActionResult CreatedAtAction(string actionName, object routeValues, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.CreatedAtActionResult CreatedAtAction(string actionName, string controllerName, object routeValues, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.CreatedAtActionResult CreatedAtAction(string actionName, string controllerName, object routeValues, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.CreatedAtRouteResult CreatedAtRoute(object routeValues, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.CreatedAtRouteResult CreatedAtRoute(object routeValues, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.CreatedAtRouteResult CreatedAtRoute(string routeName, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.CreatedAtRouteResult CreatedAtRoute(string routeName, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.CreatedAtRouteResult CreatedAtRoute(string routeName, object routeValues, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.CreatedAtRouteResult CreatedAtRoute(string routeName, object routeValues, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.FileContentResult File(byte[] fileContents, string contentType) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] @@ -422,11 +422,11 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.NotFoundResult NotFound() { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.NotFoundObjectResult NotFound(object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.NotFoundObjectResult NotFound([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.OkResult Ok() { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.OkObjectResult Ok(object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.OkObjectResult Ok([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.PhysicalFileResult PhysicalFile(string physicalPath, string contentType) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] @@ -540,9 +540,9 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.SignOutResult SignOut(params string[] authenticationSchemes) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.StatusCodeResult StatusCode(int statusCode) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.StatusCodeResult StatusCode([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultStatusCodeAttribute]int statusCode) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.ObjectResult StatusCode(int statusCode, object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.ObjectResult StatusCode([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultStatusCodeAttribute]int statusCode, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] [System.Diagnostics.DebuggerStepThroughAttribute] public virtual System.Threading.Tasks.Task TryUpdateModelAsync(object model, System.Type modelType, string prefix) { throw null; } @@ -572,19 +572,19 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.UnauthorizedResult Unauthorized() { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.UnauthorizedObjectResult Unauthorized(object value) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.UnauthorizedObjectResult Unauthorized([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.UnprocessableEntityResult UnprocessableEntity() { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.UnprocessableEntityObjectResult UnprocessableEntity(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.UnprocessableEntityObjectResult UnprocessableEntity([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.UnprocessableEntityObjectResult UnprocessableEntity(object error) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.UnprocessableEntityObjectResult UnprocessableEntity([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object error) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem() { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelStateDictionary) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelStateDictionary) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] - public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem(Microsoft.AspNetCore.Mvc.ValidationProblemDetails descriptor) { throw null; } + public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ValidationProblemDetails descriptor) { throw null; } } public partial class ControllerContext : Microsoft.AspNetCore.Mvc.ActionContext { @@ -601,7 +601,7 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute(201)] public partial class CreatedAtActionResult : Microsoft.AspNetCore.Mvc.ObjectResult { - public CreatedAtActionResult(string actionName, string controllerName, object routeValues, object value) : base (default(object)) { } + public CreatedAtActionResult(string actionName, string controllerName, object routeValues, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) : base (default(object)) { } public string ActionName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public string ControllerName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.AspNetCore.Routing.RouteValueDictionary RouteValues { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } @@ -611,8 +611,8 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute(201)] public partial class CreatedAtRouteResult : Microsoft.AspNetCore.Mvc.ObjectResult { - public CreatedAtRouteResult(object routeValues, object value) : base (default(object)) { } - public CreatedAtRouteResult(string routeName, object routeValues, object value) : base (default(object)) { } + public CreatedAtRouteResult(object routeValues, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) : base (default(object)) { } + public CreatedAtRouteResult(string routeName, object routeValues, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) : base (default(object)) { } public string RouteName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.AspNetCore.Routing.RouteValueDictionary RouteValues { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.AspNetCore.Mvc.IUrlHelper UrlHelper { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } @@ -914,7 +914,7 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute(404)] public partial class NotFoundObjectResult : Microsoft.AspNetCore.Mvc.ObjectResult { - public NotFoundObjectResult(object value) : base (default(object)) { } + public NotFoundObjectResult([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) : base (default(object)) { } } [Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute(404)] public partial class NotFoundResult : Microsoft.AspNetCore.Mvc.StatusCodeResult @@ -928,6 +928,7 @@ namespace Microsoft.AspNetCore.Mvc public System.Type DeclaredType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.AspNetCore.Mvc.Formatters.FormatterCollection Formatters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public int? StatusCode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute] public object Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public override System.Threading.Tasks.Task ExecuteResultAsync(Microsoft.AspNetCore.Mvc.ActionContext context) { throw null; } public virtual void OnFormatting(Microsoft.AspNetCore.Mvc.ActionContext context) { } @@ -1167,7 +1168,7 @@ namespace Microsoft.AspNetCore.Mvc } public partial class StatusCodeResult : Microsoft.AspNetCore.Mvc.ActionResult, Microsoft.AspNetCore.Mvc.IActionResult, Microsoft.AspNetCore.Mvc.Infrastructure.IClientErrorActionResult, Microsoft.AspNetCore.Mvc.Infrastructure.IStatusCodeActionResult { - public StatusCodeResult(int statusCode) { } + public StatusCodeResult([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultStatusCodeAttribute]int statusCode) { } int? Microsoft.AspNetCore.Mvc.Infrastructure.IStatusCodeActionResult.StatusCode { get { throw null; } } public int StatusCode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } public override void ExecuteResult(Microsoft.AspNetCore.Mvc.ActionContext context) { } @@ -1186,7 +1187,7 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute(401)] public partial class UnauthorizedObjectResult : Microsoft.AspNetCore.Mvc.ObjectResult { - public UnauthorizedObjectResult(object value) : base (default(object)) { } + public UnauthorizedObjectResult([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object value) : base (default(object)) { } } [Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute(401)] public partial class UnauthorizedResult : Microsoft.AspNetCore.Mvc.StatusCodeResult @@ -1196,8 +1197,8 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute(422)] public partial class UnprocessableEntityObjectResult : Microsoft.AspNetCore.Mvc.ObjectResult { - public UnprocessableEntityObjectResult(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) : base (default(object)) { } - public UnprocessableEntityObjectResult(object error) : base (default(object)) { } + public UnprocessableEntityObjectResult([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelState) : base (default(object)) { } + public UnprocessableEntityObjectResult([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]object error) : base (default(object)) { } } [Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute(422)] public partial class UnprocessableEntityResult : Microsoft.AspNetCore.Mvc.StatusCodeResult @@ -1547,6 +1548,12 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts protected ApplicationPart() { } public abstract string Name { get; } } + [System.AttributeUsageAttribute(System.AttributeTargets.Assembly, AllowMultiple=true)] + public sealed partial class ApplicationPartAttribute : System.Attribute + { + public ApplicationPartAttribute(string assemblyName) { } + public string AssemblyName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } public abstract partial class ApplicationPartFactory { protected ApplicationPartFactory() { } @@ -1560,13 +1567,12 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts public System.Collections.Generic.IList FeatureProviders { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } public void PopulateFeature(TFeature feature) { } } - public partial class AssemblyPart : Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPart, Microsoft.AspNetCore.Mvc.ApplicationParts.IApplicationPartTypeProvider, Microsoft.AspNetCore.Mvc.ApplicationParts.ICompilationReferencesProvider + public partial class AssemblyPart : Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPart, Microsoft.AspNetCore.Mvc.ApplicationParts.IApplicationPartTypeProvider { public AssemblyPart(System.Reflection.Assembly assembly) { } public System.Reflection.Assembly Assembly { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } public override string Name { get { throw null; } } public System.Collections.Generic.IEnumerable Types { get { throw null; } } - public System.Collections.Generic.IEnumerable GetReferencePaths() { throw null; } } public partial class DefaultApplicationPartFactory : Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartFactory { @@ -1917,6 +1923,16 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure public abstract Microsoft.AspNetCore.Mvc.Infrastructure.ActionDescriptorCollection ActionDescriptors { get; } public abstract Microsoft.Extensions.Primitives.IChangeToken GetChangeToken(); } + [System.AttributeUsageAttribute(System.AttributeTargets.Parameter | System.AttributeTargets.Property, AllowMultiple=false, Inherited=false)] + public sealed partial class ActionResultObjectValueAttribute : System.Attribute + { + public ActionResultObjectValueAttribute() { } + } + [System.AttributeUsageAttribute(System.AttributeTargets.Parameter, AllowMultiple=false, Inherited=false)] + public sealed partial class ActionResultStatusCodeAttribute : System.Attribute + { + public ActionResultStatusCodeAttribute() { } + } public partial class CompatibilitySwitch : Microsoft.AspNetCore.Mvc.Infrastructure.ICompatibilitySwitch where TValue : struct { public CompatibilitySwitch(string name) { } diff --git a/src/Mvc/Mvc.Core/src/ApplicationParts/ApplicationAssembliesProvider.cs b/src/Mvc/Mvc.Core/src/ApplicationParts/ApplicationAssembliesProvider.cs deleted file mode 100644 index c7169c7230..0000000000 --- a/src/Mvc/Mvc.Core/src/ApplicationParts/ApplicationAssembliesProvider.cs +++ /dev/null @@ -1,295 +0,0 @@ -// 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.Reflection; -using Microsoft.AspNetCore.Mvc.Core; -using Microsoft.Extensions.DependencyModel; - -namespace Microsoft.AspNetCore.Mvc.ApplicationParts -{ - internal class ApplicationAssembliesProvider - { - internal static HashSet ReferenceAssemblies { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) - { - // The deps file for the Microsoft.AspNetCore.App shared runtime is authored in a way where it does not say - // it depends on Microsoft.AspNetCore.Mvc even though it does. Explicitly list it so that referencing this runtime causes - // assembly discovery to work correctly. - "Microsoft.AspNetCore.App", - "Microsoft.AspNetCore.Mvc", - "Microsoft.AspNetCore.Mvc.Abstractions", - "Microsoft.AspNetCore.Mvc.ApiExplorer", - "Microsoft.AspNetCore.Mvc.Core", - "Microsoft.AspNetCore.Mvc.Cors", - "Microsoft.AspNetCore.Mvc.DataAnnotations", - "Microsoft.AspNetCore.Mvc.Formatters.Json", - "Microsoft.AspNetCore.Mvc.Formatters.Xml", - "Microsoft.AspNetCore.Mvc.Localization", - "Microsoft.AspNetCore.Mvc.NewtonsoftJson", - "Microsoft.AspNetCore.Mvc.Razor", - "Microsoft.AspNetCore.Mvc.RazorPages", - "Microsoft.AspNetCore.Mvc.TagHelpers", - "Microsoft.AspNetCore.Mvc.ViewFeatures", - }; - - /// - /// Returns an ordered list of application assemblies. - /// - /// The order is as follows: - /// * Entry assembly - /// * Assemblies specified in the application's deps file ordered by name. - /// - /// Each assembly is immediately followed by assemblies specified by annotated ordered by name. - /// - /// - /// - public IEnumerable ResolveAssemblies(Assembly entryAssembly) - { - var dependencyContext = LoadDependencyContext(entryAssembly); - - IEnumerable assemblyItems; - - if (dependencyContext == null || dependencyContext.CompileLibraries.Count == 0) - { - // If an application was built with PreserveCompilationContext = false, CompileLibraries will be empty and we - // can no longer reliably infer the dependency closure. In this case, treat it the same as a missing - // deps file. - assemblyItems = new[] { GetAssemblyItem(entryAssembly) }; - } - else - { - assemblyItems = ResolveFromDependencyContext(dependencyContext); - } - - assemblyItems = assemblyItems - .OrderBy(item => item.Assembly == entryAssembly ? 0 : 1) - .ThenBy(item => item.Assembly.FullName, StringComparer.Ordinal); - - foreach (var item in assemblyItems) - { - yield return item.Assembly; - - foreach (var associatedAssembly in item.RelatedAssemblies.Distinct().OrderBy(assembly => assembly.FullName, StringComparer.Ordinal)) - { - yield return associatedAssembly; - } - } - } - - protected virtual DependencyContext LoadDependencyContext(Assembly assembly) => DependencyContext.Load(assembly); - - private List ResolveFromDependencyContext(DependencyContext dependencyContext) - { - var assemblyItems = new List(); - var relatedAssemblies = new Dictionary(); - - var candidateAssemblies = GetCandidateLibraries(dependencyContext) - .SelectMany(library => GetLibraryAssemblies(dependencyContext, library)); - - foreach (var assembly in candidateAssemblies) - { - var assemblyItem = GetAssemblyItem(assembly); - assemblyItems.Add(assemblyItem); - - foreach (var relatedAssembly in assemblyItem.RelatedAssemblies) - { - if (relatedAssemblies.TryGetValue(relatedAssembly, out var otherEntry)) - { - var message = string.Join( - Environment.NewLine, - Resources.FormatApplicationAssembliesProvider_DuplicateRelatedAssembly(relatedAssembly.FullName), - otherEntry.Assembly.FullName, - assembly.FullName); - - throw new InvalidOperationException(message); - } - - relatedAssemblies.Add(relatedAssembly, assemblyItem); - } - } - - // Remove any top level assemblies that appear as an associated assembly. - assemblyItems.RemoveAll(item => relatedAssemblies.ContainsKey(item.Assembly)); - - return assemblyItems; - } - - protected virtual IEnumerable GetLibraryAssemblies(DependencyContext dependencyContext, RuntimeLibrary runtimeLibrary) - { - foreach (var assemblyName in runtimeLibrary.GetDefaultAssemblyNames(dependencyContext)) - { - var assembly = Assembly.Load(assemblyName); - yield return assembly; - } - } - - protected virtual IReadOnlyList GetRelatedAssemblies(Assembly assembly) - { - // Do not require related assemblies to be available in the default code path. - return RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: false); - } - - private AssemblyItem GetAssemblyItem(Assembly assembly) - { - var relatedAssemblies = GetRelatedAssemblies(assembly); - - // Ensure we don't have any cycles. A cycle could be formed if a related assembly points to the primary assembly. - foreach (var relatedAssembly in relatedAssemblies) - { - if (relatedAssembly.IsDefined(typeof(RelatedAssemblyAttribute))) - { - throw new InvalidOperationException( - Resources.FormatApplicationAssembliesProvider_RelatedAssemblyCannotDefineAdditional(relatedAssembly.FullName, assembly.FullName)); - } - } - - return new AssemblyItem(assembly, relatedAssemblies); - } - - // Returns a list of libraries that references the assemblies in . - // By default it returns all assemblies that reference any of the primary MVC assemblies - // while ignoring MVC assemblies. - // Internal for unit testing - internal static IEnumerable GetCandidateLibraries(DependencyContext dependencyContext) - { - // When using the Microsoft.AspNetCore.App shared runtimes, entries in the RuntimeLibraries - // get "erased" and it is no longer accurate to query to determine a library's dependency closure. - // We'll use CompileLibraries to calculate the dependency graph and runtime library to resolve assemblies to inspect. - var candidatesResolver = new CandidateResolver(dependencyContext.CompileLibraries, ReferenceAssemblies); - foreach (var library in dependencyContext.RuntimeLibraries) - { - if (candidatesResolver.IsCandidate(library)) - { - yield return library; - } - } - } - - private class CandidateResolver - { - private readonly IDictionary _compileLibraries; - - public CandidateResolver(IReadOnlyList compileLibraries, ISet referenceAssemblies) - { - var compileLibrariesWithNoDuplicates = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var library in compileLibraries) - { - if (compileLibrariesWithNoDuplicates.ContainsKey(library.Name)) - { - throw new InvalidOperationException(Resources.FormatCandidateResolver_DifferentCasedReference(library.Name)); - } - - compileLibrariesWithNoDuplicates.Add(library.Name, CreateDependency(library, referenceAssemblies)); - } - - _compileLibraries = compileLibrariesWithNoDuplicates; - } - - public bool IsCandidate(Library library) - { - var classification = ComputeClassification(library.Name); - return classification == DependencyClassification.ReferencesMvc; - } - - private Dependency CreateDependency(Library library, ISet referenceAssemblies) - { - var classification = DependencyClassification.Unknown; - if (referenceAssemblies.Contains(library.Name)) - { - classification = DependencyClassification.MvcReference; - } - - return new Dependency(library, classification); - } - - private DependencyClassification ComputeClassification(string dependency) - { - if (!_compileLibraries.ContainsKey(dependency)) - { - // Library does not have runtime dependency. Since we can't infer - // anything about it's references, we'll assume it does not have a reference to Mvc. - return DependencyClassification.DoesNotReferenceMvc; - } - - var candidateEntry = _compileLibraries[dependency]; - if (candidateEntry.Classification != DependencyClassification.Unknown) - { - return candidateEntry.Classification; - } - else - { - var classification = DependencyClassification.DoesNotReferenceMvc; - foreach (var candidateDependency in candidateEntry.Library.Dependencies) - { - var dependencyClassification = ComputeClassification(candidateDependency.Name); - if (dependencyClassification == DependencyClassification.ReferencesMvc || - dependencyClassification == DependencyClassification.MvcReference) - { - classification = DependencyClassification.ReferencesMvc; - break; - } - } - - candidateEntry.Classification = classification; - - return classification; - } - } - - private class Dependency - { - public Dependency(Library library, DependencyClassification classification) - { - Library = library; - Classification = classification; - } - - public Library Library { get; } - - public DependencyClassification Classification { get; set; } - - public override string ToString() - { - return $"Library: {Library.Name}, Classification: {Classification}"; - } - } - - private enum DependencyClassification - { - Unknown = 0, - - /// - /// References (directly or transitively) one of the Mvc packages listed in - /// . - /// - ReferencesMvc = 1, - - /// - /// Does not reference (directly or transitively) one of the Mvc packages listed by - /// . - /// - DoesNotReferenceMvc = 2, - - /// - /// One of the references listed in . - /// - MvcReference = 3, - } - } - - private readonly struct AssemblyItem - { - public AssemblyItem(Assembly assembly, IReadOnlyList associatedAssemblies) - { - Assembly = assembly; - RelatedAssemblies = associatedAssemblies; - } - - public Assembly Assembly { get; } - - public IReadOnlyList RelatedAssemblies { get; } - } - } -} diff --git a/src/Mvc/Mvc.Core/src/ApplicationParts/ApplicationPartAttribute.cs b/src/Mvc/Mvc.Core/src/ApplicationParts/ApplicationPartAttribute.cs new file mode 100644 index 0000000000..e698396929 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/ApplicationParts/ApplicationPartAttribute.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; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + /// + /// Specifies an assembly to be added as an . + /// + /// In the ordinary case, MVC will generate + /// instances on the entry assembly for each dependency that references MVC. + /// Each of these assemblies is treated as an . + /// + /// + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class ApplicationPartAttribute : Attribute + { + /// + /// Initializes a new instance of . + /// + /// The assembly name. + public ApplicationPartAttribute(string assemblyName) + { + AssemblyName = assemblyName ?? throw new ArgumentNullException(nameof(assemblyName)); + } + + /// + /// Gets the assembly name. + /// + public string AssemblyName { get; } + } +} diff --git a/src/Mvc/Mvc.Core/src/ApplicationParts/ApplicationPartManager.cs b/src/Mvc/Mvc.Core/src/ApplicationParts/ApplicationPartManager.cs index 99e1c608e1..0dd8681fe1 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationParts/ApplicationPartManager.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationParts/ApplicationPartManager.cs @@ -52,17 +52,55 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts internal void PopulateDefaultParts(string entryAssemblyName) { - var entryAssembly = Assembly.Load(new AssemblyName(entryAssemblyName)); - var assembliesProvider = new ApplicationAssembliesProvider(); - var applicationAssemblies = assembliesProvider.ResolveAssemblies(entryAssembly); + var assemblies = GetApplicationPartAssemblies(entryAssemblyName); - foreach (var assembly in applicationAssemblies) + var seenAssemblies = new HashSet(); + + foreach (var assembly in assemblies) { - var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly); - foreach (var part in partFactory.GetApplicationParts(assembly)) + if (!seenAssemblies.Add(assembly)) { - ApplicationParts.Add(part); + // "assemblies" may contain duplicate values, but we want unique ApplicationPart instances. + // Note that we prefer using a HashSet over Distinct since the latter isn't + // guaranteed to preserve the original ordering. + continue; } + + var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly); + foreach (var applicationPart in partFactory.GetApplicationParts(assembly)) + { + ApplicationParts.Add(applicationPart); + } + } + } + + private static IEnumerable GetApplicationPartAssemblies(string entryAssemblyName) + { + var entryAssembly = Assembly.Load(new AssemblyName(entryAssemblyName)); + + // Use ApplicationPartAttribute to get the closure of direct or transitive dependencies + // that reference MVC. + var assembliesFromAttributes = entryAssembly.GetCustomAttributes() + .Select(name => Assembly.Load(name.AssemblyName)) + .OrderBy(assembly => assembly.FullName, StringComparer.Ordinal) + .SelectMany(GetAsemblyClosure); + + // The SDK will not include the entry assembly as an application part. We'll explicitly list it + // and have it appear before all other assemblies \ ApplicationParts. + return GetAsemblyClosure(entryAssembly) + .Concat(assembliesFromAttributes); + } + + private static IEnumerable GetAsemblyClosure(Assembly assembly) + { + yield return assembly; + + var relatedAssemblies = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: false) + .OrderBy(assembly => assembly.FullName, StringComparer.Ordinal); + + foreach (var relatedAssembly in relatedAssemblies) + { + yield return relatedAssembly; } } } diff --git a/src/Mvc/Mvc.Core/src/ApplicationParts/AssemblyPart.cs b/src/Mvc/Mvc.Core/src/ApplicationParts/AssemblyPart.cs index e3df2d6a14..ec2d2a76f0 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationParts/AssemblyPart.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationParts/AssemblyPart.cs @@ -5,17 +5,13 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using Microsoft.Extensions.DependencyModel; namespace Microsoft.AspNetCore.Mvc.ApplicationParts { /// /// An backed by an . /// - public class AssemblyPart : - ApplicationPart, - IApplicationPartTypeProvider, - ICompilationReferencesProvider + public class AssemblyPart : ApplicationPart, IApplicationPartTypeProvider { /// /// Initializes a new instance. @@ -39,27 +35,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts /// public IEnumerable Types => Assembly.DefinedTypes; - /// - public IEnumerable GetReferencePaths() - { - if (Assembly.IsDynamic) - { - // Skip loading process for dynamic assemblies. This prevents DependencyContextLoader from reading the - // .deps.json file from either manifest resources or the assembly location, which will fail. - return Enumerable.Empty(); - } - - var dependencyContext = DependencyContext.Load(Assembly); - if (dependencyContext != null) - { - return dependencyContext.CompileLibraries.SelectMany(library => library.ResolveReferencePaths()); - } - - // If an application has been compiled without preserveCompilationContext, return the path to the assembly - // as a reference. For runtime compilation, this will allow the compilation to succeed as long as it least - // one application part has been compiled with preserveCompilationContext and contains a super set of types - // required for the compilation to succeed. - return new[] { Assembly.Location }; - } + } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ActionResultObjectValueAttribute.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ActionResultObjectValueAttribute.cs index 8a87c33b2b..dc1891561a 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ActionResultObjectValueAttribute.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ActionResultObjectValueAttribute.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure /// ObjectResult { [ActionResultObjectValueAttribute] public object Value { get; set; } } /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] - internal sealed class ActionResultObjectValueAttribute : Attribute + public sealed class ActionResultObjectValueAttribute : Attribute { } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ActionResultStatusCodeAttribute.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ActionResultStatusCodeAttribute.cs index 2b4d1d4119..9d5709ac95 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ActionResultStatusCodeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ActionResultStatusCodeAttribute.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure /// StatusCodeResult([ActionResultStatusCodeParameter] int statusCode) /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] - internal sealed class ActionResultStatusCodeAttribute : Attribute + public sealed class ActionResultStatusCodeAttribute : Attribute { } } diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index da787d60ca..fafac71bf7 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -42,7 +42,6 @@ Microsoft.AspNetCore.Mvc.RouteAttribute - diff --git a/src/Mvc/Mvc.Core/test/ApplicationParts/ApplicationAssembliesProviderTest.cs b/src/Mvc/Mvc.Core/test/ApplicationParts/ApplicationAssembliesProviderTest.cs deleted file mode 100644 index b7c040d42e..0000000000 --- a/src/Mvc/Mvc.Core/test/ApplicationParts/ApplicationAssembliesProviderTest.cs +++ /dev/null @@ -1,544 +0,0 @@ -// 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.Reflection; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyModel; -using Newtonsoft.Json; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.ApplicationParts -{ - public class ApplicationAssembliesProviderTest - { - private static readonly Assembly ThisAssembly = typeof(ApplicationAssembliesProviderTest).Assembly; - - [Fact] - public void ResolveAssemblies_ReturnsCurrentAssembly_IfNoDepsFileIsPresent() - { - // Arrange - var provider = new TestApplicationAssembliesProvider(); - - // Act - var result = provider.ResolveAssemblies(ThisAssembly); - - // Assert - Assert.Equal(new[] { ThisAssembly }, result); - } - - [Fact] - public void ResolveAssemblies_ReturnsCurrentAssembly_IfDepsFileDoesNotHaveAnyCompileLibraries() - { - // Arrange - var runtimeLibraries = new[] - { - GetRuntimeLibrary("MyApp", "Microsoft.AspNetCore.App"), - GetRuntimeLibrary("Microsoft.AspNetCore.App", "Microsoft.NETCore.App"), - GetRuntimeLibrary("Microsoft.NETCore.App"), - GetRuntimeLibrary("ClassLibrary"), - }; - var dependencyContext = GetDependencyContext(compileLibraries: Array.Empty(), runtimeLibraries); - var provider = new TestApplicationAssembliesProvider - { - DependencyContext = dependencyContext, - }; - - // Act - var result = provider.ResolveAssemblies(ThisAssembly); - - // Assert - Assert.Equal(new[] { ThisAssembly }, result); - } - - [Fact] - public void ResolveAssemblies_ReturnsRelatedAssembliesOrderedByName() - { - // Arrange - var assembly1 = typeof(ApplicationAssembliesProvider).Assembly; - var assembly2 = typeof(IActionResult).Assembly; - var assembly3 = typeof(FactAttribute).Assembly; - - var relatedAssemblies = new[] { assembly1, assembly2, assembly3 }; - var provider = new TestApplicationAssembliesProvider - { - GetRelatedAssembliesDelegate = (assembly) => relatedAssemblies, - }; - - // Act - var result = provider.ResolveAssemblies(ThisAssembly); - - // Assert - Assert.Equal(new[] { ThisAssembly, assembly2, assembly1, assembly3 }, result); - } - - [Fact] - public void ResolveAssemblies_ReturnsLibrariesFromTheDepsFileThatReferenceMvc() - { - // Arrange - var mvcAssembly = typeof(IActionResult).Assembly; - var classLibrary = typeof(FactAttribute).Assembly; - - var libraries = new Dictionary - { - [ThisAssembly.GetName().Name] = new[] { mvcAssembly.GetName().Name, classLibrary.GetName().Name }, - [mvcAssembly.GetName().Name] = Array.Empty(), - [classLibrary.GetName().Name] = new[] { mvcAssembly.GetName().Name }, - }; - - var dependencyContext = GetDependencyContext(libraries); - - var provider = new TestApplicationAssembliesProvider - { - DependencyContext = dependencyContext, - }; - - // Act - var result = provider.ResolveAssemblies(ThisAssembly); - - // Assert - Assert.Equal(new[] { ThisAssembly, classLibrary, }, result); - } - - [Fact] - public void ResolveAssemblies_ReturnsRelatedAssembliesForLibrariesFromDepsFile() - { - // Arrange - var mvcAssembly = typeof(IActionResult).Assembly; - var classLibrary = typeof(object).Assembly; - var relatedPart = typeof(FactAttribute).Assembly; - - var libraries = new Dictionary - { - [ThisAssembly.GetName().Name] = new[] { relatedPart.GetName().Name, classLibrary.GetName().Name }, - [classLibrary.GetName().Name] = new[] { mvcAssembly.GetName().Name }, - [relatedPart.GetName().Name] = new[] { mvcAssembly.GetName().Name }, - [mvcAssembly.GetName().Name] = Array.Empty(), - }; - - var dependencyContext = GetDependencyContext(libraries); - - var provider = new TestApplicationAssembliesProvider - { - DependencyContext = dependencyContext, - GetRelatedAssembliesDelegate = (assembly) => - { - if (assembly == classLibrary) - { - return new[] { relatedPart }; - } - - return Array.Empty(); - }, - }; - - // Act - var result = provider.ResolveAssemblies(ThisAssembly); - - // Assert - Assert.Equal(new[] { ThisAssembly, classLibrary, relatedPart, }, result); - } - - [Fact] - public void ResolveAssemblies_ThrowsIfRelatedAssemblyDefinesAdditionalRelatedAssemblies() - { - // Arrange - var expected = $"Assembly 'TestRelatedAssembly' declared as a related assembly by assembly '{ThisAssembly}' cannot define additional related assemblies."; - var assembly1 = typeof(ApplicationAssembliesProvider).Assembly; - var assembly2 = new TestAssembly(); - - var relatedAssemblies = new[] { assembly1, assembly2 }; - var provider = new TestApplicationAssembliesProvider - { - GetRelatedAssembliesDelegate = (assembly) => relatedAssemblies, - }; - - // Act & Assert - var ex = Assert.Throws(() => provider.ResolveAssemblies(ThisAssembly).ToArray()); - Assert.Equal(expected, ex.Message); - } - - [Fact] - public void ResolveAssemblies_ThrowsIfMultipleAssembliesDeclareTheSameRelatedPart() - { - // Arrange - var mvcAssembly = typeof(IActionResult).Assembly; - var libraryAssembly1 = typeof(HttpContext).Assembly; - var libraryAssembly2 = typeof(JsonConverter).Assembly; - var relatedPart = typeof(FactAttribute).Assembly; - var expected = string.Join( - Environment.NewLine, - $"Each related assembly must be declared by exactly one assembly. The assembly '{relatedPart.FullName}' was declared as related assembly by the following:", - libraryAssembly1.FullName, - libraryAssembly2.FullName); - - var libraries = new Dictionary - { - [ThisAssembly.GetName().Name] = new[] { relatedPart.GetName().Name, libraryAssembly1.GetName().Name }, - [libraryAssembly1.GetName().Name] = new[] { mvcAssembly.GetName().Name }, - [libraryAssembly2.GetName().Name] = new[] { mvcAssembly.GetName().Name }, - [mvcAssembly.GetName().Name] = Array.Empty(), - }; - - var dependencyContext = GetDependencyContext(libraries); - - var provider = new TestApplicationAssembliesProvider - { - DependencyContext = dependencyContext, - GetRelatedAssembliesDelegate = (assembly) => - { - if (assembly == libraryAssembly1 || assembly == libraryAssembly2) - { - return new[] { relatedPart }; - } - - return Array.Empty(); - }, - }; - - // Act & Assert - var ex = Assert.Throws(() => provider.ResolveAssemblies(ThisAssembly).ToArray()); - Assert.Equal(expected, ex.Message); - } - - [Fact] - public void CandidateResolver_ThrowsIfDependencyContextContainsDuplicateRuntimeLibraryNames() - { - // Arrange - var upperCaseLibrary = "Microsoft.AspNetCore.Mvc"; - var mixedCaseLibrary = "microsoft.aspNetCore.mvc"; - - var libraries = new Dictionary - { - [upperCaseLibrary] = Array.Empty(), - [mixedCaseLibrary] = Array.Empty(), - }; - var dependencyContext = GetDependencyContext(libraries); - - // Act - var exception = Assert.Throws(() => ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext).ToArray()); - - // Assert - Assert.Equal($"A duplicate entry for library reference {mixedCaseLibrary} was found. Please check that all package references in all projects use the same casing for the same package references.", exception.Message); - } - - [Fact] - public void GetCandidateLibraries_IgnoresMvcAssemblies() - { - // Arrange - var expected = GetRuntimeLibrary("SomeRandomAssembly", "Microsoft.AspNetCore.Mvc.Abstractions"); - var runtimeLibraries = new[] - { - GetRuntimeLibrary("Microsoft.AspNetCore.Mvc.Core"), - GetRuntimeLibrary("Microsoft.AspNetCore.Mvc"), - GetRuntimeLibrary("Microsoft.AspNetCore.Mvc.Abstractions"), - expected, - }; - - var compileLibraries = new[] - { - GetCompileLibrary("Microsoft.AspNetCore.Mvc.Core"), - GetCompileLibrary("Microsoft.AspNetCore.Mvc"), - GetCompileLibrary("Microsoft.AspNetCore.Mvc.Abstractions"), - GetCompileLibrary("SomeRandomAssembly", "Microsoft.AspNetCore.Mvc.Abstractions"), - }; - var dependencyContext = GetDependencyContext(compileLibraries, runtimeLibraries); - - // Act - var candidates = ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext); - - // Assert - Assert.Equal(new[] { expected }, candidates); - } - - [Fact] - public void GetCandidateLibraries_ReturnsRuntimeLibraries_IfCompileLibraryDependencyToMvcIsPresent() - { - // Arrange - // When an app is running against Microsoft.AspNetCore.App shared runtime or if the DependencyContext is queried - // from an app that's running on Microsoft.NETCore.App (e.g. in a unit testing scenario), the - // runtime library does not state that the app references Mvc whereas the compile library does. This test validates - // that we correctly recognize this scenario. - var expected = GetRuntimeLibrary("MyApp", "Microsoft.AspNetCore.App"); - var runtimeLibraries = new[] - { - expected, - GetRuntimeLibrary("Microsoft.AspNetCore.App", "Microsoft.NETCore.App"), - GetRuntimeLibrary("Microsoft.NETCore.App"), - }; - - var compileLibraries = new[] - { - GetCompileLibrary("MyApp", "Microsoft.AspNetCore.App"), - GetCompileLibrary("Microsoft.AspNetCore.App", "Microsoft.AspNetCore.Mvc", "Microsoft.NETCore.App"), - GetCompileLibrary("Microsoft.AspNetCore.Mvc"), - GetCompileLibrary("Microsoft.NETCore.App"), - }; - var dependencyContext = GetDependencyContext(compileLibraries, runtimeLibraries); - - // Act - var candidates = ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext); - - // Assert - Assert.Equal(new[] { expected }, candidates); - } - - [Fact] - public void GetCandidateLibraries_DoesNotThrow_IfLibraryDoesNotAppearAsCompileOrRuntimeLibrary() - { - // Arrange - var expected = GetRuntimeLibrary("MyApplication", "Microsoft.AspNetCore.Server.Kestrel", "Microsoft.AspNetCore.Mvc"); - var compileLibraries = new[] - { - GetCompileLibrary("MyApplication", "Microsoft.AspNetCore.Server.Kestrel", "Microsoft.AspNetCore.Mvc"), - GetCompileLibrary("Microsoft.AspNetCore.Mvc"), - GetCompileLibrary("Microsoft.AspNetCore.Server.Kestrel", "Libuv"), - }; - - var runtimeLibraries = new[] - { - expected, - GetRuntimeLibrary("Microsoft.AspNetCore.Server.Kestrel", "Libuv"), - GetRuntimeLibrary("Microsoft.AspNetCore.Mvc"), - }; - - var dependencyContext = GetDependencyContext(compileLibraries, runtimeLibraries); - - // Act - var candidates = ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext).ToList(); - - // Assert - Assert.Equal(new[] { expected }, candidates); - } - - [Fact] - public void GetCandidateLibraries_DoesNotThrow_IfCompileLibraryIsPresentButNotRuntimeLibrary() - { - // Arrange - var expected = GetRuntimeLibrary("MyApplication", "Microsoft.AspNetCore.Server.Kestrel", "Microsoft.AspNetCore.Mvc"); - var compileLibraries = new[] - { - GetCompileLibrary("MyApplication", "Microsoft.AspNetCore.Server.Kestrel", "Microsoft.AspNetCore.Mvc"), - GetCompileLibrary("Microsoft.AspNetCore.Mvc"), - GetCompileLibrary("Microsoft.AspNetCore.Server.Kestrel", "Libuv"), - GetCompileLibrary("Libuv"), - }; - - var runtimeLibraries = new[] - { - expected, - GetRuntimeLibrary("Microsoft.AspNetCore.Server.Kestrel", "Libuv"), - GetRuntimeLibrary("Microsoft.AspNetCore.Mvc"), - }; - - var dependencyContext = GetDependencyContext(compileLibraries, runtimeLibraries); - - // Act - var candidates = ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext).ToList(); - - // Assert - Assert.Equal(new[] { expected }, candidates); - } - - [Fact] - public void GetCandidateLibraries_ReturnsLibrariesReferencingAnyMvcAssembly() - { - // Arrange - var libraries = new Dictionary - { - ["Foo"] = new[] { "Microsoft.AspNetCore.Mvc.Core" }, - ["Bar"] = new[] { "Microsoft.AspNetCore.Mvc" }, - ["Qux"] = new[] { "Not.Mvc.Assembly", "Unofficial.Microsoft.AspNetCore.Mvc" }, - ["Baz"] = new[] { "Microsoft.AspNetCore.Mvc.Abstractions" }, - ["Microsoft.AspNetCore.Mvc.Core"] = Array.Empty(), - ["Microsoft.AspNetCore.Mvc"] = Array.Empty(), - ["Not.Mvc.Assembly"] = Array.Empty(), - ["Unofficial.Microsoft.AspNetCore.Mvc"] = Array.Empty(), - ["Microsoft.AspNetCore.Mvc.Abstractions"] = Array.Empty(), - }; - var dependencyContext = GetDependencyContext(libraries); - - // Act - var candidates = ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext); - - // Assert - Assert.Equal(new[] { "Bar", "Baz", "Foo", }, candidates.Select(a => a.Name)); - } - - [Fact] - public void GetCandidateLibraries_LibraryNameComparisonsAreCaseInsensitive() - { - // Arrange - var libraries = new Dictionary - { - ["Foo"] = new[] { "MICROSOFT.ASPNETCORE.MVC.CORE" }, - ["Bar"] = new[] { "microsoft.aspnetcore.mvc" }, - ["Qux"] = new[] { "Not.Mvc.Assembly", "Unofficial.Microsoft.AspNetCore.Mvc" }, - ["Baz"] = new[] { "mIcRoSoFt.AsPnEtCoRe.MvC.aBsTrAcTiOnS" }, - ["Microsoft.AspNetCore.Mvc.Core"] = Array.Empty(), - ["LibraryA"] = new[] { "LIBRARYB" }, - ["LibraryB"] = new[] { "microsoft.aspnetcore.mvc" }, - ["Microsoft.AspNetCore.Mvc"] = Array.Empty(), - ["Not.Mvc.Assembly"] = Array.Empty(), - ["Unofficial.Microsoft.AspNetCore.Mvc"] = Array.Empty(), - ["Microsoft.AspNetCore.Mvc.Abstractions"] = Array.Empty(), - }; - var dependencyContext = GetDependencyContext(libraries); - - // Act - var candidates = ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext); - - // Assert - Assert.Equal(new[] { "Bar", "Baz", "Foo", "LibraryA", "LibraryB" }, candidates.Select(a => a.Name)); - } - - [Fact] - public void GetCandidateLibraries_ReturnsLibrariesWithTransitiveReferencesToAnyMvcAssembly() - { - // Arrange - var expectedLibraries = new[] { "Bar", "Baz", "Foo", "LibraryA", "LibraryB", "LibraryC", "LibraryE", "LibraryG", "LibraryH" }; - - var libraries = new Dictionary - { - ["Foo"] = new[] { "Bar" }, - ["Bar"] = new[] { "Microsoft.AspNetCore.Mvc" }, - ["Qux"] = new[] { "Not.Mvc.Assembly", "Unofficial.Microsoft.AspNetCore.Mvc" }, - ["Baz"] = new[] { "Microsoft.AspNetCore.Mvc.Abstractions" }, - ["Microsoft.AspNetCore.Mvc"] = Array.Empty(), - ["Not.Mvc.Assembly"] = Array.Empty(), - ["Microsoft.AspNetCore.Mvc.Abstractions"] = Array.Empty(), - ["Unofficial.Microsoft.AspNetCore.Mvc"] = Array.Empty(), - ["LibraryA"] = new[] { "LibraryB" }, - ["LibraryB"] = new[] { "LibraryC" }, - ["LibraryC"] = new[] { "LibraryD", "Microsoft.AspNetCore.Mvc.Abstractions" }, - ["LibraryD"] = Array.Empty(), - ["LibraryE"] = new[] { "LibraryF", "LibraryG" }, - ["LibraryF"] = Array.Empty(), - ["LibraryG"] = new[] { "LibraryH" }, - ["LibraryH"] = new[] { "LibraryI", "Microsoft.AspNetCore.Mvc" }, - ["LibraryI"] = Array.Empty(), - }; - var dependencyContext = GetDependencyContext(libraries); - - // Act - var candidates = ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext); - - // Assert - Assert.Equal(expectedLibraries, candidates.Select(a => a.Name)); - } - - [Fact] - public void GetCandidateLibraries_SkipsMvcAssemblies() - { - // Arrange - var libraries = new Dictionary - { - ["MvcSandbox"] = new[] { "Microsoft.AspNetCore.Mvc.Core", "Microsoft.AspNetCore.Mvc" }, - ["Microsoft.AspNetCore.Mvc.Core"] = new[] { "Microsoft.AspNetCore.HttpAbstractions" }, - ["Microsoft.AspNetCore.HttpAbstractions"] = Array.Empty(), - ["Microsoft.AspNetCore.Mvc"] = new[] { "Microsoft.AspNetCore.Mvc.Abstractions", "Microsoft.AspNetCore.Mvc.Core" }, - ["Microsoft.AspNetCore.Mvc.Abstractions"] = Array.Empty(), - ["Microsoft.AspNetCore.Mvc.TagHelpers"] = new[] { "Microsoft.AspNetCore.Mvc.Razor" }, - ["Microsoft.AspNetCore.Mvc.Razor"] = Array.Empty(), - ["ControllersAssembly"] = new[] { "Microsoft.AspNetCore.Mvc" }, - }; - - var dependencyContext = GetDependencyContext(libraries); - - // Act - var candidates = ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext); - - // Assert - Assert.Equal(new[] { "ControllersAssembly", "MvcSandbox" }, candidates.Select(a => a.Name)); - } - - private class TestApplicationAssembliesProvider : ApplicationAssembliesProvider - { - public DependencyContext DependencyContext { get; set; } - - public Func> GetRelatedAssembliesDelegate { get; set; } = (assembly) => Array.Empty(); - - protected override DependencyContext LoadDependencyContext(Assembly assembly) => DependencyContext; - - protected override IReadOnlyList GetRelatedAssemblies(Assembly assembly) => GetRelatedAssembliesDelegate(assembly); - - protected override IEnumerable GetLibraryAssemblies(DependencyContext dependencyContext, RuntimeLibrary runtimeLibrary) - { - var assemblyName = new AssemblyName(runtimeLibrary.Name); - yield return Assembly.Load(assemblyName); - } - } - - private static DependencyContext GetDependencyContext(IDictionary libraries) - { - var compileLibraries = new List(); - var runtimeLibraries = new List(); - - foreach (var kvp in libraries.OrderBy(kvp => kvp.Key, StringComparer.Ordinal)) - { - var compileLibrary = GetCompileLibrary(kvp.Key, kvp.Value); - compileLibraries.Add(compileLibrary); - - var runtimeLibrary = GetRuntimeLibrary(kvp.Key, kvp.Value); - runtimeLibraries.Add(runtimeLibrary); - } - - return GetDependencyContext(compileLibraries, runtimeLibraries); - } - - private static DependencyContext GetDependencyContext( - IReadOnlyList compileLibraries, - IReadOnlyList runtimeLibraries) - { - var dependencyContext = new DependencyContext( - new TargetInfo("framework", "runtime", "signature", isPortable: true), - CompilationOptions.Default, - compileLibraries, - runtimeLibraries, - Enumerable.Empty()); - return dependencyContext; - } - - private static RuntimeLibrary GetRuntimeLibrary(string name, params string[] dependencyNames) - { - var dependencies = dependencyNames?.Select(d => new Dependency(d, "42.0.0")) ?? new Dependency[0]; - - return new RuntimeLibrary( - "package", - name, - "23.0.0", - "hash", - new RuntimeAssetGroup[0], - new RuntimeAssetGroup[0], - new ResourceAssembly[0], - dependencies: dependencies.ToArray(), - serviceable: true); - } - - private static CompilationLibrary GetCompileLibrary(string name, params string[] dependencyNames) - { - var dependencies = dependencyNames?.Select(d => new Dependency(d, "42.0.0")) ?? new Dependency[0]; - - return new CompilationLibrary( - "package", - name, - "23.0.0", - "hash", - Enumerable.Empty(), - dependencies: dependencies.ToArray(), - serviceable: true); - } - - private class TestAssembly : Assembly - { - public override string FullName => "TestRelatedAssembly"; - - public override bool IsDefined(Type attributeType, bool inherit) - { - return attributeType == typeof(RelatedAssemblyAttribute); - } - } - } -} diff --git a/src/Mvc/Mvc.Core/test/ApplicationParts/AssemblyPartTest.cs b/src/Mvc/Mvc.Core/test/ApplicationParts/AssemblyPartTest.cs index b922a9eff0..8350e24243 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationParts/AssemblyPartTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationParts/AssemblyPartTest.cs @@ -50,55 +50,5 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts // Act & Assert Assert.Equal(part.Assembly, assembly); } - - [Fact] - public void GetReferencePaths_ReturnsReferencesFromDependencyContext_IfPreserveCompilationContextIsSet() - { - // Arrange - var assembly = GetType().GetTypeInfo().Assembly; - var part = new AssemblyPart(assembly); - - // Act - var references = part.GetReferencePaths().ToList(); - - // Assert - Assert.Contains(assembly.Location, references); - Assert.Contains( - typeof(AssemblyPart).GetTypeInfo().Assembly.GetName().Name, - references.Select(Path.GetFileNameWithoutExtension)); - } - - [Fact] - public void GetReferencePaths_ReturnsAssemblyLocation_IfPreserveCompilationContextIsNotSet() - { - // Arrange - // src projects do not have preserveCompilationContext specified. - var assembly = typeof(AssemblyPart).GetTypeInfo().Assembly; - var part = new AssemblyPart(assembly); - - // Act - var references = part.GetReferencePaths().ToList(); - - // Assert - var actual = Assert.Single(references); - Assert.Equal(assembly.Location, actual); - } - - [Fact] - public void GetReferencePaths_ReturnsEmptySequenceForDynamicAssembly() - { - // Arrange - var name = new AssemblyName($"DynamicAssembly-{Guid.NewGuid()}"); - var assembly = AssemblyBuilder.DefineDynamicAssembly(name, - AssemblyBuilderAccess.RunAndCollect); - - var part = new AssemblyPart(assembly); - - // Act - var references = part.GetReferencePaths().ToList(); - - // Assert - Assert.Empty(references); - } } } diff --git a/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj b/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj index b3f8e2c9a5..39df066d88 100644 --- a/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj +++ b/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj @@ -1,8 +1,7 @@ - + netcoreapp3.0 - true diff --git a/src/Mvc/Mvc.Formatters.Json/src/Properties/AssemblyInfo.cs b/src/Mvc/Mvc.Formatters.Json/src/Properties/AssemblyInfo.cs index 95a17d6462..039f419cd4 100644 --- a/src/Mvc/Mvc.Formatters.Json/src/Properties/AssemblyInfo.cs +++ b/src/Mvc/Mvc.Formatters.Json/src/Properties/AssemblyInfo.cs @@ -5,4 +5,3 @@ using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Mvc; [assembly: TypeForwardedTo(typeof(JsonResult))] - diff --git a/src/Mvc/Mvc.NewtonsoftJson/test/BsonTempDataSerializerTest.cs b/src/Mvc/Mvc.NewtonsoftJson/test/BsonTempDataSerializerTest.cs index a2438b1b8b..5bfe813d19 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/test/BsonTempDataSerializerTest.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/test/BsonTempDataSerializerTest.cs @@ -235,6 +235,33 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson Assert.Equal(value.ToString(), roundTripValue); } + [Fact] + public void RoundTripTest_ListOfDateTime() + { + // Arrange + var key = "test-key"; + var dateTime = new DateTime(2007, 1, 1); + var testProvider = GetTempDataSerializer(); + var value = new List + { + dateTime, + dateTime.AddDays(3), + }; + + var input = new Dictionary + { + { key, value } + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = Assert.IsType(values[key]); + Assert.Equal(value, roundTripValue); + } + private class TestItem { public int DummyInt { get; set; } diff --git a/src/Mvc/Mvc.Razor.RuntimeCompilation/ref/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj b/src/Mvc/Mvc.Razor.RuntimeCompilation/ref/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj index 6842a5c61a..0f66178b6d 100644 --- a/src/Mvc/Mvc.Razor.RuntimeCompilation/ref/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj +++ b/src/Mvc/Mvc.Razor.RuntimeCompilation/ref/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj @@ -9,5 +9,6 @@ + diff --git a/src/Mvc/Mvc.Razor.RuntimeCompilation/ref/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.netcoreapp3.0.cs b/src/Mvc/Mvc.Razor.RuntimeCompilation/ref/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.netcoreapp3.0.cs index c5368df39d..a22420cffc 100644 --- a/src/Mvc/Mvc.Razor.RuntimeCompilation/ref/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.netcoreapp3.0.cs +++ b/src/Mvc/Mvc.Razor.RuntimeCompilation/ref/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.netcoreapp3.0.cs @@ -1,6 +1,13 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + public static partial class AssemblyPartExtensions + { + public static System.Collections.Generic.IEnumerable GetReferencePaths(this Microsoft.AspNetCore.Mvc.ApplicationParts.AssemblyPart assemblyPart) { throw null; } + } +} namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation { public partial class FileProviderRazorProjectItem : Microsoft.AspNetCore.Razor.Language.RazorProjectItem diff --git a/src/Mvc/Mvc.Razor.RuntimeCompilation/src/AssemblyPartExtensions.cs b/src/Mvc/Mvc.Razor.RuntimeCompilation/src/AssemblyPartExtensions.cs new file mode 100644 index 0000000000..e3ceaa8817 --- /dev/null +++ b/src/Mvc/Mvc.Razor.RuntimeCompilation/src/AssemblyPartExtensions.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.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyModel; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + public static class AssemblyPartExtensions + { + /// + public static IEnumerable GetReferencePaths(this AssemblyPart assemblyPart) + { + var assembly = assemblyPart?.Assembly ?? throw new ArgumentNullException(nameof(assemblyPart)); + + if (assembly.IsDynamic) + { + // Skip loading process for dynamic assemblies. This prevents DependencyContextLoader from reading the + // .deps.json file from either manifest resources or the assembly location, which will fail. + return Enumerable.Empty(); + } + + var dependencyContext = DependencyContext.Load(assembly); + if (dependencyContext != null) + { + return dependencyContext.CompileLibraries.SelectMany(library => library.ResolveReferencePaths()); + } + + // If an application has been compiled without preserveCompilationContext, return the path to the assembly + // as a reference. For runtime compilation, this will allow the compilation to succeed as long as it least + // one application part has been compiled with preserveCompilationContext and contains a super set of types + // required for the compilation to succeed. + return new[] { assembly.Location }; + } + } +} diff --git a/src/Mvc/Mvc.Razor.RuntimeCompilation/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj b/src/Mvc/Mvc.Razor.RuntimeCompilation/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj index e0775f860b..b75277126a 100644 --- a/src/Mvc/Mvc.Razor.RuntimeCompilation/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj +++ b/src/Mvc/Mvc.Razor.RuntimeCompilation/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs b/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs index ce0094a911..b672dbaff6 100644 --- a/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs +++ b/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs @@ -1,7 +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; using System.Collections.Generic; using System.IO; using System.Linq; @@ -53,14 +52,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation // For unit testing internal IEnumerable GetReferencePaths() { - var referencesFromApplicationParts = _partManager - .ApplicationParts - .OfType() - .SelectMany(part => part.GetReferencePaths()); + var referencePaths = new List(); - var referencePaths = referencesFromApplicationParts - .Concat(_options.AdditionalReferencePaths) - .Distinct(StringComparer.OrdinalIgnoreCase); + foreach (var part in _partManager.ApplicationParts) + { + if (part is ICompilationReferencesProvider compilationReferenceProvider) + { + referencePaths.AddRange(compilationReferenceProvider.GetReferencePaths()); + } + else if (part is AssemblyPart assemblyPart) + { + referencePaths.AddRange(assemblyPart.GetReferencePaths()); + } + } + + referencePaths.AddRange(_options.AdditionalReferencePaths); return referencePaths; } diff --git a/src/Mvc/Mvc.Razor.RuntimeCompilation/test/AssemblyPartExtensionTest.cs b/src/Mvc/Mvc.Razor.RuntimeCompilation/test/AssemblyPartExtensionTest.cs new file mode 100644 index 0000000000..eb84d85728 --- /dev/null +++ b/src/Mvc/Mvc.Razor.RuntimeCompilation/test/AssemblyPartExtensionTest.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + public class AssemblyPartExtensionTest + { + [Fact] + public void GetReferencePaths_ReturnsReferencesFromDependencyContext_IfPreserveCompilationContextIsSet() + { + // Arrange + var assembly = GetType().GetTypeInfo().Assembly; + var part = new AssemblyPart(assembly); + + // Act + var references = part.GetReferencePaths().ToList(); + + // Assert + Assert.Contains(assembly.Location, references); + Assert.Contains( + typeof(AssemblyPart).GetTypeInfo().Assembly.GetName().Name, + references.Select(Path.GetFileNameWithoutExtension)); + } + + [Fact] + public void GetReferencePaths_ReturnsAssemblyLocation_IfPreserveCompilationContextIsNotSet() + { + // Arrange + // src projects do not have preserveCompilationContext specified. + var assembly = typeof(AssemblyPart).GetTypeInfo().Assembly; + var part = new AssemblyPart(assembly); + + // Act + var references = part.GetReferencePaths().ToList(); + + // Assert + var actual = Assert.Single(references); + Assert.Equal(assembly.Location, actual); + } + + [Fact] + public void GetReferencePaths_ReturnsEmptySequenceForDynamicAssembly() + { + // Arrange + var name = new AssemblyName($"DynamicAssembly-{Guid.NewGuid()}"); + var assembly = AssemblyBuilder.DefineDynamicAssembly(name, + AssemblyBuilderAccess.RunAndCollect); + + var part = new AssemblyPart(assembly); + + // Act + var references = part.GetReferencePaths().ToList(); + + // Assert + Assert.Empty(references); + } + } +} diff --git a/src/Mvc/Mvc.Testing/ref/Microsoft.AspNetCore.Mvc.Testing.csproj b/src/Mvc/Mvc.Testing/ref/Microsoft.AspNetCore.Mvc.Testing.csproj index 00d3840db3..b5e0c8a5df 100644 --- a/src/Mvc/Mvc.Testing/ref/Microsoft.AspNetCore.Mvc.Testing.csproj +++ b/src/Mvc/Mvc.Testing/ref/Microsoft.AspNetCore.Mvc.Testing.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Mvc/Mvc.Testing/src/Microsoft.AspNetCore.Mvc.Testing.csproj b/src/Mvc/Mvc.Testing/src/Microsoft.AspNetCore.Mvc.Testing.csproj index 03f5bf0d8c..649663aaf7 100644 --- a/src/Mvc/Mvc.Testing/src/Microsoft.AspNetCore.Mvc.Testing.csproj +++ b/src/Mvc/Mvc.Testing/src/Microsoft.AspNetCore.Mvc.Testing.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/DefaultTempDataSerializer.cs b/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/DefaultTempDataSerializer.cs index 27b30c9dc6..a67670e595 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/DefaultTempDataSerializer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/DefaultTempDataSerializer.cs @@ -63,6 +63,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure deserializedValue = null; break; + case JsonValueType.Array: + deserializedValue = DeserializeArray(item.Value); + break; + + case JsonValueType.Object: + deserializedValue = DeserializeDictionaryEntry(item.Value); + break; + default: throw new InvalidOperationException(Resources.FormatTempData_CannotDeserializeType(item.Value.Type)); } @@ -73,6 +81,53 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure return deserialized; } + private static object DeserializeArray(in JsonElement arrayElement) + { + if (arrayElement.GetArrayLength() == 0) + { + // We have to infer the type of the array by inspecting it's elements. + // If there's nothing to inspect, return a null value since we do not know + // what type the user code is expecting. + return null; + } + + if (arrayElement[0].Type == JsonValueType.String) + { + var array = new List(); + + foreach (var item in arrayElement.EnumerateArray()) + { + array.Add(item.GetString()); + } + + return array.ToArray(); + } + else if (arrayElement[0].Type == JsonValueType.Number) + { + var array = new List(); + + foreach (var item in arrayElement.EnumerateArray()) + { + array.Add(item.GetInt32()); + } + + return array.ToArray(); + } + + throw new InvalidOperationException(Resources.FormatTempData_CannotDeserializeType(arrayElement.Type)); + } + + private static object DeserializeDictionaryEntry(in JsonElement objectElement) + { + var dictionary = new Dictionary(StringComparer.Ordinal); + foreach (var item in objectElement.EnumerateObject()) + { + dictionary[item.Name] = item.Value.GetString(); + } + + return dictionary; + } + public override byte[] Serialize(IDictionary values) { if (values == null || values.Count == 0) @@ -126,6 +181,33 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure case Guid guid: writer.WriteString(key, guid); break; + + case ICollection intCollection: + writer.WriteStartArray(key); + foreach (var element in intCollection) + { + writer.WriteNumberValue(element); + } + writer.WriteEndArray(); + break; + + case ICollection stringCollection: + writer.WriteStartArray(key); + foreach (var element in stringCollection) + { + writer.WriteStringValue(element); + } + writer.WriteEndArray(); + break; + + case IDictionary dictionary: + writer.WriteStartObject(key); + foreach (var element in dictionary) + { + writer.WriteString(element.Key, element.Value); + } + writer.WriteEndObject(); + break; } } writer.WriteEndObject(); @@ -150,7 +232,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure type == typeof(string) || type == typeof(bool) || type == typeof(DateTime) || - type == typeof(Guid); + type == typeof(Guid) || + typeof(ICollection).IsAssignableFrom(type) || + typeof(ICollection).IsAssignableFrom(type) || + typeof(IDictionary).IsAssignableFrom(type); } } } diff --git a/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/DefaultTempDataSerializerTest.cs b/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/DefaultTempDataSerializerTest.cs index 982e9fc9f1..22f2185fd1 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/DefaultTempDataSerializerTest.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/DefaultTempDataSerializerTest.cs @@ -34,5 +34,26 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure var roundTripValue = Assert.IsType(values[key]); Assert.Equal(value.ToString("r"), roundTripValue); } + + [Fact] + public override void RoundTripTest_DictionaryOfInt() + { + // Arrange + var key = "test-key"; + var testProvider = GetTempDataSerializer(); + var value = new Dictionary + { + { "Key1", 7 }, + { "Key2", 24 }, + }; + var input = new Dictionary + { + { key, value } + }; + + // Act + var ex = Assert.Throws(() => testProvider.Serialize(input)); + Assert.Equal($"The '{testProvider.GetType()}' cannot serialize an object of type '{value.GetType()}'.", ex.Message); + } } } diff --git a/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/TempDataSerializerTestBase.cs b/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/TempDataSerializerTestBase.cs index 38a540f8e7..93fb4df0e2 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/TempDataSerializerTestBase.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/TempDataSerializerTestBase.cs @@ -239,6 +239,119 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure Assert.Equal(value, roundTripValue); } + [Fact] + public void RoundTripTest_CollectionOfInts() + { + // Arrange + var key = "test-key"; + var testProvider = GetTempDataSerializer(); + var value = new[] { 1, 2, 4, 3 }; + var input = new Dictionary + { + { key, value } + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = Assert.IsType(values[key]); + Assert.Equal(value, roundTripValue); + } + + [Fact] + public void RoundTripTest_ArrayOfStringss() + { + // Arrange + var key = "test-key"; + var testProvider = GetTempDataSerializer(); + var value = new[] { "Hello", "world" }; + var input = new Dictionary + { + { key, value } + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = Assert.IsType(values[key]); + Assert.Equal(value, roundTripValue); + } + + [Fact] + public void RoundTripTest_ListOfStringss() + { + // Arrange + var key = "test-key"; + var testProvider = GetTempDataSerializer(); + var value = new List { "Hello", "world" }; + var input = new Dictionary + { + { key, value } + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = Assert.IsType(values[key]); + Assert.Equal(value, roundTripValue); + } + + [Fact] + public void RoundTripTest_DictionaryOfString() + { + // Arrange + var key = "test-key"; + var testProvider = GetTempDataSerializer(); + var value = new Dictionary + { + { "Key1", "Value1" }, + { "Key2", "Value2" }, + }; + var input = new Dictionary + { + { key, value } + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = Assert.IsType>(values[key]); + Assert.Equal(value, roundTripValue); + } + + [Fact] + public virtual void RoundTripTest_DictionaryOfInt() + { + // Arrange + var key = "test-key"; + var testProvider = GetTempDataSerializer(); + var value = new Dictionary + { + { "Key1", 7 }, + { "Key2", 24 }, + }; + var input = new Dictionary + { + { key, value } + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = Assert.IsType>(values[key]); + Assert.Equal(value, roundTripValue); + } + protected abstract TempDataSerializer GetTempDataSerializer(); } } diff --git a/src/Mvc/Mvc/test/ApplicationParts/ApplicationAssembliesProviderTest.cs b/src/Mvc/Mvc/test/ApplicationParts/ApplicationAssembliesProviderTest.cs deleted file mode 100644 index 03d4ef3c73..0000000000 --- a/src/Mvc/Mvc/test/ApplicationParts/ApplicationAssembliesProviderTest.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Linq; -using System.Reflection; -using Microsoft.Extensions.DependencyModel; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.ApplicationParts -{ - public class ApplicationAssembliesProviderTest - { - private static readonly Assembly ThisAssembly = typeof(ApplicationAssembliesProviderTest).Assembly; - - // This test verifies ApplicationAssembliesProviderTest.ReferenceAssemblies reflects the actual loadable assemblies - // of the libraries that Microsoft.AspNetCore.Mvc depends on. - // If we add or remove dependencies, this test should be changed together. - [Fact] - public void ReferenceAssemblies_ReturnsLoadableReferenceAssemblies() - { - // Arrange - var excludeAssemblies = new string[] - { - "Microsoft.AspNetCore.Mvc.Analyzers", - "Microsoft.AspNetCore.Mvc.Test", - "Microsoft.AspNetCore.Mvc.Core.TestCommon", - }; - - var additionalAssemblies = new[] - { - // The following assemblies are not reachable from Microsoft.AspNetCore.Mvc - "Microsoft.AspNetCore.App", - "Microsoft.AspNetCore.Mvc.NewtonsoftJson", - "Microsoft.AspNetCore.Mvc.Formatters.Xml", - }; - - var dependencyContextLibraries = DependencyContext.Load(ThisAssembly) - .CompileLibraries - .Where(r => r.Name.StartsWith("Microsoft.AspNetCore.Mvc", StringComparison.OrdinalIgnoreCase) && - !excludeAssemblies.Contains(r.Name, StringComparer.OrdinalIgnoreCase)) - .Select(r => r.Name); - - var expected = dependencyContextLibraries - .Concat(additionalAssemblies) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(p => p, StringComparer.OrdinalIgnoreCase); - - // Act - var referenceAssemblies = ApplicationAssembliesProvider - .ReferenceAssemblies - .OrderBy(p => p, StringComparer.OrdinalIgnoreCase); - - // Assert - Assert.Equal(expected, referenceAssemblies, StringComparer.OrdinalIgnoreCase); - } - } -} diff --git a/src/Mvc/MvcNoDeps.slnf b/src/Mvc/MvcNoDeps.slnf new file mode 100644 index 0000000000..f67bdb6d53 --- /dev/null +++ b/src/Mvc/MvcNoDeps.slnf @@ -0,0 +1,73 @@ +{ + "solution": { + "path": "Mvc.sln", + "projects": [ + "test\\WebSites\\BasicWebSite\\BasicWebSite.csproj", + "test\\WebSites\\RoutingWebSite\\Mvc.RoutingWebSite.csproj", + "test\\WebSites\\RazorWebSite\\RazorWebSite.csproj", + "test\\WebSites\\FormatterWebSite\\FormatterWebSite.csproj", + "test\\WebSites\\ApiExplorerWebSite\\ApiExplorerWebSite.csproj", + "test\\WebSites\\VersioningWebSite\\VersioningWebSite.csproj", + "test\\WebSites\\TagHelpersWebSite\\TagHelpersWebSite.csproj", + "test\\WebSites\\FilesWebSite\\FilesWebSite.csproj", + "test\\WebSites\\ApplicationModelWebSite\\ApplicationModelWebSite.csproj", + "test\\WebSites\\HtmlGenerationWebSite\\HtmlGenerationWebSite.csproj", + "test\\WebSites\\ErrorPageMiddlewareWebSite\\ErrorPageMiddlewareWebSite.csproj", + "test\\WebSites\\XmlFormattersWebSite\\XmlFormattersWebSite.csproj", + "test\\WebSites\\ControllersFromServicesWebSite\\ControllersFromServicesWebSite.csproj", + "test\\WebSites\\ControllersFromServicesClassLibrary\\ControllersFromServicesClassLibrary.csproj", + "test\\WebSites\\CorsWebSite\\CorsWebSite.csproj", + "samples\\MvcSandbox\\MvcSandbox.csproj", + "test\\WebSites\\SimpleWebSite\\SimpleWebSite.csproj", + "test\\WebSites\\SecurityWebSite\\SecurityWebSite.csproj", + "test\\WebSites\\RazorPagesWebSite\\RazorPagesWebSite.csproj", + "benchmarks\\Microsoft.AspNetCore.Mvc.Performance\\Microsoft.AspNetCore.Mvc.Performance.csproj", + "test\\WebSites\\RazorBuildWebSite\\RazorBuildWebSite.csproj", + "test\\WebSites\\RazorBuildWebSite.Views\\RazorBuildWebSite.Views.csproj", + "Mvc.Analyzers\\src\\Microsoft.AspNetCore.Mvc.Analyzers.csproj", + "Mvc.Analyzers\\test\\Mvc.Analyzers.Test.csproj", + "test\\WebSites\\RazorPagesClassLibrary\\RazorPagesClassLibrary.csproj", + "shared\\Mvc.Views.TestCommon\\Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj", + "Mvc.Api.Analyzers\\test\\Mvc.Api.Analyzers.Test.csproj", + "Mvc.Api.Analyzers\\src\\Microsoft.AspNetCore.Mvc.Api.Analyzers.csproj", + "test\\WebSites\\GenericHostWebSite\\GenericHostWebSite.csproj", + "Mvc\\src\\Microsoft.AspNetCore.Mvc.csproj", + "Mvc\\test\\Microsoft.AspNetCore.Mvc.Test.csproj", + "Mvc.Abstractions\\src\\Microsoft.AspNetCore.Mvc.Abstractions.csproj", + "Mvc.Abstractions\\test\\Microsoft.AspNetCore.Mvc.Abstractions.Test.csproj", + "Mvc.ApiExplorer\\src\\Microsoft.AspNetCore.Mvc.ApiExplorer.csproj", + "Mvc.ApiExplorer\\test\\Microsoft.AspNetCore.Mvc.ApiExplorer.Test.csproj", + "Mvc.Core\\src\\Microsoft.AspNetCore.Mvc.Core.csproj", + "Mvc.Core\\test\\Microsoft.AspNetCore.Mvc.Core.Test.csproj", + "Mvc.Cors\\src\\Microsoft.AspNetCore.Mvc.Cors.csproj", + "Mvc.Cors\\test\\Microsoft.AspNetCore.Mvc.Cors.Test.csproj", + "Mvc.DataAnnotations\\src\\Microsoft.AspNetCore.Mvc.DataAnnotations.csproj", + "Mvc.DataAnnotations\\test\\Microsoft.AspNetCore.Mvc.DataAnnotations.Test.csproj", + "Mvc.Formatters.Json\\src\\Microsoft.AspNetCore.Mvc.Formatters.Json.csproj", + "Mvc.Formatters.Xml\\src\\Microsoft.AspNetCore.Mvc.Formatters.Xml.csproj", + "Mvc.Formatters.Xml\\test\\Microsoft.AspNetCore.Mvc.Formatters.Xml.Test.csproj", + "Mvc.Localization\\src\\Microsoft.AspNetCore.Mvc.Localization.csproj", + "Mvc.Localization\\test\\Microsoft.AspNetCore.Mvc.Localization.Test.csproj", + "Mvc.Razor\\src\\Microsoft.AspNetCore.Mvc.Razor.csproj", + "Mvc.Razor\\test\\Microsoft.AspNetCore.Mvc.Razor.Test.csproj", + "Mvc.RazorPages\\src\\Microsoft.AspNetCore.Mvc.RazorPages.csproj", + "Mvc.RazorPages\\test\\Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj", + "Mvc.TagHelpers\\src\\Microsoft.AspNetCore.Mvc.TagHelpers.csproj", + "Mvc.TagHelpers\\test\\Microsoft.AspNetCore.Mvc.TagHelpers.Test.csproj", + "Mvc.ViewFeatures\\src\\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj", + "Mvc.ViewFeatures\\test\\Microsoft.AspNetCore.Mvc.ViewFeatures.Test.csproj", + "test\\Mvc.FunctionalTests\\Microsoft.AspNetCore.Mvc.FunctionalTests.csproj", + "test\\Mvc.IntegrationTests\\Microsoft.AspNetCore.Mvc.IntegrationTests.csproj", + "shared\\Mvc.TestDiagnosticListener\\Microsoft.AspNetCore.Mvc.TestDiagnosticListener.csproj", + "Mvc.Testing\\src\\Microsoft.AspNetCore.Mvc.Testing.csproj", + "shared\\Mvc.Core.TestCommon\\Microsoft.AspNetCore.Mvc.Core.TestCommon.csproj", + "Mvc.NewtonsoftJson\\src\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.csproj", + "Mvc.NewtonsoftJson\\test\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj", + "Mvc.Razor.RuntimeCompilation\\src\\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj", + "Mvc.Razor.RuntimeCompilation\\test\\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test.csproj", + "test\\WebSites\\RazorBuildWebSite.PrecompiledViews\\RazorBuildWebSite.PrecompiledViews.csproj", + "Mvc.Components.Prerendering\\src\\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj", + "Mvc.Components.Prerendering\\test\\Microsoft.AspNetCore.Mvc.Components.Prerendering.Test.csproj", + ] + } +} diff --git a/src/Mvc/mvc.slnf b/src/Mvc/mvc.slnf new file mode 100644 index 0000000000..79d4fb7df0 --- /dev/null +++ b/src/Mvc/mvc.slnf @@ -0,0 +1,73 @@ +{ + "solution": { + "path": "Mvc.sln", + "projects": [ + "test\\WebSites\\BasicWebSite\\BasicWebSite.csproj", + "test\\WebSites\\RoutingWebSite\\Mvc.RoutingWebSite.csproj", + "test\\WebSites\\RazorWebSite\\RazorWebSite.csproj", + "test\\WebSites\\FormatterWebSite\\FormatterWebSite.csproj", + "test\\WebSites\\ApiExplorerWebSite\\ApiExplorerWebSite.csproj", + "test\\WebSites\\VersioningWebSite\\VersioningWebSite.csproj", + "test\\WebSites\\TagHelpersWebSite\\TagHelpersWebSite.csproj", + "test\\WebSites\\FilesWebSite\\FilesWebSite.csproj", + "test\\WebSites\\ApplicationModelWebSite\\ApplicationModelWebSite.csproj", + "test\\WebSites\\HtmlGenerationWebSite\\HtmlGenerationWebSite.csproj", + "test\\WebSites\\ErrorPageMiddlewareWebSite\\ErrorPageMiddlewareWebSite.csproj", + "test\\WebSites\\XmlFormattersWebSite\\XmlFormattersWebSite.csproj", + "test\\WebSites\\ControllersFromServicesWebSite\\ControllersFromServicesWebSite.csproj", + "test\\WebSites\\ControllersFromServicesClassLibrary\\ControllersFromServicesClassLibrary.csproj", + "test\\WebSites\\CorsWebSite\\CorsWebSite.csproj", + "samples\\MvcSandbox\\MvcSandbox.csproj", + "test\\WebSites\\SimpleWebSite\\SimpleWebSite.csproj", + "test\\WebSites\\SecurityWebSite\\SecurityWebSite.csproj", + "test\\WebSites\\RazorPagesWebSite\\RazorPagesWebSite.csproj", + "benchmarks\\Microsoft.AspNetCore.Mvc.Performance\\Microsoft.AspNetCore.Mvc.Performance.csproj", + "test\\WebSites\\RazorBuildWebSite\\RazorBuildWebSite.csproj", + "test\\WebSites\\RazorBuildWebSite.Views\\RazorBuildWebSite.Views.csproj", + "Mvc.Analyzers\\src\\Microsoft.AspNetCore.Mvc.Analyzers.csproj", + "Mvc.Analyzers\\test\\Mvc.Analyzers.Test.csproj", + "test\\WebSites\\RazorPagesClassLibrary\\RazorPagesClassLibrary.csproj", + "shared\\Mvc.Views.TestCommon\\Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj", + "Mvc.Api.Analyzers\\test\\Mvc.Api.Analyzers.Test.csproj", + "Mvc.Api.Analyzers\\src\\Microsoft.AspNetCore.Mvc.Api.Analyzers.csproj", + "test\\WebSites\\GenericHostWebSite\\GenericHostWebSite.csproj", + "Mvc\\src\\Microsoft.AspNetCore.Mvc.csproj", + "Mvc\\test\\Microsoft.AspNetCore.Mvc.Test.csproj", + "Mvc.Abstractions\\src\\Microsoft.AspNetCore.Mvc.Abstractions.csproj", + "Mvc.Abstractions\\test\\Microsoft.AspNetCore.Mvc.Abstractions.Test.csproj", + "Mvc.ApiExplorer\\src\\Microsoft.AspNetCore.Mvc.ApiExplorer.csproj", + "Mvc.ApiExplorer\\test\\Microsoft.AspNetCore.Mvc.ApiExplorer.Test.csproj", + "Mvc.Core\\src\\Microsoft.AspNetCore.Mvc.Core.csproj", + "Mvc.Core\\test\\Microsoft.AspNetCore.Mvc.Core.Test.csproj", + "Mvc.Cors\\src\\Microsoft.AspNetCore.Mvc.Cors.csproj", + "Mvc.Cors\\test\\Microsoft.AspNetCore.Mvc.Cors.Test.csproj", + "Mvc.DataAnnotations\\src\\Microsoft.AspNetCore.Mvc.DataAnnotations.csproj", + "Mvc.DataAnnotations\\test\\Microsoft.AspNetCore.Mvc.DataAnnotations.Test.csproj", + "Mvc.Formatters.Json\\src\\Microsoft.AspNetCore.Mvc.Formatters.Json.csproj", + "Mvc.Formatters.Xml\\src\\Microsoft.AspNetCore.Mvc.Formatters.Xml.csproj", + "Mvc.Formatters.Xml\\test\\Microsoft.AspNetCore.Mvc.Formatters.Xml.Test.csproj", + "Mvc.Localization\\src\\Microsoft.AspNetCore.Mvc.Localization.csproj", + "Mvc.Localization\\test\\Microsoft.AspNetCore.Mvc.Localization.Test.csproj", + "Mvc.Razor\\src\\Microsoft.AspNetCore.Mvc.Razor.csproj", + "Mvc.Razor\\test\\Microsoft.AspNetCore.Mvc.Razor.Test.csproj", + "Mvc.RazorPages\\src\\Microsoft.AspNetCore.Mvc.RazorPages.csproj", + "Mvc.RazorPages\\test\\Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj", + "Mvc.TagHelpers\\src\\Microsoft.AspNetCore.Mvc.TagHelpers.csproj", + "Mvc.TagHelpers\\test\\Microsoft.AspNetCore.Mvc.TagHelpers.Test.csproj", + "Mvc.ViewFeatures\\src\\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj", + "Mvc.ViewFeatures\\test\\Microsoft.AspNetCore.Mvc.ViewFeatures.Test.csproj", + "test\\Mvc.FunctionalTests\\Microsoft.AspNetCore.Mvc.FunctionalTests.csproj", + "test\\Mvc.IntegrationTests\\Microsoft.AspNetCore.Mvc.IntegrationTests.csproj", + "shared\\Mvc.TestDiagnosticListener\\Microsoft.AspNetCore.Mvc.TestDiagnosticListener.csproj", + "Mvc.Testing\\src\\Microsoft.AspNetCore.Mvc.Testing.csproj", + "shared\\Mvc.Core.TestCommon\\Microsoft.AspNetCore.Mvc.Core.TestCommon.csproj", + "Mvc.NewtonsoftJson\\src\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.csproj", + "Mvc.NewtonsoftJson\\test\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj", + "Mvc.Razor.RuntimeCompilation\\src\\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj", + "Mvc.Razor.RuntimeCompilation\\test\\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test.csproj", + "test\\WebSites\\RazorBuildWebSite.PrecompiledViews\\RazorBuildWebSite.PrecompiledViews.csproj", + "Mvc.Components.Prerendering\\src\\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj", + "Mvc.Components.Prerendering\\test\\Microsoft.AspNetCore.Mvc.Components.Prerendering.Test.csproj" + ] + } +} \ No newline at end of file diff --git a/src/Mvc/test/Mvc.FunctionalTests/Infrastructure/ResourceFile.cs b/src/Mvc/test/Mvc.FunctionalTests/Infrastructure/ResourceFile.cs index 97c51a0d6e..013847e683 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/Infrastructure/ResourceFile.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/Infrastructure/ResourceFile.cs @@ -191,7 +191,11 @@ namespace Microsoft.AspNetCore.Mvc { // The build system compiles every file under the resources folder as a resource available at runtime // with the same name as the file name. Need to update this file on disc. + +// https://github.com/aspnet/AspNetCore/issues/10423 +#pragma warning disable 0618 var solutionPath = TestPathUtilities.GetSolutionRootDirectory("Mvc"); +#pragma warning restore 0618 var projectPath = Path.Combine(solutionPath, "test", assembly.GetName().Name); var fullPath = Path.Combine(projectPath, resourceName); WriteFile(fullPath, content); diff --git a/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj b/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj index 2a8c7e0504..51388a1c39 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj +++ b/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj @@ -9,7 +9,7 @@ Mvc.FunctionalTests false - + false diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/Properties/AssemblyInfo.cs b/src/Mvc/test/WebSites/RazorPagesWebSite/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..bd7ade2f84 --- /dev/null +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// 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.Mvc.ApplicationParts; + +// This will eventually be codegened, but it's fine for duplicates to be present +[assembly: ApplicationPart("RazorPagesClassLibrary")] \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Pages/_Host.cshtml b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Pages/_Host.cshtml index fc23391b1d..66a7aa0628 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Pages/_Host.cshtml +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Pages/_Host.cshtml @@ -3,7 +3,7 @@ @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers - + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_Layout.cshtml b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_Layout.cshtml index 0706f9911b..5506d943d3 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_Layout.cshtml +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Pages/Shared/_Layout.cshtml @@ -1,5 +1,5 @@  - + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/css/site.css b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/css/site.css index e60e0d5caf..e679a8ea7f 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/css/site.css +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/css/site.css @@ -12,6 +12,18 @@ a { color: #0366d6; } +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + /* Sticky footer styles -------------------------------------------------- */ html { diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml index fb908db5ec..b370df12dd 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml @@ -1,5 +1,5 @@  - + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/css/site.css b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/css/site.css index e60e0d5caf..e679a8ea7f 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/css/site.css +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/wwwroot/css/site.css @@ -12,6 +12,18 @@ a { color: #0366d6; } +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + /* Sticky footer styles -------------------------------------------------- */ html { diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/css/site.css b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/css/site.css index 631a0eba10..e679a8ea7f 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/css/site.css +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/css/site.css @@ -7,10 +7,23 @@ a.navbar-brand { word-break: break-all; } +/* Provide sufficient contrast against white background */ a { color: #0366d6; } +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + /* Sticky footer styles -------------------------------------------------- */ html { diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/styles.css b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/styles.css index 90d4ee0072..3ebae31331 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/styles.css +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/styles.css @@ -1 +1,16 @@ /* You can add global styles to this file, and also import other style files */ + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +code { + color: #E01A76; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/App.js b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/App.js index b55d0317fe..f0bcaf3200 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/App.js +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/App.js @@ -10,6 +10,8 @@ import ApiAuthorizationRoutes from './components/api-authorization/ApiAuthorizat import { ApplicationPaths } from './components/api-authorization/ApiAuthorizationConstants'; ////#endif +import './custom.css' + export default class App extends Component { static displayName = App.name; diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/custom.css b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/custom.css new file mode 100644 index 0000000000..5fdfd061c3 --- /dev/null +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/custom.css @@ -0,0 +1,14 @@ +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +code { + color: #E01A76; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} diff --git a/src/ProjectTemplates/scripts/Test-Template.ps1 b/src/ProjectTemplates/scripts/Test-Template.ps1 index 36bdefa1da..af199993f8 100644 --- a/src/ProjectTemplates/scripts/Test-Template.ps1 +++ b/src/ProjectTemplates/scripts/Test-Template.ps1 @@ -7,7 +7,13 @@ $ErrorActionPreference = 'Stop' function Test-Template($templateName, $templateArgs, $templateNupkg, $isSPA) { $tmpDir = "$PSScriptRoot/$templateName" Remove-Item -Path $tmpDir -Recurse -ErrorAction Ignore - dotnet pack + Push-Location .. + try { + dotnet pack + } + finally { + Pop-Location + } Run-DotnetNew "--install", "$PSScriptRoot/../../../artifacts/packages/Debug/Shipping/$templateNupkg" diff --git a/src/ProjectTemplates/test/template-baselines.json b/src/ProjectTemplates/test/template-baselines.json index 25c524a32d..24b5f0f88a 100644 --- a/src/ProjectTemplates/test/template-baselines.json +++ b/src/ProjectTemplates/test/template-baselines.json @@ -1005,13 +1005,13 @@ "ClientApp/public/favicon.ico", "ClientApp/public/index.html", "ClientApp/public/manifest.json", - "ClientApp/scss/custom.scss", "ClientApp/src/components/Counter.js", "ClientApp/src/components/FetchData.js", "ClientApp/src/components/Home.js", "ClientApp/src/components/Layout.js", "ClientApp/src/components/NavMenu.css", "ClientApp/src/components/NavMenu.js", + "ClientApp/src/custom.css", "ClientApp/src/App.js", "ClientApp/src/App.test.js", "ClientApp/src/index.js", diff --git a/src/Security/Authentication/Negotiate/ref/Microsoft.AspNetCore.Authentication.Negotiate.csproj b/src/Security/Authentication/Negotiate/ref/Microsoft.AspNetCore.Authentication.Negotiate.csproj new file mode 100644 index 0000000000..ea9c20cd85 --- /dev/null +++ b/src/Security/Authentication/Negotiate/ref/Microsoft.AspNetCore.Authentication.Negotiate.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.0 + + + + + + + diff --git a/src/Security/Authentication/Negotiate/ref/Microsoft.AspNetCore.Authentication.Negotiate.netcoreapp3.0.cs b/src/Security/Authentication/Negotiate/ref/Microsoft.AspNetCore.Authentication.Negotiate.netcoreapp3.0.cs new file mode 100644 index 0000000000..4f9f3a900e --- /dev/null +++ b/src/Security/Authentication/Negotiate/ref/Microsoft.AspNetCore.Authentication.Negotiate.netcoreapp3.0.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + public partial class AuthenticatedContext : Microsoft.AspNetCore.Authentication.ResultContext + { + public AuthenticatedContext(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Authentication.AuthenticationScheme scheme, Microsoft.AspNetCore.Authentication.Negotiate.NegotiateOptions options) : base (default(Microsoft.AspNetCore.Http.HttpContext), default(Microsoft.AspNetCore.Authentication.AuthenticationScheme), default(Microsoft.AspNetCore.Authentication.Negotiate.NegotiateOptions)) { } + } + public partial class AuthenticationFailedContext : Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext + { + public AuthenticationFailedContext(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Authentication.AuthenticationScheme scheme, Microsoft.AspNetCore.Authentication.Negotiate.NegotiateOptions options) : base (default(Microsoft.AspNetCore.Http.HttpContext), default(Microsoft.AspNetCore.Authentication.AuthenticationScheme), default(Microsoft.AspNetCore.Authentication.Negotiate.NegotiateOptions), default(Microsoft.AspNetCore.Authentication.AuthenticationProperties)) { } + public System.Exception Exception { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + } + public partial class ChallengeContext : Microsoft.AspNetCore.Authentication.PropertiesContext + { + public ChallengeContext(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Authentication.AuthenticationScheme scheme, Microsoft.AspNetCore.Authentication.Negotiate.NegotiateOptions options, Microsoft.AspNetCore.Authentication.AuthenticationProperties properties) : base (default(Microsoft.AspNetCore.Http.HttpContext), default(Microsoft.AspNetCore.Authentication.AuthenticationScheme), default(Microsoft.AspNetCore.Authentication.Negotiate.NegotiateOptions), default(Microsoft.AspNetCore.Authentication.AuthenticationProperties)) { } + public bool Handled { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public void HandleResponse() { } + } + public static partial class NegotiateDefaults + { + public const string AuthenticationScheme = "Negotiate"; + } + public partial class NegotiateEvents + { + public NegotiateEvents() { } + public System.Func OnAuthenticated { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public System.Func OnAuthenticationFailed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public System.Func OnChallenge { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public virtual System.Threading.Tasks.Task Authenticated(Microsoft.AspNetCore.Authentication.Negotiate.AuthenticatedContext context) { throw null; } + public virtual System.Threading.Tasks.Task AuthenticationFailed(Microsoft.AspNetCore.Authentication.Negotiate.AuthenticationFailedContext context) { throw null; } + public virtual System.Threading.Tasks.Task Challenge(Microsoft.AspNetCore.Authentication.Negotiate.ChallengeContext context) { throw null; } + } + public partial class NegotiateHandler : Microsoft.AspNetCore.Authentication.AuthenticationHandler, Microsoft.AspNetCore.Authentication.IAuthenticationHandler, Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler + { + public NegotiateHandler(Microsoft.Extensions.Options.IOptionsMonitor options, Microsoft.Extensions.Logging.ILoggerFactory logger, System.Text.Encodings.Web.UrlEncoder encoder, Microsoft.AspNetCore.Authentication.ISystemClock clock) : base (default(Microsoft.Extensions.Options.IOptionsMonitor), default(Microsoft.Extensions.Logging.ILoggerFactory), default(System.Text.Encodings.Web.UrlEncoder), default(Microsoft.AspNetCore.Authentication.ISystemClock)) { } + protected new Microsoft.AspNetCore.Authentication.Negotiate.NegotiateEvents Events { get { throw null; } set { } } + protected override System.Threading.Tasks.Task CreateEventsAsync() { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + protected override System.Threading.Tasks.Task HandleAuthenticateAsync() { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + protected override System.Threading.Tasks.Task HandleChallengeAsync(Microsoft.AspNetCore.Authentication.AuthenticationProperties properties) { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + public System.Threading.Tasks.Task HandleRequestAsync() { throw null; } + } + public partial class NegotiateOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions + { + public NegotiateOptions() { } + public new Microsoft.AspNetCore.Authentication.Negotiate.NegotiateEvents Events { get { throw null; } set { } } + public bool PersistKerberosCredentials { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public bool PersistNtlmCredentials { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + } +} +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class NegotiateExtensions + { + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddNegotiate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder) { throw null; } + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddNegotiate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, System.Action configureOptions) { throw null; } + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddNegotiate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, System.Action configureOptions) { throw null; } + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddNegotiate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, string displayName, System.Action configureOptions) { throw null; } + } +} diff --git a/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/NegotiateAuthSample.csproj b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/NegotiateAuthSample.csproj new file mode 100644 index 0000000000..225b146856 --- /dev/null +++ b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/NegotiateAuthSample.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.0 + OutOfProcess + + + + + + + + + + diff --git a/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Program.cs b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Program.cs new file mode 100644 index 0000000000..05f8132cb2 --- /dev/null +++ b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Program.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace NegotiateAuthSample +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Properties/launchSettings.json b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Properties/launchSettings.json new file mode 100644 index 0000000000..128e6417fa --- /dev/null +++ b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:6449", + "sslPort": 44369 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "NegotiateAuthSample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} diff --git a/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Startup.cs b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Startup.cs new file mode 100644 index 0000000000..1643abe614 --- /dev/null +++ b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Startup.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Negotiate; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace NegotiateAuthSample +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthorization(options => + { + options.FallbackPolicy = options.DefaultPolicy; + }); + services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) + .AddNegotiate(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + app.UseAuthentication(); + app.UseAuthorization(); + app.Run(HandleRequest); + } + + public async Task HandleRequest(HttpContext context) + { + var user = context.User.Identity; + await context.Response.WriteAsync($"Authenticated? {user.IsAuthenticated}, Name: {user.Name}, Protocol: {context.Request.Protocol}"); + } + } +} diff --git a/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/appsettings.Development.json b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/appsettings.Development.json new file mode 100644 index 0000000000..6d1c8438d2 --- /dev/null +++ b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "Microsoft.AspNetCore.Authentication": "Debug" + } + } +} diff --git a/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/appsettings.json b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/appsettings.json new file mode 100644 index 0000000000..7cb5ac8193 --- /dev/null +++ b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Security/Authentication/Negotiate/src/Events/AuthenticatedContext.cs b/src/Security/Authentication/Negotiate/src/Events/AuthenticatedContext.cs new file mode 100644 index 0000000000..fee54f3e8d --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/Events/AuthenticatedContext.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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + /// + /// State for the Authenticated event. + /// + public class AuthenticatedContext : ResultContext + { + /// + /// Creates a new . + /// + /// + /// + /// + public AuthenticatedContext( + HttpContext context, + AuthenticationScheme scheme, + NegotiateOptions options) + : base(context, scheme, options) { } + } +} diff --git a/src/Security/Authentication/Negotiate/src/Events/AuthenticationFailedContext.cs b/src/Security/Authentication/Negotiate/src/Events/AuthenticationFailedContext.cs new file mode 100644 index 0000000000..d64c8da43e --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/Events/AuthenticationFailedContext.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 System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + /// + /// State for the AuthenticationFailed event. + /// + public class AuthenticationFailedContext : RemoteAuthenticationContext + { + /// + /// Creates a . + /// + /// + /// + /// + public AuthenticationFailedContext( + HttpContext context, + AuthenticationScheme scheme, + NegotiateOptions options) + : base(context, scheme, options, properties: null) { } + + /// + /// The exception that occured while processing the authentication. + /// + public Exception Exception { get; set; } + } +} diff --git a/src/Security/Authentication/Negotiate/src/Events/ChallengeContext.cs b/src/Security/Authentication/Negotiate/src/Events/ChallengeContext.cs new file mode 100644 index 0000000000..23b6ab041d --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/Events/ChallengeContext.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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + /// + /// State for the Challenge event. + /// + public class ChallengeContext : PropertiesContext + { + /// + /// Creates a new . + /// + /// + /// + /// + /// + public ChallengeContext( + HttpContext context, + AuthenticationScheme scheme, + NegotiateOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) { } + + /// + /// If true, will skip any default logic for this challenge. + /// + public bool Handled { get; private set; } + + /// + /// Skips any default logic for this challenge. + /// + public void HandleResponse() => Handled = true; + } +} diff --git a/src/Security/Authentication/Negotiate/src/Events/NegotiateEvents.cs b/src/Security/Authentication/Negotiate/src/Events/NegotiateEvents.cs new file mode 100644 index 0000000000..0d57be28eb --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/Events/NegotiateEvents.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + /// + /// Specifies events which the invokes to enable developer control over the authentication process. + /// + public class NegotiateEvents + { + /// + /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. + /// + public Func OnAuthenticationFailed { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked after the authentication is complete and a ClaimsIdentity has been generated. + /// + public Func OnAuthenticated { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked before a challenge is sent back to the caller. + /// + public Func OnChallenge { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. + /// + public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context); + + /// + /// Invoked after the authentication is complete and a ClaimsIdentity has been generated. + /// + public virtual Task Authenticated(AuthenticatedContext context) => OnAuthenticated(context); + + /// + /// Invoked before a challenge is sent back to the caller. + /// + public virtual Task Challenge(ChallengeContext context) => OnChallenge(context); + } +} diff --git a/src/Security/Authentication/Negotiate/src/Internal/INegotiateState.cs b/src/Security/Authentication/Negotiate/src/Internal/INegotiateState.cs new file mode 100644 index 0000000000..dbc215ef7d --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/Internal/INegotiateState.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Principal; + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + // For testing + internal interface INegotiateState : IDisposable + { + string GetOutgoingBlob(string incomingBlob); + + bool IsCompleted { get; } + + string Protocol { get; } + + IIdentity GetIdentity(); + } +} diff --git a/src/Security/Authentication/Negotiate/src/Internal/INegotiateStateFactory.cs b/src/Security/Authentication/Negotiate/src/Internal/INegotiateStateFactory.cs new file mode 100644 index 0000000000..32cdeef1e9 --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/Internal/INegotiateStateFactory.cs @@ -0,0 +1,11 @@ +// 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.Authentication.Negotiate +{ + // For testing + internal interface INegotiateStateFactory + { + INegotiateState CreateInstance(); + } +} diff --git a/src/Security/Authentication/Negotiate/src/Internal/NegotiateLoggingExtensions.cs b/src/Security/Authentication/Negotiate/src/Internal/NegotiateLoggingExtensions.cs new file mode 100644 index 0000000000..399b6fbe52 --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/Internal/NegotiateLoggingExtensions.cs @@ -0,0 +1,71 @@ +// 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.Extensions.Logging +{ + internal static class NegotiateLoggingExtensions + { + private static Action _incompleteNegotiateChallenge; + private static Action _negotiateComplete; + private static Action _enablingCredentialPersistence; + private static Action _disablingCredentialPersistence; + private static Action _exceptionProcessingAuth; + private static Action _challengeNegotiate; + private static Action _reauthenticating; + + static NegotiateLoggingExtensions() + { + _incompleteNegotiateChallenge = LoggerMessage.Define( + eventId: new EventId(1, "IncompleteNegotiateChallenge"), + logLevel: LogLevel.Information, + formatString: "Incomplete Negotiate handshake, sending an additional 401 Negotiate challenge."); + _negotiateComplete = LoggerMessage.Define( + eventId: new EventId(2, "NegotiateComplete"), + logLevel: LogLevel.Debug, + formatString: "Completed Negotiate authentication."); + _enablingCredentialPersistence = LoggerMessage.Define( + eventId: new EventId(3, "EnablingCredentialPersistence"), + logLevel: LogLevel.Debug, + formatString: "Enabling credential persistence for a complete Kerberos handshake."); + _disablingCredentialPersistence = LoggerMessage.Define( + eventId: new EventId(4, "DisablingCredentialPersistence"), + logLevel: LogLevel.Debug, + formatString: "Disabling credential persistence for a complete {protocol} handshake."); + _exceptionProcessingAuth = LoggerMessage.Define( + eventId: new EventId(5, "ExceptionProcessingAuth"), + logLevel: LogLevel.Error, + formatString: "An exception occurred while processing the authentication request."); + _challengeNegotiate = LoggerMessage.Define( + eventId: new EventId(6, "ChallengeNegotiate"), + logLevel: LogLevel.Debug, + formatString: "Challenged 401 Negotiate"); + _reauthenticating = LoggerMessage.Define( + eventId: new EventId(7, "Reauthenticating"), + logLevel: LogLevel.Debug, + formatString: "Negotiate data received for an already authenticated connection, Re-authenticating."); + } + + public static void IncompleteNegotiateChallenge(this ILogger logger) + => _incompleteNegotiateChallenge(logger, null); + + public static void NegotiateComplete(this ILogger logger) + => _negotiateComplete(logger, null); + + public static void EnablingCredentialPersistence(this ILogger logger) + => _enablingCredentialPersistence(logger, null); + + public static void DisablingCredentialPersistence(this ILogger logger, string protocol) + => _disablingCredentialPersistence(logger, protocol, null); + + public static void ExceptionProcessingAuth(this ILogger logger, Exception ex) + => _exceptionProcessingAuth(logger, ex); + + public static void ChallengeNegotiate(this ILogger logger) + => _challengeNegotiate(logger, null); + + public static void Reauthenticating(this ILogger logger) + => _reauthenticating(logger, null); + } +} diff --git a/src/Security/Authentication/Negotiate/src/Internal/ReflectedNegotiateState.cs b/src/Security/Authentication/Negotiate/src/Internal/ReflectedNegotiateState.cs new file mode 100644 index 0000000000..37a1dff6f0 --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/Internal/ReflectedNegotiateState.cs @@ -0,0 +1,96 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Security.Authentication; +using System.Security.Principal; + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + internal class ReflectedNegotiateState : INegotiateState + { + private static readonly ConstructorInfo _constructor; + private static readonly MethodInfo _getOutgoingBlob; + private static readonly MethodInfo _isCompleted; + private static readonly MethodInfo _protocol; + private static readonly MethodInfo _getIdentity; + private static readonly MethodInfo _closeContext; + + private readonly object _instance; + + static ReflectedNegotiateState() + { + var ntAuthType = typeof(AuthenticationException).Assembly.GetType("System.Net.NTAuthentication"); + _constructor = ntAuthType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).First(); + _getOutgoingBlob = ntAuthType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(info => + info.Name.Equals("GetOutgoingBlob") && info.GetParameters().Count() == 2).Single(); + _isCompleted = ntAuthType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(info => + info.Name.Equals("get_IsCompleted")).Single(); + _protocol = ntAuthType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(info => + info.Name.Equals("get_ProtocolName")).Single(); + _closeContext = ntAuthType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(info => + info.Name.Equals("CloseContext")).Single(); + + var negoStreamPalType = typeof(AuthenticationException).Assembly.GetType("System.Net.Security.NegotiateStreamPal"); + _getIdentity = negoStreamPalType.GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(info => + info.Name.Equals("GetIdentity")).Single(); + } + + public ReflectedNegotiateState() + { + // internal NTAuthentication(bool isServer, string package, NetworkCredential credential, string spn, ContextFlagsPal requestedContextFlags, ChannelBinding channelBinding) + var credential = CredentialCache.DefaultCredentials; + _instance = _constructor.Invoke(new object[] { true, "Negotiate", credential, null, 0, null }); + } + + // Copied rather than reflected to remove the IsCompleted -> CloseContext check. + // The client doesn't need the context once auth is complete, but the server does. + // I'm not sure why it auto-closes for the client given that the client closes it just a few lines later. + // https://github.com/dotnet/corefx/blob/a3ab91e10045bb298f48c1d1f9bd5b0782a8ac46/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs#L134 + public string GetOutgoingBlob(string incomingBlob) + { + byte[] decodedIncomingBlob = null; + if (incomingBlob != null && incomingBlob.Length > 0) + { + decodedIncomingBlob = Convert.FromBase64String(incomingBlob); + } + byte[] decodedOutgoingBlob = GetOutgoingBlob(decodedIncomingBlob, true); + + string outgoingBlob = null; + if (decodedOutgoingBlob != null && decodedOutgoingBlob.Length > 0) + { + outgoingBlob = Convert.ToBase64String(decodedOutgoingBlob); + } + + return outgoingBlob; + } + + private byte[] GetOutgoingBlob(byte[] incomingBlob, bool thrownOnError) + { + return (byte[])_getOutgoingBlob.Invoke(_instance, new object[] { incomingBlob, thrownOnError }); + } + + public bool IsCompleted + { + get => (bool)_isCompleted.Invoke(_instance, Array.Empty()); + } + + public string Protocol + { + get => (string)_protocol.Invoke(_instance, Array.Empty()); + } + + public IIdentity GetIdentity() + { + return (IIdentity)_getIdentity.Invoke(obj: null, parameters: new object[] { _instance }); + } + + public void Dispose() + { + _closeContext.Invoke(_instance, Array.Empty()); + } + } +} diff --git a/src/Security/Authentication/Negotiate/src/Internal/ReflectedNegotiateStateFactory.cs b/src/Security/Authentication/Negotiate/src/Internal/ReflectedNegotiateStateFactory.cs new file mode 100644 index 0000000000..31fa29d49b --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/Internal/ReflectedNegotiateStateFactory.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + internal class ReflectedNegotiateStateFactory : INegotiateStateFactory + { + public INegotiateState CreateInstance() + { + return new ReflectedNegotiateState(); + } + } +} diff --git a/src/Security/Authentication/Negotiate/src/Microsoft.AspNetCore.Authentication.Negotiate.csproj b/src/Security/Authentication/Negotiate/src/Microsoft.AspNetCore.Authentication.Negotiate.csproj new file mode 100644 index 0000000000..f71af581a9 --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/Microsoft.AspNetCore.Authentication.Negotiate.csproj @@ -0,0 +1,16 @@ + + + + ASP.NET Core authentication handler used to authenticate requests using Negotiate, Kerberos, or NTLM. + netcoreapp3.0 + true + aspnetcore;authentication;security + true + + + + + + + + diff --git a/src/Security/Authentication/Negotiate/src/NegotiateDefaults.cs b/src/Security/Authentication/Negotiate/src/NegotiateDefaults.cs new file mode 100644 index 0000000000..1c119ea871 --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/NegotiateDefaults.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.Authentication.Negotiate +{ + /// + /// Default values used by Negotiate authentication. + /// + public static class NegotiateDefaults + { + /// + /// Default value for AuthenticationScheme used to identify the Negotiate auth handler. + /// + public const string AuthenticationScheme = "Negotiate"; + } +} diff --git a/src/Security/Authentication/Negotiate/src/NegotiateExtensions.cs b/src/Security/Authentication/Negotiate/src/NegotiateExtensions.cs new file mode 100644 index 0000000000..ea01f040a8 --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/NegotiateExtensions.cs @@ -0,0 +1,62 @@ +// 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.Authentication; +using Microsoft.AspNetCore.Authentication.Negotiate; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extensions for enabling Negotiate authentication. + /// + public static class NegotiateExtensions + { + /// + /// Adds Negotiate authentication. + /// + /// The . + /// The original builder. + public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder) + => builder.AddNegotiate(NegotiateDefaults.AuthenticationScheme, _ => { }); + + /// + /// Adds and configures Negotiate authentication. + /// + /// The . + /// Allows for configuring the authentication handler. + /// The original builder. + public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddNegotiate(NegotiateDefaults.AuthenticationScheme, configureOptions); + + /// + /// Adds and configures Negotiate authentication. + /// + /// The . + /// The scheme name used to identify the authentication handler internally. + /// Allows for configuring the authentication handler. + /// The original builder. + public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddNegotiate(authenticationScheme, displayName: null, configureOptions: configureOptions); + + /// + /// Adds and configures Negotiate authentication. + /// + /// The . + /// The scheme name used to identify the authentication handler internally. + /// The name displayed to users when selecting an authentication handler. The default is null to prevent this from displaying. + /// Allows for configuring the authentication handler. + /// The original builder. + public static AuthenticationBuilder AddNegotiate(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable($"ASPNETCORE_TOKEN"))) + { + throw new NotSupportedException( + "The Negotiate authentication handler must not be used with IIS out-of-process mode or similar reverse proxies that share connections between users." + + " Use the Windows Authentication features available within IIS or IIS Express."); + } + + return builder.AddScheme(authenticationScheme, displayName, configureOptions); + } + } +} diff --git a/src/Security/Authentication/Negotiate/src/NegotiateHandler.cs b/src/Security/Authentication/Negotiate/src/NegotiateHandler.cs new file mode 100644 index 0000000000..27844382fb --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/NegotiateHandler.cs @@ -0,0 +1,340 @@ +// 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.Diagnostics; +using System.Linq; +using System.Security.Claims; +using System.Security.Principal; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + /// + /// Authenticates requests using Negotiate, Kerberos, or NTLM. + /// + public class NegotiateHandler : AuthenticationHandler, IAuthenticationRequestHandler + { + private const string AuthPersistenceKey = nameof(AuthPersistence); + private const string NegotiateVerb = "Negotiate"; + private const string AuthHeaderPrefix = NegotiateVerb + " "; + + private bool _requestProcessed; + private INegotiateState _negotiateState; + + /// + /// Creates a new + /// + /// + /// + /// + /// + public NegotiateHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { } + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected new NegotiateEvents Events + { + get => (NegotiateEvents)base.Events; + set => base.Events = value; + } + + /// + /// Creates the default events type. + /// + /// + protected override Task CreateEventsAsync() => Task.FromResult(new NegotiateEvents()); + + private bool IsHttp2 => string.Equals("HTTP/2", Request.Protocol, StringComparison.OrdinalIgnoreCase); + + /// + /// Intercepts incomplete Negotiate authentication handshakes and continues or completes them. + /// + /// True if a response was generated, false otherwise. + public async Task HandleRequestAsync() + { + try + { + if (_requestProcessed) + { + // This request was already processed but something is re-executing it like an exception handler. + // Don't re-run because we could corrupt the connection state, e.g. if this was a stage2 NTLM request + // that we've already completed the handshake for. + return false; + } + + _requestProcessed = true; + + if (IsHttp2) + { + // HTTP/2 is not supported. Do not throw because this may be running on a server that supports + // both HTTP/1 and HTTP/2. + return false; + } + + var connectionItems = GetConnectionItems(); + var persistence = (AuthPersistence)connectionItems[AuthPersistenceKey]; + _negotiateState = persistence?.State; + + var authorizationHeader = Request.Headers[HeaderNames.Authorization]; + + if (StringValues.IsNullOrEmpty(authorizationHeader)) + { + if (_negotiateState?.IsCompleted == false) + { + throw new InvalidOperationException("An anonymous request was received in between authentication handshake requests."); + } + return false; + } + + var authorization = authorizationHeader.ToString(); + string token = null; + if (authorization.StartsWith(AuthHeaderPrefix, StringComparison.OrdinalIgnoreCase)) + { + token = authorization.Substring(AuthHeaderPrefix.Length).Trim(); + } + else + { + if (_negotiateState?.IsCompleted == false) + { + throw new InvalidOperationException("Non-negotiate request was received in between authentication handshake requests."); + } + return false; + } + + // WinHttpHandler re-authenticates an existing connection if it gets another challenge on subsequent requests. + if (_negotiateState?.IsCompleted == true) + { + Logger.Reauthenticating(); + _negotiateState.Dispose(); + _negotiateState = null; + persistence.State = null; + } + + _negotiateState ??= Options.StateFactory.CreateInstance(); + + var outgoing = _negotiateState.GetOutgoingBlob(token); + + if (!_negotiateState.IsCompleted) + { + persistence ??= EstablishConnectionPersistence(connectionItems); + // Save the state long enough to complete the multi-stage handshake. + // We'll remove it once complete if !PersistNtlm/KerberosCredentials. + persistence.State = _negotiateState; + + Logger.IncompleteNegotiateChallenge(); + Response.StatusCode = StatusCodes.Status401Unauthorized; + Response.Headers.Append(HeaderNames.WWWAuthenticate, AuthHeaderPrefix + outgoing); + return true; + } + + Logger.NegotiateComplete(); + + // There can be a final blob of data we need to send to the client, but let the request execute as normal. + if (!string.IsNullOrEmpty(outgoing)) + { + Response.OnStarting(() => + { + // Only include it if the response ultimately succeeds. This avoids adding it twice if Challenge is called again. + if (Response.StatusCode < StatusCodes.Status400BadRequest) + { + Response.Headers.Append(HeaderNames.WWWAuthenticate, AuthHeaderPrefix + outgoing); + } + return Task.CompletedTask; + }); + } + + // Deal with connection credential persistence. + + if (_negotiateState.Protocol == "NTLM" && !Options.PersistNtlmCredentials) + { + // NTLM was already put in the persitence cache on the prior request so we could complete the handshake. + // Take it out if we don't want it to persist. + Debug.Assert(object.ReferenceEquals(persistence?.State, _negotiateState), + "NTLM is a two stage process, it must have already been in the cache for the handshake to succeed."); + Logger.DisablingCredentialPersistence(_negotiateState.Protocol); + persistence.State = null; + Response.RegisterForDispose(_negotiateState); + } + else if (_negotiateState.Protocol == "Kerberos") + { + // Kerberos can require one or two stage handshakes + if (Options.PersistKerberosCredentials) + { + Logger.EnablingCredentialPersistence(); + persistence ??= EstablishConnectionPersistence(connectionItems); + persistence.State = _negotiateState; + } + else + { + if (persistence?.State != null) + { + Logger.DisablingCredentialPersistence(_negotiateState.Protocol); + persistence.State = null; + } + Response.RegisterForDispose(_negotiateState); + } + } + + // Note we run the Authenticated event in HandleAuthenticateAsync so it is per-request rather than per connection. + } + catch (Exception ex) + { + Logger.ExceptionProcessingAuth(ex); + var errorContext = new AuthenticationFailedContext(Context, Scheme, Options) { Exception = ex }; + await Events.AuthenticationFailed(errorContext); + + if (errorContext.Result != null) + { + if (errorContext.Result.Handled) + { + return true; + } + else if (errorContext.Result.Skipped) + { + return false; + } + else if (errorContext.Result.Failure != null) + { + throw new Exception("An error was returned from the AuthenticationFailed event.", errorContext.Result.Failure); + } + } + + throw; + } + + return false; + } + + /// + /// Checks if the current request is authenticated and returns the user. + /// + /// + protected override async Task HandleAuthenticateAsync() + { + if (!_requestProcessed) + { + throw new InvalidOperationException("AuthenticateAsync must not be called before the UseAuthentication middleware runs."); + } + + if (IsHttp2) + { + // Not supported. We don't throw because Negotiate may be set as the default auth + // handler on a server that's running HTTP/1 and HTTP/2. We'll challenge HTTP/2 requests + // that require auth and they'll downgrade to HTTP/1.1. + return AuthenticateResult.NoResult(); + } + + if (_negotiateState == null) + { + return AuthenticateResult.NoResult(); + } + + if (!_negotiateState.IsCompleted) + { + // This case should have been rejected by HandleRequestAsync + throw new InvalidOperationException("Attempting to use an incomplete authentication context."); + } + + // Make a new copy of the user for each request, they are mutable objects and + // things like ClaimsTransformation run per request. + var identity = _negotiateState.GetIdentity(); + ClaimsPrincipal user; + if (identity is WindowsIdentity winIdentity) + { + user = new WindowsPrincipal(winIdentity); + Response.RegisterForDispose(winIdentity); + } + else + { + user = new ClaimsPrincipal(new ClaimsIdentity(identity)); + } + + var authenticatedContext = new AuthenticatedContext(Context, Scheme, Options) + { + Principal = user + }; + await Events.Authenticated(authenticatedContext); + + if (authenticatedContext.Result != null) + { + return authenticatedContext.Result; + } + + var ticket = new AuthenticationTicket(authenticatedContext.Principal, authenticatedContext.Properties, Scheme.Name); + return AuthenticateResult.Success(ticket); + } + + /// + /// Issues a 401 WWW-Authenticate Negotiate challenge. + /// + /// + /// + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { + // We allow issuing a challenge from an HTTP/2 request. Browser clients will gracefully downgrade to HTTP/1.1. + // SocketHttpHandler will not downgrade (https://github.com/dotnet/corefx/issues/35195), but WinHttpHandler will. + var eventContext = new ChallengeContext(Context, Scheme, Options, properties); + await Events.Challenge(eventContext); + if (eventContext.Handled) + { + return; + } + + Response.StatusCode = StatusCodes.Status401Unauthorized; + Response.Headers.Append(HeaderNames.WWWAuthenticate, NegotiateVerb); + Logger.ChallengeNegotiate(); + } + + private AuthPersistence EstablishConnectionPersistence(IDictionary items) + { + Debug.Assert(!items.ContainsKey(AuthPersistenceKey), "This should only be registered once per connection"); + var persistence = new AuthPersistence(); + RegisterForConnectionDispose(persistence); + items[AuthPersistenceKey] = persistence; + return persistence; + } + + private IDictionary GetConnectionItems() + { + return Context.Features.Get()?.Items + ?? throw new NotSupportedException($"Negotiate authentication requires a server that supports {nameof(IConnectionItemsFeature)} like Kestrel."); + } + + private void RegisterForConnectionDispose(IDisposable authState) + { + var connectionCompleteFeature = Context.Features.Get() + ??throw new NotSupportedException($"Negotiate authentication requires a server that supports {nameof(IConnectionCompleteFeature)} like Kestrel."); + connectionCompleteFeature.OnCompleted(DisposeState, authState); + } + + private static Task DisposeState(object state) + { + ((IDisposable)state).Dispose(); + return Task.CompletedTask; + } + + // This allows us to have one disposal registration per connection and limits churn on the Items collection. + private class AuthPersistence : IDisposable + { + internal INegotiateState State { get; set; } + + public void Dispose() + { + State?.Dispose(); + } + } + } +} diff --git a/src/Security/Authentication/Negotiate/src/NegotiateOptions.cs b/src/Security/Authentication/Negotiate/src/NegotiateOptions.cs new file mode 100644 index 0000000000..78df485897 --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/NegotiateOptions.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. + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + /// + /// Options class provides information needed to control Negotiate Authentication handler behavior + /// + public class NegotiateOptions : AuthenticationSchemeOptions + { + /// + /// The object provided by the application to process events raised by the negotiate authentication handler. + /// The application may use the existing NegotiateEvents instance and assign delegates only to the events it + /// wants to process. The application may also replace it with its own derived instance. + /// + public new NegotiateEvents Events + { + get { return (NegotiateEvents)base.Events; } + set { base.Events = value; } + } + + /// + /// Indicates if Kerberos credentials should be persisted and re-used for subsquent anonymous requests. + /// This option must not be used if connections may be shared by requests from different users. + /// The default is false. + /// + public bool PersistKerberosCredentials { get; set; } = false; + + /// + /// Indicates if NTLM credentials should be persisted and re-used for subsquent anonymous requests. + /// This option must not be used if connections may be shared by requests from different users. + /// The default is true. + /// + public bool PersistNtlmCredentials { get; set; } = true; + + // For testing + internal INegotiateStateFactory StateFactory { get; set; } = new ReflectedNegotiateStateFactory(); + } +} diff --git a/src/Security/Authentication/Negotiate/src/Properties/AssemblyInfo.cs b/src/Security/Authentication/Negotiate/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..626e2fd7ed --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/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.Authentication.Negotiate.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/CrossMachineReadMe.md b/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/CrossMachineReadMe.md new file mode 100644 index 0000000000..e263a2c5f7 --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/CrossMachineReadMe.md @@ -0,0 +1,48 @@ +Cross Machine Tests + +Kerberos can only be tested in a multi-machine environment. On localhost it always falls back to NTLM which has different requirements. Multi-machine is also neccisary for interop testing across OSs. Kerberos also requires domain controler SPN configuration so we can't test it on arbitrary test boxes. + +Test structure: +- A remote test server with various endpoints with different authentication restrictions. +- A remote test client with endpoints that execute specific scenarios. The input for these endpoints is theory data. The output is either 200Ok, or a failure code and desciption. +- The CrossMachineTest class that drives the tests. It invokes the client app with the theory data and confirms the results. + +We use these three components beceause it allows us to run the tests from a dev machine or CI agent that is not part of the dedicated test domain/environment. + +(Static) Environment Setup: +- Warning, this environment can take a day to set up. That's why we want a static test environment that we can re-use. +- Create a Windows server running DNS and Active Directory. Promote it to a domain controller. + - Create an SPN on this machine for Windows -> Windows testing. `setspn -S "http/chrross-dc.crkerberos.com" -U administrator` + - Future: Can we replace the domain controller with an AAD instance? We'd still want a second windows machine for Windows -> Windows testing, but AAD might be easier to configure. + - https://docs.microsoft.com/en-us/azure/active-directory-domain-services/active-directory-ds-getting-started + - https://docs.microsoft.com/en-us/azure/active-directory-domain-services/active-directory-ds-join-ubuntu-linux-vm + - https://docs.microsoft.com/en-us/azure/active-directory-domain-services/active-directory-ds-enable-kcd +- Create another Windows machine and join it to the test domain. +- Create a Linux machine and joing it to the domain. Ubuntu 18.04 has been used in the past. + - https://www.safesquid.com/content-filtering/integrating-linux-host-windows-ad-kerberos-sso-authentication + - Include an HTTP SPN + +Test deployment variations, prioritized: +- Windows -> Linux +- Windows -> Windows +- Localhost Windows -> Windows +Future: +- Note the Linux HttpClient doesn't support default credentials, you have to update Negotiate.Client to provide explicit credentials. +- Linux -> Windows +- Linux -> Linux +- Localhost Linux -> Linux + +Test run setup: +- Publish Negotiate.Client as a standalone application targeting the OS you want to run it on. Copy it to that machine and run it. + - Make sure it's running on a public IP and that the port is not blocked by the firewall. + - Note we primarily care about having the server on the latest runtime. Publishing the client the same way is convenient but not required. We do want to update it periodically for interop testing. + - HTTPS is optional for this client. +- Publish Negotiate.Server as a standalone application targeting the OS you want to run it on. Copy it to that machine and run it. + - Make sure it's running on a public IP and that the port is not blocked by the firewall. + - Note the server app starts two server instances, one with connection persistence enabled and the other with it disabled. + - HTTPS is needed on the server for some HTTP/2 downgrade tests. These tests can be ignored if HTTPS is not conveniently available. + - Future: Automate remote publishing +- In CrossMachineTests: + - Set ClientAddress, ServerPersistAddress, and ServerNonPersistAddress. (Future: Pull from environment variables?) + - UnSkip the test cases. (Future: Make these conditional on the test environment being available, environment variables?) +- Run tests! diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/CrossMachineTests.cs b/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/CrossMachineTests.cs new file mode 100644 index 0000000000..0bfed42c03 --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/CrossMachineTests.cs @@ -0,0 +1,150 @@ +// 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + // The OS's being tested are on other machines, don't duplicate the tests across runs. + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)] + // See CrossMachineReadMe.md + public class CrossMachineTests + { + private const string Http11Version = "HTTP/1.1"; + private const string Http2Version = "HTTP/2"; + + private const string ClientAddress = + // "http://chrross-udesk:5004"; + "https://localhost:5005"; + private const string ServerName = + "chrross-dc"; + // "chrross-udesk"; + private static readonly string ServerPersistAddress = $"http://{ServerName}.CRKerberos.com:5000"; + private static readonly string ServerNonPersistAddress = $"http://{ServerName}.CRKerberos.com:5002"; + + public static IEnumerable Http11And2 => + new List + { + new object[] { Http11Version }, + new object[] { Http2Version }, + }; + + [ConditionalTheory(Skip = "Manual testing only")] + [MemberData(nameof(Http11And2))] + public Task Anonymous_NoChallenge_NoOps(string protocol) + { + return RunTest(protocol, "/Anonymous/Unrestricted"); + } + + [ConditionalTheory(Skip = "Manual testing only")] + [MemberData(nameof(Http11And2))] + public Task Anonymous_Challenge_401Negotiate(string protocol) + { + return RunTest(protocol, "/Anonymous/Authorized"); + } + + [ConditionalTheory(Skip = "Manual testing only")] + [MemberData(nameof(Http11And2))] + public Task DefautCredentials_Success(string protocol) + { + return RunTest(protocol, "/DefaultCredentials/Authorized"); + } + + public static IEnumerable HttpOrders => + new List + { + new object[] { Http11Version, Http11Version }, + new object[] { Http11Version, Http2Version }, + new object[] { Http2Version, Http11Version }, + }; + + [ConditionalTheory(Skip = "Manual testing only")] + [MemberData(nameof(HttpOrders))] + // AuthorizedRequestAfterAuth_ReUses1WithPersistence would give the same results + public Task UrestrictedRequestAfterAuth_ReUses1WithPersistence(string protocol1, string protocol2) + { + return RunTest(ServerPersistAddress, protocol1, protocol2, "/AfterAuth/Unrestricted/Persist"); + } + + [ConditionalTheory(Skip = "Manual testing only")] + [MemberData(nameof(HttpOrders))] + public Task UrestrictedRequestAfterAuth_AnonymousWhenNotPersisted(string protocol1, string protocol2) + { + return RunTest(ServerNonPersistAddress, protocol1, protocol2, "/AfterAuth/Unrestricted/NonPersist"); + } + + [ConditionalTheory(Skip = "Manual testing only")] + [MemberData(nameof(HttpOrders))] + public Task AuthorizedRequestAfterAuth_ReauthenticatesWhenNotPersisted(string protocol1, string protocol2) + { + return RunTest(ServerNonPersistAddress, protocol1, protocol2, "/AfterAuth/Authorized/NonPersist"); + } + + [ConditionalTheory(Skip = "Manual testing only")] + [MemberData(nameof(Http11And2))] + public Task Unauthorized_401Negotiate(string protocol) + { + return RunTest(protocol, "/Unauthorized"); + } + + [ConditionalTheory(Skip = "Manual testing only")] + [MemberData(nameof(Http11And2))] + public Task UnauthorizedAfterAuthenticated_Success(string protocol) + { + return RunTest(protocol, "/AfterAuth/Unauthorized", persist: true); + } + + private Task RunTest(string protocol, string path, bool persist = false) + { + var queryBuilder = new QueryBuilder + { + { "server", persist ? ServerPersistAddress : ServerNonPersistAddress }, + { "protocol", protocol } + }; + + return RunTest(path, queryBuilder); + } + + private Task RunTest(string server, string protocol1, string protocol2, string path) + { + var queryBuilder = new QueryBuilder + { + { "server", server }, + { "protocol1", protocol1 }, + { "protocol2", protocol2 } + }; + + return RunTest(path, queryBuilder); + } + + private async Task RunTest(string path, QueryBuilder queryBuilder) + { + using var client = CreateClient(ClientAddress); + + var response = await client.GetAsync("/authtest" + path + queryBuilder.ToString()); + var body = await response.Content.ReadAsStringAsync(); + + Assert.True(HttpStatusCode.OK == response.StatusCode, $"{response.StatusCode}: {body}"); + } + + private static HttpClient CreateClient(string address) + { + return new HttpClient(new HttpClientHandler() + { + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, + }) + { + BaseAddress = new Uri(address), + DefaultRequestVersion = new Version(2, 0), + }; + } + } +} diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/Microsoft.AspNetCore.Authentication.Negotiate.FunctionalTest.csproj b/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/Microsoft.AspNetCore.Authentication.Negotiate.FunctionalTest.csproj new file mode 100644 index 0000000000..24c387964f --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/Microsoft.AspNetCore.Authentication.Negotiate.FunctionalTest.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp3.0 + true + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/NegotiateHandlerFunctionalTests.cs b/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/NegotiateHandlerFunctionalTests.cs new file mode 100644 index 0000000000..742bb426b0 --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/NegotiateHandlerFunctionalTests.cs @@ -0,0 +1,307 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + // In theory this would work on Linux and Mac, but the client would require explicit credentials. + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] + public class NegotiateHandlerFunctionalTests + { + private static readonly Version Http11Version = new Version(1, 1); + private static readonly Version Http2Version = new Version(2, 0); + + public static IEnumerable Http11And2 => + new List + { + new object[] { Http11Version }, + new object[] { Http2Version }, + }; + + [ConditionalTheory] + [MemberData(nameof(Http11And2))] + public async Task Anonymous_NoChallenge_NoOps(Version version) + { + using var host = await CreateHostAsync(); + using var client = CreateSocketHttpClient(host); + client.DefaultRequestVersion = version; + + var result = await client.GetAsync("/Anonymous" + version.Major); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.False(result.Headers.Contains(HeaderNames.WWWAuthenticate)); + Assert.Equal(version, result.Version); + } + + [ConditionalTheory] + [MemberData(nameof(Http11And2))] + public async Task Anonymous_Challenge_401Negotiate(Version version) + { + using var host = await CreateHostAsync(); + // WinHttpHandler can't disable default credentials on localhost, use SocketHttpHandler. + using var client = CreateSocketHttpClient(host); + client.DefaultRequestVersion = version; + + var result = await client.GetAsync("/Authenticate"); + Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); + Assert.Equal("Negotiate", result.Headers.WwwAuthenticate.ToString()); + Assert.Equal(version, result.Version); + } + + [ConditionalTheory] + [MemberData(nameof(Http11And2))] + public async Task DefautCredentials_Success(Version version) + { + using var host = await CreateHostAsync(); + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade HTTP/2. WinHttpHandler does. + using var client = CreateWinHttpClient(host); + client.DefaultRequestVersion = version; + + var result = await client.GetAsync("/Authenticate"); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(Http11Version, result.Version); // HTTP/2 downgrades. + } + + public static IEnumerable HttpOrders => + new List + { + new object[] { Http11Version, Http11Version }, + new object[] { Http11Version, Http2Version }, + new object[] { Http2Version, Http11Version }, + }; + + [ConditionalTheory] + [MemberData(nameof(HttpOrders))] + public async Task RequestAfterAuth_ReUses1WithPersistence(Version first, Version second) + { + using var host = await CreateHostAsync(options => options.PersistNtlmCredentials = true); + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade HTTP/2. WinHttpHandler does. + using var client = CreateWinHttpClient(host); + client.DefaultRequestVersion = first; + + var result = await client.GetAsync("/Authenticate"); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(Http11Version, result.Version); // Http/2 downgrades + + // Re-uses the 1.1 connection. + result = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/AlreadyAuthenticated") { Version = second }); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(Http11Version, result.Version); + } + + [ConditionalTheory] + [MemberData(nameof(HttpOrders))] + public async Task RequestAfterAuth_ReauthenticatesWhenNotPersisted(Version first, Version second) + { + using var host = await CreateHostAsync(options => options.PersistNtlmCredentials = false); + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade HTTP/2. WinHttpHandler does. + using var client = CreateWinHttpClient(host); + client.DefaultRequestVersion = first; + + var result = await client.GetAsync("/Authenticate"); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(Http11Version, result.Version); // Http/2 downgrades + + // Re-uses the 1.1 connection. + result = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/Authenticate") { Version = second }); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(Http11Version, result.Version); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public async Task RequestAfterAuth_Http2Then2_Success(bool persistNtlm) + { + using var host = await CreateHostAsync(options => options.PersistNtlmCredentials = persistNtlm); + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade HTTP/2. WinHttpHandler does. + using var client = CreateWinHttpClient(host); + client.DefaultRequestVersion = Http2Version; + + // Falls back to HTTP/1.1 after trying HTTP/2. + var result = await client.GetAsync("/Authenticate"); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(Http11Version, result.Version); + + // Tries HTTP/2, falls back to HTTP/1.1 and re-authenticates. + result = await client.GetAsync("/Authenticate"); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(Http11Version, result.Version); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public async Task RequestAfterAuth_Http2Then2Anonymous_Success(bool persistNtlm) + { + using var host = await CreateHostAsync(options => options.PersistNtlmCredentials = persistNtlm); + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade HTTP/2. WinHttpHandler does. + using var client = CreateWinHttpClient(host); + client.DefaultRequestVersion = Http2Version; + + // Falls back to HTTP/1.1 after trying HTTP/2. + var result = await client.GetAsync("/Authenticate"); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(Http11Version, result.Version); + + // Makes an anonymous HTTP/2 request + result = await client.GetAsync("/Anonymous2"); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(Http2Version, result.Version); + } + + [ConditionalTheory] + [MemberData(nameof(Http11And2))] + public async Task Unauthorized_401Negotiate(Version version) + { + using var host = await CreateHostAsync(); + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade. WinHttpHandler does. + using var client = CreateWinHttpClient(host); + client.DefaultRequestVersion = version; + + var result = await client.GetAsync("/Unauthorized"); + Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); + Assert.Equal("Negotiate", result.Headers.WwwAuthenticate.ToString()); + Assert.Equal(Http11Version, result.Version); // HTTP/2 downgrades. + } + + [ConditionalTheory] + [MemberData(nameof(Http11And2))] + public async Task UnauthorizedAfterAuthenticated_Success(Version version) + { + using var host = await CreateHostAsync(); + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade. WinHttpHandler does. + using var client = CreateWinHttpClient(host); + client.DefaultRequestVersion = version; + + var result = await client.GetAsync("/Authenticate"); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal(Http11Version, result.Version); // HTTP/2 downgrades. + + result = await client.GetAsync("/Unauthorized"); + Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); + Assert.Equal("Negotiate", result.Headers.WwwAuthenticate.ToString()); + Assert.Equal(Http11Version, result.Version); // HTTP/2 downgrades. + } + + private static Task CreateHostAsync(Action configureOptions = null) + { + var builder = new HostBuilder() + .ConfigureServices(services => services + .AddRouting() + .AddAuthentication(NegotiateDefaults.AuthenticationScheme) + .AddNegotiate(configureOptions)) + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder.UseKestrel(options => + { + options.Listen(IPAddress.Loopback, 0, endpoint => + { + endpoint.UseHttps("testCert.pfx", "testPassword"); + }); + }); + webHostBuilder.Configure(app => + { + app.UseRouting(); + app.UseAuthentication(); + app.UseEndpoints(ConfigureEndpoints); + }); + }); + + return builder.StartAsync(); + } + + private static void ConfigureEndpoints(IEndpointRouteBuilder builder) + { + builder.Map("/Anonymous1", context => + { + Assert.Equal("HTTP/1.1", context.Request.Protocol); + Assert.False(context.User.Identity.IsAuthenticated, "Anonymous"); + return Task.CompletedTask; + }); + + builder.Map("/Anonymous2", context => + { + Assert.Equal("HTTP/2", context.Request.Protocol); + Assert.False(context.User.Identity.IsAuthenticated, "Anonymous"); + return Task.CompletedTask; + }); + + builder.Map("/Authenticate", async context => + { + if (!context.User.Identity.IsAuthenticated) + { + await context.ChallengeAsync(); + return; + } + + Assert.Equal("HTTP/1.1", context.Request.Protocol); // Not HTTP/2 + var name = context.User.Identity.Name; + Assert.False(string.IsNullOrEmpty(name), "name"); + await context.Response.WriteAsync(name); + }); + + builder.Map("/AlreadyAuthenticated", async context => + { + Assert.Equal("HTTP/1.1", context.Request.Protocol); // Not HTTP/2 + Assert.True(context.User.Identity.IsAuthenticated, "Authenticated"); + var name = context.User.Identity.Name; + Assert.False(string.IsNullOrEmpty(name), "name"); + await context.Response.WriteAsync(name); + }); + + builder.Map("/Unauthorized", async context => + { + // Simulate Authorization failure + var result = await context.AuthenticateAsync(); + await context.ChallengeAsync(); + }); + } + + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade. WinHttpHandler does. + private static HttpClient CreateWinHttpClient(IHost host) + { + var address = host.Services.GetRequiredService().Features.Get().Addresses.First(); + + // WinHttpHandler always uses default credentials on localhost + return new HttpClient(new WinHttpHandler() + { + ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, + }) + { + BaseAddress = new Uri(address) + }; + } + + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade. WinHttpHandler does. + private static HttpClient CreateSocketHttpClient(IHost host, bool useDefaultCredentials = false) + { + var address = host.Services.GetRequiredService().Features.Get().Addresses.First(); + + return new HttpClient(new HttpClientHandler() + { + UseDefaultCredentials = useDefaultCredentials, + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, + }) + { + BaseAddress = new Uri(address) + }; + } + } +} diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/testCert.pfx b/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/testCert.pfx new file mode 100644 index 0000000000..888ccb032a Binary files /dev/null and b/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/testCert.pfx differ diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs b/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs new file mode 100644 index 0000000000..adc562d0dd --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs @@ -0,0 +1,393 @@ +// 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.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + public class EventTests + { + [Fact] + public async Task OnChallenge_Fires() + { + var eventInvoked = false; + var server = await CreateServerAsync(options => + { + options.Events = new NegotiateEvents() + { + OnChallenge = context => + { + // Not changed yet + eventInvoked = true; + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + Assert.False(context.Response.Headers.ContainsKey(HeaderNames.WWWAuthenticate)); + return Task.CompletedTask; + } + }; + }); + + var result = await SendAsync(server, "/Authenticate", new TestConnection()); + Assert.Equal(StatusCodes.Status401Unauthorized, result.Response.StatusCode); + Assert.Equal("Negotiate", result.Response.Headers[HeaderNames.WWWAuthenticate]); + Assert.True(eventInvoked); + } + + [Fact] + public async Task OnChallenge_Handled() + { + var server = await CreateServerAsync(options => + { + options.Events = new NegotiateEvents() + { + OnChallenge = context => + { + context.Response.StatusCode = StatusCodes.Status418ImATeapot; + context.Response.Headers[HeaderNames.WWWAuthenticate] = "Teapot"; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + }); + + var result = await SendAsync(server, "/Authenticate", new TestConnection()); + Assert.Equal(StatusCodes.Status418ImATeapot, result.Response.StatusCode); + Assert.Equal("Teapot", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + [Fact] + public async Task OnAuthenticationFailed_Fires() + { + var eventInvoked = false; + var server = await CreateServerAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + eventInvoked = true; + Assert.IsType(context.Exception); + Assert.Equal("InvalidBlob", context.Exception.Message); + return Task.CompletedTask; + } + }; + }); + + var ex = await Assert.ThrowsAsync(() => + SendAsync(server, "/404", new TestConnection(), "Negotiate InvalidBlob")); + Assert.Equal("InvalidBlob", ex.Message); + Assert.True(eventInvoked); + } + + [Fact] + public async Task OnAuthenticationFailed_Handled() + { + var server = await CreateServerAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + context.Response.StatusCode = StatusCodes.Status418ImATeapot; ; + context.Response.Headers[HeaderNames.WWWAuthenticate] = "Teapot"; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + }); + + var result = await SendAsync(server, "/404", new TestConnection(), "Negotiate InvalidBlob"); + Assert.Equal(StatusCodes.Status418ImATeapot, result.Response.StatusCode); + Assert.Equal("Teapot", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + [Fact] + public async Task OnAuthenticated_FiresOncePerRequest() + { + var callCount = 0; + var server = await CreateServerAsync(options => + { + options.PersistKerberosCredentials = true; + options.Events = new NegotiateEvents() + { + OnAuthenticated = context => + { + var identity = context.Principal.Identity; + Assert.True(identity.IsAuthenticated); + Assert.Equal("name", identity.Name); + Assert.Equal("Kerberos", identity.AuthenticationType); + callCount++; + return Task.CompletedTask; + } + }; + }); + + var testConnection = new TestConnection(); + await KerberosStage1And2Auth(server, testConnection); + var result = await SendAsync(server, "/Authenticate", testConnection); + Assert.Equal(StatusCodes.Status200OK, result.Response.StatusCode); + Assert.False(result.Response.Headers.ContainsKey(HeaderNames.WWWAuthenticate)); + Assert.Equal(2, callCount); + } + + [Fact] + public async Task OnAuthenticated_Success_Continues() + { + var callCount = 0; + var server = await CreateServerAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticated = context => + { + context.Success(); + callCount++; + return Task.CompletedTask; + } + }; + }); + + await KerberosStage1And2Auth(server, new TestConnection()); + Assert.Equal(1, callCount); + } + + [Fact] + public async Task OnAuthenticated_NoResult_SuppresesCredentials() + { + var callCount = 0; + var server = await CreateServerAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticated = context => + { + context.NoResult(); + callCount++; + return Task.CompletedTask; + } + }; + }); + + var result = await SendAsync(server, "/Authenticate", new TestConnection(), "Negotiate ClientKerberosBlob"); + Assert.Equal(StatusCodes.Status401Unauthorized, result.Response.StatusCode); + Assert.Equal("Negotiate", result.Response.Headers[HeaderNames.WWWAuthenticate]); + Assert.Equal(1, callCount); + } + + [Fact] + public async Task OnAuthenticated_Fail_SuppresesCredentials() + { + var callCount = 0; + var server = await CreateServerAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticated = context => + { + callCount++; + context.Fail("Event error."); + return Task.CompletedTask; + } + }; + }); + + var result = await SendAsync(server, "/Authenticate", new TestConnection(), "Negotiate ClientKerberosBlob"); + Assert.Equal(StatusCodes.Status401Unauthorized, result.Response.StatusCode); + Assert.Equal("Negotiate", result.Response.Headers[HeaderNames.WWWAuthenticate]); + Assert.Equal(1, callCount); + } + + private static async Task KerberosStage1And2Auth(TestServer server, TestConnection testConnection) + { + await KerberosStage1Auth(server, testConnection); + await KerberosStage2Auth(server, testConnection); + } + + private static async Task KerberosStage1Auth(TestServer server, TestConnection testConnection) + { + var result = await SendAsync(server, "/Authenticate", testConnection, "Negotiate ClientKerberosBlob1"); + Assert.Equal(StatusCodes.Status401Unauthorized, result.Response.StatusCode); + Assert.Equal("Negotiate ServerKerberosBlob1", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + private static async Task KerberosStage2Auth(TestServer server, TestConnection testConnection) + { + var result = await SendAsync(server, "/Authenticate", testConnection, "Negotiate ClientKerberosBlob2"); + Assert.Equal(StatusCodes.Status200OK, result.Response.StatusCode); + Assert.Equal("Negotiate ServerKerberosBlob2", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + private static async Task CreateServerAsync(Action configureOptions = null) + { + var builder = new HostBuilder() + .ConfigureServices(services => services + .AddRouting() + .AddAuthentication(NegotiateDefaults.AuthenticationScheme) + .AddNegotiate(options => + { + options.StateFactory = new TestNegotiateStateFactory(); + configureOptions?.Invoke(options); + })) + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder.UseTestServer(); + webHostBuilder.Configure(app => + { + app.UseRouting(); + app.UseAuthentication(); + app.UseEndpoints(ConfigureEndpoints); + }); + }); + + var server = (await builder.StartAsync()).GetTestServer(); + return server; + } + + private static void ConfigureEndpoints(IEndpointRouteBuilder builder) + { + builder.Map("/Authenticate", async context => + { + if (!context.User.Identity.IsAuthenticated) + { + await context.ChallengeAsync(); + return; + } + + Assert.Equal("HTTP/1.1", context.Request.Protocol); // Not HTTP/2 + var name = context.User.Identity.Name; + Assert.False(string.IsNullOrEmpty(name), "name"); + await context.Response.WriteAsync(name); + }); + } + + private static Task SendAsync(TestServer server, string path, TestConnection connection, string authorizationHeader = null) + { + return server.SendAsync(context => + { + context.Request.Path = path; + if (!string.IsNullOrEmpty(authorizationHeader)) + { + context.Request.Headers[HeaderNames.Authorization] = authorizationHeader; + } + if (connection != null) + { + context.Features.Set(connection); + context.Features.Set(connection); + } + }); + } + + private class TestConnection : IConnectionItemsFeature, IConnectionCompleteFeature + { + public IDictionary Items { get; set; } = new ConnectionItems(); + + public void OnCompleted(Func callback, object state) + { + } + } + + private class TestNegotiateStateFactory : INegotiateStateFactory + { + public INegotiateState CreateInstance() => new TestNegotiateState(); + } + + private class TestNegotiateState : INegotiateState + { + private string _protocol; + private bool Stage1Complete { get; set; } + public bool IsCompleted { get; private set; } + public bool IsDisposed { get; private set; } + + public string Protocol + { + get + { + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(TestNegotiateState)); + } + if (!Stage1Complete) + { + throw new InvalidOperationException("Authentication has not started yet."); + } + return _protocol; + } + } + + public void Dispose() + { + IsDisposed = true; + } + + public IIdentity GetIdentity() + { + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(TestNegotiateState)); + } + if (!IsCompleted) + { + throw new InvalidOperationException("Authentication is not complete."); + } + return new GenericIdentity("name", _protocol); + } + + public string GetOutgoingBlob(string incomingBlob) + { + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(TestNegotiateState)); + } + if (IsCompleted) + { + throw new InvalidOperationException("Authentication is already complete."); + } + switch (incomingBlob) + { + case "ClientNtlmBlob1": + Assert.False(Stage1Complete, nameof(Stage1Complete)); + Stage1Complete = true; + _protocol = "NTLM"; + return "ServerNtlmBlob1"; + case "ClientNtlmBlob2": + Assert.True(Stage1Complete, nameof(Stage1Complete)); + Assert.Equal("NTLM", _protocol); + IsCompleted = true; + return "ServerNtlmBlob2"; + // Kerberos can require one or two stages + case "ClientKerberosBlob": + Assert.False(Stage1Complete, nameof(Stage1Complete)); + _protocol = "Kerberos"; + Stage1Complete = true; + IsCompleted = true; + return "ServerKerberosBlob"; + case "ClientKerberosBlob1": + Assert.False(Stage1Complete, nameof(Stage1Complete)); + _protocol = "Kerberos"; + Stage1Complete = true; + return "ServerKerberosBlob1"; + case "ClientKerberosBlob2": + Assert.True(Stage1Complete, nameof(Stage1Complete)); + Assert.Equal("Kerberos", _protocol); + IsCompleted = true; + return "ServerKerberosBlob2"; + default: + throw new InvalidOperationException(incomingBlob); + } + } + } + } +} diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.Test/Microsoft.AspNetCore.Authentication.Negotiate.Test.csproj b/src/Security/Authentication/Negotiate/test/Negotiate.Test/Microsoft.AspNetCore.Authentication.Negotiate.Test.csproj new file mode 100644 index 0000000000..f0f681cda4 --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/Negotiate.Test/Microsoft.AspNetCore.Authentication.Negotiate.Test.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.0 + + + + + + + + + + + + + + + diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs b/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs new file mode 100644 index 0000000000..bced9c7608 --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs @@ -0,0 +1,506 @@ +// 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.Security.Claims; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Net.Http.Headers; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + public class NegotiateHandlerTests + { + [Fact] + public async Task Anonymous_MissingConnectionFeatures_ThrowsNotSupported() + { + var server = await CreateServerAsync(); + + var ex = await Assert.ThrowsAsync(() => SendAsync(server, "/Anonymous1", connection: null)); + Assert.Equal("Negotiate authentication requires a server that supports IConnectionItemsFeature like Kestrel.", ex.Message); + } + + [Fact] + public async Task Anonymous_NoChallenge_NoOps() + { + var server = await CreateServerAsync(); + + var result = await SendAsync(server, "/Anonymous1", new TestConnection()); + Assert.Equal(StatusCodes.Status200OK, result.Response.StatusCode); + } + + [Fact] + public async Task Anonymous_Http2_NoOps() + { + var server = await CreateServerAsync(); + + var result = await SendAsync(server, "/Anonymous2", connection: null, http2: true); + Assert.Equal(StatusCodes.Status200OK, result.Response.StatusCode); + } + + [Fact] + public async Task Anonymous_Challenge_401Negotiate() + { + var server = await CreateServerAsync(); + + var result = await SendAsync(server, "/Authenticate", new TestConnection()); + Assert.Equal(StatusCodes.Status401Unauthorized, result.Response.StatusCode); + Assert.Equal("Negotiate", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + [Fact] + public async Task Anonymous_ChallengeHttp2_401Negotiate() + { + var server = await CreateServerAsync(); + + var result = await SendAsync(server, "/Authenticate", connection: null, http2: true); + // Clients will downgrade to HTTP/1.1 and authenticate. + Assert.Equal(StatusCodes.Status401Unauthorized, result.Response.StatusCode); + Assert.Equal("Negotiate", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + [Fact] + public async Task NtlmStage1Auth_401NegotiateServerBlob1() + { + var server = await CreateServerAsync(); + var result = await SendAsync(server, "/404", new TestConnection(), "Negotiate ClientNtlmBlob1"); + Assert.Equal(StatusCodes.Status401Unauthorized, result.Response.StatusCode); + Assert.Equal("Negotiate ServerNtlmBlob1", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + [Fact] + public async Task AnonymousAfterNtlmStage1_Throws() + { + var server = await CreateServerAsync(); + var testConnection = new TestConnection(); + await NtlmStage1Auth(server, testConnection); + + var ex = await Assert.ThrowsAsync(() => SendAsync(server, "/404", testConnection)); + Assert.Equal("An anonymous request was received in between authentication handshake requests.", ex.Message); + } + + [Fact] + public async Task NtlmStage2Auth_WithoutStage1_Throws() + { + var server = await CreateServerAsync(); + + var ex = await Assert.ThrowsAsync(() => SendAsync(server, "/404", new TestConnection(), "Negotiate ClientNtlmBlob2")); + Assert.Equal("Stage1Complete", ex.UserMessage); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task NtlmStage1And2Auth_Success(bool persistNtlm) + { + var server = await CreateServerAsync(options => options.PersistNtlmCredentials = persistNtlm); + var testConnection = new TestConnection(); + await NtlmStage1And2Auth(server, testConnection); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task KerberosAuth_Success(bool persistKerberos) + { + var server = await CreateServerAsync(options => options.PersistKerberosCredentials = persistKerberos); + var testConnection = new TestConnection(); + await KerberosAuth(server, testConnection); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task KerberosTwoStageAuth_Success(bool persistKerberos) + { + var server = await CreateServerAsync(options => options.PersistKerberosCredentials = persistKerberos); + var testConnection = new TestConnection(); + await KerberosStage1And2Auth(server, testConnection); + } + + [Theory] + [InlineData("NTLM")] + [InlineData("Kerberos")] + [InlineData("Kerberos2")] + public async Task AnonymousAfterCompletedPersist_Cached(string protocol) + { + var server = await CreateServerAsync(options => options.PersistNtlmCredentials = options.PersistKerberosCredentials = true); + var testConnection = new TestConnection(); + if (protocol == "NTLM") + { + await NtlmStage1And2Auth(server, testConnection); + } + else if (protocol == "Kerberos2") + { + await KerberosStage1And2Auth(server, testConnection); + } + else + { + await KerberosAuth(server, testConnection); + } + + var result = await SendAsync(server, "/Authenticate", testConnection); + Assert.Equal(StatusCodes.Status200OK, result.Response.StatusCode); + Assert.False(result.Response.Headers.ContainsKey(HeaderNames.WWWAuthenticate)); + } + + [Theory] + [InlineData("NTLM")] + [InlineData("Kerberos")] + [InlineData("Kerberos2")] + public async Task AnonymousAfterCompletedNoPersist_Denied(string protocol) + { + var server = await CreateServerAsync(options => options.PersistNtlmCredentials = options.PersistKerberosCredentials = false); + var testConnection = new TestConnection(); + if (protocol == "NTLM") + { + await NtlmStage1And2Auth(server, testConnection); + } + else if (protocol == "Kerberos2") + { + await KerberosStage1And2Auth(server, testConnection); + } + else + { + await KerberosAuth(server, testConnection); + } + + var result = await SendAsync(server, "/Authenticate", testConnection); + Assert.Equal(StatusCodes.Status401Unauthorized, result.Response.StatusCode); + Assert.Equal("Negotiate", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task AuthHeaderAfterNtlmCompleted_ReAuthenticates(bool persist) + { + var server = await CreateServerAsync(options => options.PersistNtlmCredentials = persist); + var testConnection = new TestConnection(); + await NtlmStage1And2Auth(server, testConnection); + await NtlmStage1And2Auth(server, testConnection); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task AuthHeaderAfterKerberosCompleted_ReAuthenticates(bool persist) + { + var server = await CreateServerAsync(options => options.PersistNtlmCredentials = persist); + var testConnection = new TestConnection(); + await KerberosAuth(server, testConnection); + await KerberosAuth(server, testConnection); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task AuthHeaderAfterKerberos2StageCompleted_ReAuthenticates(bool persist) + { + var server = await CreateServerAsync(options => options.PersistNtlmCredentials = persist); + var testConnection = new TestConnection(); + await KerberosStage1And2Auth(server, testConnection); + await KerberosStage1And2Auth(server, testConnection); + } + + [Fact] + public async Task ApplicationExceptionReExecute_AfterComplete_DoesntReRun() + { + var builder = new HostBuilder() + .ConfigureServices(services => services + .AddAuthentication(NegotiateDefaults.AuthenticationScheme) + .AddNegotiate(options => + { + options.StateFactory = new TestNegotiateStateFactory(); + })) + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder.UseTestServer(); + webHostBuilder.Configure(app => + { + app.UseExceptionHandler("/error"); + app.UseAuthentication(); + app.Run(context => + { + Assert.True(context.User.Identity.IsAuthenticated); + if (context.Request.Path.Equals("/error")) + { + return context.Response.WriteAsync("Error Handler"); + } + + throw new TimeZoneNotFoundException(); + }); + }); + }); + + var server = (await builder.StartAsync()).GetTestServer(); + + var testConnection = new TestConnection(); + await NtlmStage1Auth(server, testConnection); + var result = await SendAsync(server, "/Authenticate", testConnection, "Negotiate ClientNtlmBlob2"); + Assert.Equal(StatusCodes.Status500InternalServerError, result.Response.StatusCode); + Assert.False(result.Response.Headers.ContainsKey(HeaderNames.WWWAuthenticate)); + } + + // Single Stage + private static async Task KerberosAuth(TestServer server, TestConnection testConnection) + { + var result = await SendAsync(server, "/Authenticate", testConnection, "Negotiate ClientKerberosBlob"); + Assert.Equal(StatusCodes.Status200OK, result.Response.StatusCode); + Assert.Equal("Negotiate ServerKerberosBlob", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + private static async Task KerberosStage1And2Auth(TestServer server, TestConnection testConnection) + { + await KerberosStage1Auth(server, testConnection); + await KerberosStage2Auth(server, testConnection); + } + + private static async Task KerberosStage1Auth(TestServer server, TestConnection testConnection) + { + var result = await SendAsync(server, "/Authenticate", testConnection, "Negotiate ClientKerberosBlob1"); + Assert.Equal(StatusCodes.Status401Unauthorized, result.Response.StatusCode); + Assert.Equal("Negotiate ServerKerberosBlob1", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + private static async Task KerberosStage2Auth(TestServer server, TestConnection testConnection) + { + var result = await SendAsync(server, "/Authenticate", testConnection, "Negotiate ClientKerberosBlob2"); + Assert.Equal(StatusCodes.Status200OK, result.Response.StatusCode); + Assert.Equal("Negotiate ServerKerberosBlob2", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + private static async Task NtlmStage1And2Auth(TestServer server, TestConnection testConnection) + { + await NtlmStage1Auth(server, testConnection); + await NtlmStage2Auth(server, testConnection); + } + + private static async Task NtlmStage1Auth(TestServer server, TestConnection testConnection) + { + var result = await SendAsync(server, "/404", testConnection, "Negotiate ClientNtlmBlob1"); + Assert.Equal(StatusCodes.Status401Unauthorized, result.Response.StatusCode); + Assert.Equal("Negotiate ServerNtlmBlob1", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + private static async Task NtlmStage2Auth(TestServer server, TestConnection testConnection) + { + var result = await SendAsync(server, "/Authenticate", testConnection, "Negotiate ClientNtlmBlob2"); + Assert.Equal(StatusCodes.Status200OK, result.Response.StatusCode); + Assert.Equal("Negotiate ServerNtlmBlob2", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + private static async Task CreateServerAsync(Action configureOptions = null) + { + var builder = new HostBuilder() + .ConfigureServices(services => services + .AddRouting() + .AddAuthentication(NegotiateDefaults.AuthenticationScheme) + .AddNegotiate(options => + { + options.StateFactory = new TestNegotiateStateFactory(); + configureOptions?.Invoke(options); + })) + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder.UseTestServer(); + webHostBuilder.Configure(app => + { + app.UseRouting(); + app.UseAuthentication(); + app.UseEndpoints(ConfigureEndpoints); + }); + }); + + var server = (await builder.StartAsync()).GetTestServer(); + return server; + } + + private static void ConfigureEndpoints(IEndpointRouteBuilder builder) + { + builder.Map("/Anonymous1", context => + { + Assert.Equal("HTTP/1.1", context.Request.Protocol); + Assert.False(context.User.Identity.IsAuthenticated, "Anonymous"); + return Task.CompletedTask; + }); + + builder.Map("/Anonymous2", context => + { + Assert.Equal("HTTP/2", context.Request.Protocol); + Assert.False(context.User.Identity.IsAuthenticated, "Anonymous"); + return Task.CompletedTask; + }); + + builder.Map("/Authenticate", async context => + { + if (!context.User.Identity.IsAuthenticated) + { + await context.ChallengeAsync(); + return; + } + + Assert.Equal("HTTP/1.1", context.Request.Protocol); // Not HTTP/2 + var name = context.User.Identity.Name; + Assert.False(string.IsNullOrEmpty(name), "name"); + await context.Response.WriteAsync(name); + }); + + builder.Map("/AlreadyAuthenticated", async context => + { + Assert.Equal("HTTP/1.1", context.Request.Protocol); // Not HTTP/2 + Assert.True(context.User.Identity.IsAuthenticated, "Authenticated"); + var name = context.User.Identity.Name; + Assert.False(string.IsNullOrEmpty(name), "name"); + await context.Response.WriteAsync(name); + }); + + builder.Map("/Unauthorized", async context => + { + // Simulate Authorization failure + var result = await context.AuthenticateAsync(); + await context.ChallengeAsync(); + }); + + builder.Map("/SignIn", context => + { + return Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + }); + + builder.Map("/signOut", context => + { + return Assert.ThrowsAsync(() => context.SignOutAsync()); + }); + } + + private static Task SendAsync(TestServer server, string path, TestConnection connection, string authorizationHeader = null, bool http2 = false) + { + return server.SendAsync(context => + { + context.Request.Protocol = http2 ? "HTTP/2" : "HTTP/1.1"; + context.Request.Path = path; + if (!string.IsNullOrEmpty(authorizationHeader)) + { + context.Request.Headers[HeaderNames.Authorization] = authorizationHeader; + } + if (connection != null) + { + context.Features.Set(connection); + context.Features.Set(connection); + } + }); + } + + private class TestConnection : IConnectionItemsFeature, IConnectionCompleteFeature + { + public IDictionary Items { get; set; } = new ConnectionItems(); + + public void OnCompleted(Func callback, object state) + { + } + } + + private class TestNegotiateStateFactory : INegotiateStateFactory + { + public INegotiateState CreateInstance() => new TestNegotiateState(); + } + + private class TestNegotiateState : INegotiateState + { + private string _protocol; + private bool Stage1Complete { get; set; } + public bool IsCompleted { get; private set; } + public bool IsDisposed { get; private set; } + + public string Protocol + { + get + { + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(TestNegotiateState)); + } + if (!Stage1Complete) + { + throw new InvalidOperationException("Authentication has not started yet."); + } + return _protocol; + } + } + + public void Dispose() + { + IsDisposed = true; + } + + public IIdentity GetIdentity() + { + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(TestNegotiateState)); + } + if (!IsCompleted) + { + throw new InvalidOperationException("Authentication is not complete."); + } + return new GenericIdentity("name", _protocol); + } + + public string GetOutgoingBlob(string incomingBlob) + { + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(TestNegotiateState)); + } + if (IsCompleted) + { + throw new InvalidOperationException("Authentication is already complete."); + } + switch (incomingBlob) + { + case "ClientNtlmBlob1": + Assert.False(Stage1Complete, nameof(Stage1Complete)); + Stage1Complete = true; + _protocol = "NTLM"; + return "ServerNtlmBlob1"; + case "ClientNtlmBlob2": + Assert.True(Stage1Complete, nameof(Stage1Complete)); + Assert.Equal("NTLM", _protocol); + IsCompleted = true; + return "ServerNtlmBlob2"; + // Kerberos can require one or two stages + case "ClientKerberosBlob": + Assert.False(Stage1Complete, nameof(Stage1Complete)); + _protocol = "Kerberos"; + Stage1Complete = true; + IsCompleted = true; + return "ServerKerberosBlob"; + case "ClientKerberosBlob1": + Assert.False(Stage1Complete, nameof(Stage1Complete)); + _protocol = "Kerberos"; + Stage1Complete = true; + return "ServerKerberosBlob1"; + case "ClientKerberosBlob2": + Assert.True(Stage1Complete, nameof(Stage1Complete)); + Assert.Equal("Kerberos", _protocol); + IsCompleted = true; + return "ServerKerberosBlob2"; + default: + throw new InvalidOperationException(incomingBlob); + } + } + } + } +} diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/Controllers/AuthTestController.cs b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/Controllers/AuthTestController.cs new file mode 100644 index 0000000000..ec201b996b --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/Controllers/AuthTestController.cs @@ -0,0 +1,361 @@ +// 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; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Negotiate.Client.Controllers +{ + [Route("authtest")] + [ApiController] + public class AuthTestController : ControllerBase + { + private const int StatusCode600WrongStatusCode = 600; + private const int StatusCode601WrongUser = 601; + private const int StatusCode602WrongAuthType = 602; + private const int StatusCode603WrongAuthHeader = 603; + private const int StatusCode604WrongProtocol = 604; + + private const string Http11Protocol = "HTTP/1.1"; + private const string Http2Protocol = "HTTP/2"; + + [HttpGet] + [Route("Anonymous/Unrestricted")] + public async Task AnonymousUnrestricted([FromQuery] string server, [FromQuery] string protocol) + { + var client = CreateSocketHttpClient(server); + client.DefaultRequestVersion = GetProtocolVersion(protocol); + + var result = await client.GetAsync("auth/Unrestricted"); + var body = await result.Content.ReadAsStringAsync(); + + if (HasWrongStatusCode(StatusCodes.Status200OK, result.StatusCode, body, out var actionResult) + || HasWrongProtocol(protocol, result.Version, out actionResult) + || HasUser(body, out actionResult)) + { + return actionResult; + } + + return Ok(); + } + + [HttpGet] + [Route("Anonymous/Authorized")] + public async Task AnonymousAuthorized([FromQuery] string server, [FromQuery] string protocol) + { + // Note WinHttpHandler cannot disable default credentials on localhost. + var client = CreateSocketHttpClient(server); + client.DefaultRequestVersion = GetProtocolVersion(protocol); + + var result = await client.GetAsync("auth/Authorized"); + var body = await result.Content.ReadAsStringAsync(); + + if (HasWrongStatusCode(StatusCodes.Status401Unauthorized, result.StatusCode, body, out var actionResult) + || HasWrongProtocol(protocol, result.Version, out actionResult)) + { + return actionResult; + } + + var authHeader = result.Headers.WwwAuthenticate.ToString(); + + if (!string.Equals("Negotiate", authHeader)) + { + return StatusCode(StatusCode603WrongAuthHeader, authHeader); + } + + return Ok(); + } + + [HttpGet] + [Route("DefaultCredentials/Authorized")] + public async Task DefaultCredentialsAuthorized([FromQuery] string server, [FromQuery] string protocol) + { + // Note WinHttpHandler cannot disable default credentials on localhost. + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade HTTP/2. WinHttpHandler does. + var client = CreateWinHttpClient(server, useDefaultCredentials: true); + client.DefaultRequestVersion = GetProtocolVersion(protocol); + + var result = await client.GetAsync("auth/Authorized"); + var body = await result.Content.ReadAsStringAsync(); + + if (HasWrongStatusCode(StatusCodes.Status200OK, result.StatusCode, body, out var actionResult) + // Automatic downgrade to HTTP/1.1 + || HasWrongProtocol(Http11Protocol, result.Version, out actionResult) + || MissingUser(body, out actionResult)) + { + return actionResult; + } + + return Ok(); + } + + [HttpGet] + [Route("AfterAuth/Unrestricted/Persist")] + public async Task AfterAuthUnrestrictedPersist([FromQuery] string server, [FromQuery] string protocol1, [FromQuery] string protocol2) + { + // Note WinHttpHandler cannot disable default credentials on localhost. + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade HTTP/2. WinHttpHandler does. + var client = CreateWinHttpClient(server, useDefaultCredentials: true); + client.DefaultRequestVersion = GetProtocolVersion(protocol1); + + var result = await client.GetAsync("auth/Authorized"); + var body = await result.Content.ReadAsStringAsync(); + + if (HasWrongStatusCode(StatusCodes.Status200OK, result.StatusCode, body, out var actionResult) + // Automatic downgrade to HTTP/1.1 + || HasWrongProtocol(Http11Protocol, result.Version, out actionResult) + || MissingUser(body, out actionResult)) + { + return actionResult; + } + + result = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "auth/Unrestricted") { Version = GetProtocolVersion(protocol2) }); + body = await result.Content.ReadAsStringAsync(); + + if (HasWrongStatusCode(StatusCodes.Status200OK, result.StatusCode, body, out actionResult) + || HasWrongProtocol(Http11Protocol, result.Version, out actionResult) + || MissingUser(body, out actionResult)) + { + return actionResult; + } + + return Ok(); + } + + [HttpGet] + [Route("AfterAuth/Unrestricted/NonPersist")] + public async Task AfterAuthUnrestrictedNonPersist([FromQuery] string server, [FromQuery] string protocol1, [FromQuery] string protocol2) + { + // Note WinHttpHandler cannot disable default credentials on localhost. + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade HTTP/2. WinHttpHandler does. + var client = CreateWinHttpClient(server, useDefaultCredentials: true); + client.DefaultRequestVersion = GetProtocolVersion(protocol1); + + var result = await client.GetAsync("auth/Authorized"); + var body = await result.Content.ReadAsStringAsync(); + + if (HasWrongStatusCode(StatusCodes.Status200OK, result.StatusCode, body, out var actionResult) + // Automatic downgrade to HTTP/1.1 + || HasWrongProtocol(Http11Protocol, result.Version, out actionResult) + || MissingUser(body, out actionResult)) + { + return actionResult; + } + + result = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "auth/Unrestricted") { Version = GetProtocolVersion(protocol2) }); + body = await result.Content.ReadAsStringAsync(); + + if (HasWrongStatusCode(StatusCodes.Status200OK, result.StatusCode, body, out actionResult) + || HasWrongProtocol(Http11Protocol, result.Version, out actionResult) + || HasUser(body, out actionResult)) + { + return actionResult; + } + + return Ok(); + } + + [HttpGet] + [Route("AfterAuth/Authorized/NonPersist")] + public async Task AfterAuthAuthorizedNonPersist([FromQuery] string server, [FromQuery] string protocol1, [FromQuery] string protocol2) + { + // Note WinHttpHandler cannot disable default credentials on localhost. + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade HTTP/2. WinHttpHandler does. + var client = CreateWinHttpClient(server, useDefaultCredentials: true); + client.DefaultRequestVersion = GetProtocolVersion(protocol1); + + var result = await client.GetAsync("auth/Authorized"); + var body = await result.Content.ReadAsStringAsync(); + + if (HasWrongStatusCode(StatusCodes.Status200OK, result.StatusCode, body, out var actionResult) + // Automatic downgrade to HTTP/1.1 + || HasWrongProtocol(Http11Protocol, result.Version, out actionResult) + || MissingUser(body, out actionResult)) + { + return actionResult; + } + + result = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "auth/Authorized") { Version = GetProtocolVersion(protocol2) }); + body = await result.Content.ReadAsStringAsync(); + + if (HasWrongStatusCode(StatusCodes.Status200OK, result.StatusCode, body, out actionResult) + || HasWrongProtocol(Http11Protocol, result.Version, out actionResult) + || MissingUser(body, out actionResult)) + { + return actionResult; + } + + return Ok(); + } + + [HttpGet] + [Route("Unauthorized")] + public async Task Unauthorized([FromQuery] string server, [FromQuery] string protocol) + { + var client = CreateWinHttpClient(server, useDefaultCredentials: true); + client.DefaultRequestVersion = GetProtocolVersion(protocol); + + var result = await client.GetAsync("auth/Unauthorized"); + var body = await result.Content.ReadAsStringAsync(); + + if (HasWrongStatusCode(StatusCodes.Status401Unauthorized, result.StatusCode, body, out var actionResult) + || HasWrongProtocol(Http11Protocol, result.Version, out actionResult)) // HTTP/2 downgrades. + { + return actionResult; + } + + var authHeader = result.Headers.WwwAuthenticate.ToString(); + + if (!string.Equals("Negotiate", authHeader)) + { + return StatusCode(StatusCode603WrongAuthHeader, authHeader); + } + + return Ok(); + } + + [HttpGet] + [Route("AfterAuth/Unauthorized")] + public async Task AfterAuthUnauthorized([FromQuery] string server, [FromQuery] string protocol) + { + var client = CreateWinHttpClient(server, useDefaultCredentials: true); + client.DefaultRequestVersion = GetProtocolVersion(protocol); + + var result = await client.GetAsync("auth/Authorized"); + var body = await result.Content.ReadAsStringAsync(); + + if (HasWrongStatusCode(StatusCodes.Status200OK, result.StatusCode, body, out var actionResult) + // Automatic downgrade to HTTP/1.1 + || HasWrongProtocol(Http11Protocol, result.Version, out actionResult) + || MissingUser(body, out actionResult)) + { + return actionResult; + } + + result = await client.GetAsync("auth/Unauthorized"); + body = await result.Content.ReadAsStringAsync(); + + if (HasWrongStatusCode(StatusCodes.Status401Unauthorized, result.StatusCode, body, out actionResult) + || HasWrongProtocol(Http11Protocol, result.Version, out actionResult)) // HTTP/2 downgrades. + { + return actionResult; + } + + var authHeader = result.Headers.WwwAuthenticate.ToString(); + + if (!string.Equals("Negotiate", authHeader)) + { + return StatusCode(StatusCode603WrongAuthHeader, authHeader); + } + + return Ok(); + } + + private bool HasWrongStatusCode(int expected, HttpStatusCode actual, string body, out IActionResult actionResult) + { + if (expected != (int)actual) + { + actionResult = StatusCode(StatusCode600WrongStatusCode, $"{actual} {body}"); + return true; + } + actionResult = null; + return false; + } + + private bool HasWrongProtocol(string expected, Version actual, out IActionResult actionResult) + { + if ((expected == Http11Protocol && actual != new Version(1, 1)) + || (expected == Http2Protocol && actual != new Version(2, 0))) + { + actionResult = StatusCode(StatusCode604WrongProtocol, actual.ToString()); + return true; + } + actionResult = null; + return false; + } + + private bool MissingUser(string body, out IActionResult actionResult) + { + var details = JsonDocument.Parse(body).RootElement; + + if (string.IsNullOrEmpty(details.GetProperty("name").GetString())) + { + actionResult = StatusCode(StatusCode601WrongUser, body); + return true; + } + + if (string.IsNullOrEmpty(details.GetProperty("authenticationType").GetString())) + { + actionResult = StatusCode(StatusCode602WrongAuthType, body); + return true; + } + + actionResult = null; + return false; + } + + private bool HasUser(string body, out IActionResult actionResult) + { + var details = JsonDocument.Parse(body).RootElement; + + if (!string.IsNullOrEmpty(details.GetProperty("name").GetString())) + { + actionResult = StatusCode(StatusCode601WrongUser, body); + return true; + } + + if (!string.IsNullOrEmpty(details.GetProperty("authenticationType").GetString())) + { + actionResult = StatusCode(StatusCode602WrongAuthType, body); + return true; + } + + actionResult = null; + return false; + } + + // Normally you'd want to re-use clients, but we want to ensure we have fresh state for each test. + + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade HTTP/2. WinHttpHandler does. + private HttpClient CreateSocketHttpClient(string remote, bool useDefaultCredentials = false) + { + return new HttpClient(new HttpClientHandler() + { + UseDefaultCredentials = useDefaultCredentials, + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, + }) + { + BaseAddress = new Uri(remote), + }; + } + + // https://github.com/dotnet/corefx/issues/35195 SocketHttpHandler won't downgrade HTTP/2. WinHttpHandler does. + private HttpClient CreateWinHttpClient(string remote, bool useDefaultCredentials = false) + { + // WinHttpHandler always uses default credentials on localhost + return new HttpClient(new WinHttpHandler() + { + ServerCredentials = CredentialCache.DefaultCredentials, + ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, + }) + { + BaseAddress = new Uri(remote) + }; + } + + private Version GetProtocolVersion(string protocol) + { + switch (protocol) + { + case "HTTP/1.1": return new Version(1, 1); + case "HTTP/2": return new Version(2, 0); + default: throw new NotImplementedException(Request.Protocol); + } + } + } +} diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/Negotiate.Client.csproj b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/Negotiate.Client.csproj new file mode 100644 index 0000000000..55a22b834d --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/Negotiate.Client.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp3.0 + OutOfProcess + + + + + + + + + + + + diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/Program.cs b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/Program.cs new file mode 100644 index 0000000000..04676b121b --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/Program.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Negotiate.Client +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/Properties/launchSettings.json b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/Properties/launchSettings.json new file mode 100644 index 0000000000..d4027d33b3 --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:2517", + "sslPort": 44346 + } + }, + "profiles": {/* + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + },*/ + "Negotiate.Client": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "https://localhost:5005;http://localhost:5004", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/ReadMe.md b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/ReadMe.md new file mode 100644 index 0000000000..a382593efb --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/ReadMe.md @@ -0,0 +1,4 @@ +Negotiate Client + +This project is part of a suite of cross machine tests. It's intended to be deployed to a client machine, controled via WebApi requests, +and make outbound authentication requests to a specified server. diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/Startup.cs b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/Startup.cs new file mode 100644 index 0000000000..67bb147440 --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/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.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Negotiate.Client +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/appsettings.Development.json b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/appsettings.Development.json new file mode 100644 index 0000000000..e203e9407e --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/appsettings.json b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/appsettings.json new file mode 100644 index 0000000000..d9d9a9bff6 --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Client/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Controllers/AuthController.cs b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Controllers/AuthController.cs new file mode 100644 index 0000000000..3e699e7fec --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Controllers/AuthController.cs @@ -0,0 +1,46 @@ +// 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.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Negotiate.Server.Controllers +{ + [Route("auth")] + [ApiController] + public class AuthController : ControllerBase + { + [HttpGet] + [Route("Unrestricted")] + public ObjectResult GetUnrestricted() + { + var user = HttpContext.User.Identity; + return new ObjectResult(new + { + user.Name, + user.AuthenticationType, + }); + } + + [HttpGet] + [Authorize] + [Route("Authorized")] + public ObjectResult GetAuthorized() + { + var user = HttpContext.User.Identity; + return new ObjectResult(new + { + user.Name, + user.AuthenticationType, + }); + } + + [HttpGet] + [Authorize] + [Route("Unauthorized")] + public ChallengeResult GetUnauthorized() + { + return Challenge(); + } + } +} diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Negotiate.Server.csproj b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Negotiate.Server.csproj new file mode 100644 index 0000000000..627f54bc35 --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Negotiate.Server.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp3.0 + OutOfProcess + + + + + + + + + + + + diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Program.cs b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Program.cs new file mode 100644 index 0000000000..f993cfe93e --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Program.cs @@ -0,0 +1,43 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Negotiate.Server +{ + public class Program + { + public static async Task Main(string[] args) + { + using var host1 = CreateHostBuilder(args.Append("Persist=true").ToArray()).Build(); + using var host2 = CreateHostBuilder(args.Append("Persist=false").ToArray()).Build(); + await host1.StartAsync(); + await host2.StartAsync(); + await host1.WaitForShutdownAsync(); // CTL+C + await host2.StopAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + webBuilder.ConfigureKestrel((context, options) => + { + if (string.Equals("true", context.Configuration["Persist"])) + { + options.ListenAnyIP(5000); + options.ListenAnyIP(5001, listenOptions => listenOptions.UseHttps()); + } + else + { + options.ListenAnyIP(5002); + options.ListenAnyIP(5003, listenOptions => listenOptions.UseHttps()); + } + }); + }); + } +} diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Properties/launchSettings.json b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Properties/launchSettings.json new file mode 100644 index 0000000000..089c887c88 --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:3049", + "sslPort": 44306 + } + }, + "profiles": { /* + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + },*/ + "Negotiate.Server": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "auth/user", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/ReadMe.md b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/ReadMe.md new file mode 100644 index 0000000000..9913fdf7c3 --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/ReadMe.md @@ -0,0 +1,5 @@ +Negotiate Server + +This project is part of a suite of cross machine tests. It's intended to be deployed to a server machine and invoked indirectly via a client. + +This project launches two servers on different ports, one with connection credential persistence enabled, and the other with it disabled. diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Startup.cs b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Startup.cs new file mode 100644 index 0000000000..9f2af6b18c --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/Startup.cs @@ -0,0 +1,49 @@ +// 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.Authentication.Negotiate; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Negotiate.Server +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) + .AddNegotiate(options => + { + var persist = string.Equals("true", Configuration["Persist"]); + options.PersistKerberosCredentials = persist; + options.PersistNtlmCredentials = persist; + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/appsettings.Development.json b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/appsettings.Development.json new file mode 100644 index 0000000000..e203e9407e --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/appsettings.json b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/appsettings.json new file mode 100644 index 0000000000..4e1d4aa1bb --- /dev/null +++ b/src/Security/Authentication/Negotiate/test/testassets/Negotiate.Server/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.AspNetCore.Authentication": "Debug", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Security/Authentication/test/SecureDataFormatTests.cs b/src/Security/Authentication/test/SecureDataFormatTests.cs index 4a821ca2db..9ff1c26728 100644 --- a/src/Security/Authentication/test/SecureDataFormatTests.cs +++ b/src/Security/Authentication/test/SecureDataFormatTests.cs @@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.Authentication.DataHandler } [ConditionalFact] - [SkipOnHelix] + [SkipOnHelix("https://github.com/aspnet/AspNetCore-Internal/issues/1974")] public void UnprotectWithDifferentPurposeFails() { var provider = ServiceProvider.GetRequiredService(); diff --git a/src/Security/Authentication/test/selfSigned.cer b/src/Security/Authentication/test/selfSigned.cer deleted file mode 100644 index 6acc7af5a6..0000000000 Binary files a/src/Security/Authentication/test/selfSigned.cer and /dev/null differ diff --git a/src/Security/Authorization/Core/ref/Microsoft.AspNetCore.Authorization.csproj b/src/Security/Authorization/Core/ref/Microsoft.AspNetCore.Authorization.csproj index edcb7281ec..e965b33d18 100644 --- a/src/Security/Authorization/Core/ref/Microsoft.AspNetCore.Authorization.csproj +++ b/src/Security/Authorization/Core/ref/Microsoft.AspNetCore.Authorization.csproj @@ -1,13 +1,11 @@ - netcoreapp3.0 + netstandard2.0 - - - - - + + + diff --git a/src/Security/Authorization/Core/ref/Microsoft.AspNetCore.Authorization.netcoreapp3.0.cs b/src/Security/Authorization/Core/ref/Microsoft.AspNetCore.Authorization.netstandard2.0.cs similarity index 84% rename from src/Security/Authorization/Core/ref/Microsoft.AspNetCore.Authorization.netcoreapp3.0.cs rename to src/Security/Authorization/Core/ref/Microsoft.AspNetCore.Authorization.netstandard2.0.cs index ef38eddb47..ab30a7c6d5 100644 --- a/src/Security/Authorization/Core/ref/Microsoft.AspNetCore.Authorization.netcoreapp3.0.cs +++ b/src/Security/Authorization/Core/ref/Microsoft.AspNetCore.Authorization.netstandard2.0.cs @@ -42,12 +42,6 @@ namespace Microsoft.AspNetCore.Authorization public virtual System.Threading.Tasks.Task HandleAsync(Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext context) { throw null; } protected abstract System.Threading.Tasks.Task HandleRequirementAsync(Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext context, TRequirement requirement, TResource resource); } - public partial class AuthorizationMiddleware - { - public AuthorizationMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider policyProvider) { } - [System.Diagnostics.DebuggerStepThroughAttribute] - public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) { throw null; } - } public partial class AuthorizationOptions { public AuthorizationOptions() { } @@ -220,50 +214,11 @@ namespace Microsoft.AspNetCore.Authorization.Infrastructure protected override System.Threading.Tasks.Task HandleRequirementAsync(Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext context, Microsoft.AspNetCore.Authorization.Infrastructure.RolesAuthorizationRequirement requirement) { throw null; } } } -namespace Microsoft.AspNetCore.Authorization.Policy -{ - public partial interface IPolicyEvaluator - { - System.Threading.Tasks.Task AuthenticateAsync(Microsoft.AspNetCore.Authorization.AuthorizationPolicy policy, Microsoft.AspNetCore.Http.HttpContext context); - System.Threading.Tasks.Task AuthorizeAsync(Microsoft.AspNetCore.Authorization.AuthorizationPolicy policy, Microsoft.AspNetCore.Authentication.AuthenticateResult authenticationResult, Microsoft.AspNetCore.Http.HttpContext context, object resource); - } - public partial class PolicyAuthorizationResult - { - internal PolicyAuthorizationResult() { } - public bool Challenged { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - public bool Forbidden { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - public bool Succeeded { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - public static Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult Challenge() { throw null; } - public static Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult Forbid() { throw null; } - public static Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult Success() { throw null; } - } - public partial class PolicyEvaluator : Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator - { - public PolicyEvaluator(Microsoft.AspNetCore.Authorization.IAuthorizationService authorization) { } - [System.Diagnostics.DebuggerStepThroughAttribute] - public virtual System.Threading.Tasks.Task AuthenticateAsync(Microsoft.AspNetCore.Authorization.AuthorizationPolicy policy, Microsoft.AspNetCore.Http.HttpContext context) { throw null; } - [System.Diagnostics.DebuggerStepThroughAttribute] - public virtual System.Threading.Tasks.Task AuthorizeAsync(Microsoft.AspNetCore.Authorization.AuthorizationPolicy policy, Microsoft.AspNetCore.Authentication.AuthenticateResult authenticationResult, Microsoft.AspNetCore.Http.HttpContext context, object resource) { throw null; } - } -} -namespace Microsoft.AspNetCore.Builder -{ - public static partial class AuthorizationAppBuilderExtensions - { - public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseAuthorization(this Microsoft.AspNetCore.Builder.IApplicationBuilder app) { throw null; } - } - public static partial class AuthorizationEndpointConventionBuilderExtensions - { - public static TBuilder RequireAuthorization(this TBuilder builder) where TBuilder : Microsoft.AspNetCore.Builder.IEndpointConventionBuilder { throw null; } - public static TBuilder RequireAuthorization(this TBuilder builder, params Microsoft.AspNetCore.Authorization.IAuthorizeData[] authorizeData) where TBuilder : Microsoft.AspNetCore.Builder.IEndpointConventionBuilder { throw null; } - public static TBuilder RequireAuthorization(this TBuilder builder, params string[] policyNames) where TBuilder : Microsoft.AspNetCore.Builder.IEndpointConventionBuilder { throw null; } - } -} namespace Microsoft.Extensions.DependencyInjection { public static partial class AuthorizationServiceCollectionExtensions { - public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAuthorization(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; } - public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAuthorization(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action configure) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAuthorizationCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAuthorizationCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action configure) { throw null; } } } diff --git a/src/Security/Authorization/Core/src/AuthorizationOptions.cs b/src/Security/Authorization/Core/src/AuthorizationOptions.cs index d9121f60ba..c834441813 100644 --- a/src/Security/Authorization/Core/src/AuthorizationOptions.cs +++ b/src/Security/Authorization/Core/src/AuthorizationOptions.cs @@ -29,10 +29,10 @@ namespace Microsoft.AspNetCore.Authorization /// /// Gets or sets the fallback authorization policy used by - /// when no IAuthorizeData have been provided. As a result, the uses the fallback policy + /// when no IAuthorizeData have been provided. As a result, the AuthorizationMiddleware uses the fallback policy /// if there are no instances for a resource. If a resource has any /// then they are evaluated instead of the fallback policy. By default the fallback policy is null, and usually will have no - /// effect unless you have the middleware in your pipeline. It is not used in any way by the + /// effect unless you have the AuthorizationMiddleware in your pipeline. It is not used in any way by the /// default . /// public AuthorizationPolicy FallbackPolicy { get; set; } diff --git a/src/Security/Authorization/Core/src/AuthorizationServiceCollectionExtensions.cs b/src/Security/Authorization/Core/src/AuthorizationServiceCollectionExtensions.cs index c3b0dc580b..0f788cd5ad 100644 --- a/src/Security/Authorization/Core/src/AuthorizationServiceCollectionExtensions.cs +++ b/src/Security/Authorization/Core/src/AuthorizationServiceCollectionExtensions.cs @@ -4,7 +4,6 @@ using System; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; -using Microsoft.AspNetCore.Authorization.Policy; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.Extensions.DependencyInjection @@ -19,7 +18,7 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The to add services to. /// The so that additional calls can be chained. - public static IServiceCollection AddAuthorization(this IServiceCollection services) + public static IServiceCollection AddAuthorizationCore(this IServiceCollection services) { if (services == null) { @@ -32,11 +31,6 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAdd(ServiceDescriptor.Transient()); services.TryAdd(ServiceDescriptor.Transient()); services.TryAddEnumerable(ServiceDescriptor.Transient()); - services.TryAddSingleton(); - - // Policy - services.TryAdd(ServiceDescriptor.Transient()); - return services; } @@ -46,20 +40,19 @@ namespace Microsoft.Extensions.DependencyInjection /// The to add services to. /// An action delegate to configure the provided . /// The so that additional calls can be chained. - public static IServiceCollection AddAuthorization(this IServiceCollection services, Action configure) + public static IServiceCollection AddAuthorizationCore(this IServiceCollection services, Action configure) { if (services == null) { throw new ArgumentNullException(nameof(services)); } - if (configure == null) + if (configure != null) { - throw new ArgumentNullException(nameof(configure)); + services.Configure(configure); } - services.Configure(configure); - return services.AddAuthorization(); + return services.AddAuthorizationCore(); } } } diff --git a/src/Security/Authorization/Core/src/Microsoft.AspNetCore.Authorization.csproj b/src/Security/Authorization/Core/src/Microsoft.AspNetCore.Authorization.csproj index 726247c53b..ca6b8ce526 100644 --- a/src/Security/Authorization/Core/src/Microsoft.AspNetCore.Authorization.csproj +++ b/src/Security/Authorization/Core/src/Microsoft.AspNetCore.Authorization.csproj @@ -1,11 +1,11 @@ - + ASP.NET Core authorization classes. Commonly used types: Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute Microsoft.AspNetCore.Authorization.AuthorizeAttribute - netcoreapp3.0 + netstandard2.0 true $(NoWarn);CS1591 true @@ -13,13 +13,7 @@ Microsoft.AspNetCore.Authorization.AuthorizeAttribute - - - - - - - + diff --git a/src/Security/Authorization/Core/src/Properties/AssemblyInfo.cs b/src/Security/Authorization/Core/src/Properties/AssemblyInfo.cs index aa5f527121..aaaa24d8c9 100644 --- a/src/Security/Authorization/Core/src/Properties/AssemblyInfo.cs +++ b/src/Security/Authorization/Core/src/Properties/AssemblyInfo.cs @@ -1,9 +1,10 @@ + // 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; using Microsoft.AspNetCore.Authorization; -// Microsoft.AspNetCore.Http.Abstractions +// Microsoft.AspNetCore.Metadata [assembly: TypeForwardedTo(typeof(IAuthorizeData))] [assembly: TypeForwardedTo(typeof(IAllowAnonymous))] diff --git a/src/Security/Authorization/Core/src/Properties/Resources.Designer.cs b/src/Security/Authorization/Core/src/Properties/Resources.Designer.cs index 876d0321b0..c83fa9ea5e 100644 --- a/src/Security/Authorization/Core/src/Properties/Resources.Designer.cs +++ b/src/Security/Authorization/Core/src/Properties/Resources.Designer.cs @@ -52,20 +52,6 @@ namespace Microsoft.AspNetCore.Authorization internal static string FormatException_RoleRequirementEmpty() => GetString("Exception_RoleRequirementEmpty"); - /// - /// Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. - /// - internal static string Exception_UnableToFindServices - { - get => GetString("Exception_UnableToFindServices"); - } - - /// - /// Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. - /// - internal static string FormatException_UnableToFindServices(object p0, object p1, object p2) - => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UnableToFindServices"), p0, p1, p2); - private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Security/Authorization/Core/src/Resources.resx b/src/Security/Authorization/Core/src/Resources.resx index ebf1328616..a36e55d6b0 100644 --- a/src/Security/Authorization/Core/src/Resources.resx +++ b/src/Security/Authorization/Core/src/Resources.resx @@ -126,7 +126,4 @@ At least one role must be specified. - - Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. - \ No newline at end of file diff --git a/src/Security/Authorization/Policy/ref/Microsoft.AspNetCore.Authorization.Policy.csproj b/src/Security/Authorization/Policy/ref/Microsoft.AspNetCore.Authorization.Policy.csproj index 6f81120877..2abea2d038 100644 --- a/src/Security/Authorization/Policy/ref/Microsoft.AspNetCore.Authorization.Policy.csproj +++ b/src/Security/Authorization/Policy/ref/Microsoft.AspNetCore.Authorization.Policy.csproj @@ -6,5 +6,8 @@ + + + diff --git a/src/Security/Authorization/Policy/ref/Microsoft.AspNetCore.Authorization.Policy.netcoreapp3.0.cs b/src/Security/Authorization/Policy/ref/Microsoft.AspNetCore.Authorization.Policy.netcoreapp3.0.cs index 14f716c60c..571d21a03d 100644 --- a/src/Security/Authorization/Policy/ref/Microsoft.AspNetCore.Authorization.Policy.netcoreapp3.0.cs +++ b/src/Security/Authorization/Policy/ref/Microsoft.AspNetCore.Authorization.Policy.netcoreapp3.0.cs @@ -1,11 +1,60 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +namespace Microsoft.AspNetCore.Authorization +{ + public partial class AuthorizationMiddleware + { + public AuthorizationMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider policyProvider) { } + [System.Diagnostics.DebuggerStepThroughAttribute] + public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) { throw null; } + } +} +namespace Microsoft.AspNetCore.Authorization.Policy +{ + public partial interface IPolicyEvaluator + { + System.Threading.Tasks.Task AuthenticateAsync(Microsoft.AspNetCore.Authorization.AuthorizationPolicy policy, Microsoft.AspNetCore.Http.HttpContext context); + System.Threading.Tasks.Task AuthorizeAsync(Microsoft.AspNetCore.Authorization.AuthorizationPolicy policy, Microsoft.AspNetCore.Authentication.AuthenticateResult authenticationResult, Microsoft.AspNetCore.Http.HttpContext context, object resource); + } + public partial class PolicyAuthorizationResult + { + internal PolicyAuthorizationResult() { } + public bool Challenged { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public bool Forbidden { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public bool Succeeded { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public static Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult Challenge() { throw null; } + public static Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult Forbid() { throw null; } + public static Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult Success() { throw null; } + } + public partial class PolicyEvaluator : Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator + { + public PolicyEvaluator(Microsoft.AspNetCore.Authorization.IAuthorizationService authorization) { } + [System.Diagnostics.DebuggerStepThroughAttribute] + public virtual System.Threading.Tasks.Task AuthenticateAsync(Microsoft.AspNetCore.Authorization.AuthorizationPolicy policy, Microsoft.AspNetCore.Http.HttpContext context) { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + public virtual System.Threading.Tasks.Task AuthorizeAsync(Microsoft.AspNetCore.Authorization.AuthorizationPolicy policy, Microsoft.AspNetCore.Authentication.AuthenticateResult authenticationResult, Microsoft.AspNetCore.Http.HttpContext context, object resource) { throw null; } + } +} +namespace Microsoft.AspNetCore.Builder +{ + public static partial class AuthorizationAppBuilderExtensions + { + public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseAuthorization(this Microsoft.AspNetCore.Builder.IApplicationBuilder app) { throw null; } + } + public static partial class AuthorizationEndpointConventionBuilderExtensions + { + public static TBuilder RequireAuthorization(this TBuilder builder) where TBuilder : Microsoft.AspNetCore.Builder.IEndpointConventionBuilder { throw null; } + public static TBuilder RequireAuthorization(this TBuilder builder, params Microsoft.AspNetCore.Authorization.IAuthorizeData[] authorizeData) where TBuilder : Microsoft.AspNetCore.Builder.IEndpointConventionBuilder { throw null; } + public static TBuilder RequireAuthorization(this TBuilder builder, params string[] policyNames) where TBuilder : Microsoft.AspNetCore.Builder.IEndpointConventionBuilder { throw null; } + } +} namespace Microsoft.Extensions.DependencyInjection { public static partial class PolicyServiceCollectionExtensions { - [System.ObsoleteAttribute("AddAuthorizationPolicyEvaluator is obsolete and will be removed in a future release. Use AddAuthorization instead.")] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAuthorization(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; } + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAuthorization(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action configure) { throw null; } public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAuthorizationPolicyEvaluator(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; } } } diff --git a/src/Security/Authorization/Core/src/Policy/AuthorizationAppBuilderExtensions.cs b/src/Security/Authorization/Policy/src/AuthorizationAppBuilderExtensions.cs similarity index 94% rename from src/Security/Authorization/Core/src/Policy/AuthorizationAppBuilderExtensions.cs rename to src/Security/Authorization/Policy/src/AuthorizationAppBuilderExtensions.cs index a34551243b..41cfcb96fb 100644 --- a/src/Security/Authorization/Core/src/Policy/AuthorizationAppBuilderExtensions.cs +++ b/src/Security/Authorization/Policy/src/AuthorizationAppBuilderExtensions.cs @@ -34,11 +34,11 @@ namespace Microsoft.AspNetCore.Builder { // Verify that AddAuthorizationPolicy was called before calling UseAuthorization // We use the AuthorizationPolicyMarkerService to ensure all the services were added. - if (app.ApplicationServices.GetService(typeof(AuthorizationMarkerService)) == null) + if (app.ApplicationServices.GetService(typeof(AuthorizationPolicyMarkerService)) == null) { throw new InvalidOperationException(Resources.FormatException_UnableToFindServices( nameof(IServiceCollection), - nameof(AuthorizationServiceCollectionExtensions.AddAuthorization), + nameof(PolicyServiceCollectionExtensions.AddAuthorization), "ConfigureServices(...)")); } } diff --git a/src/Security/Authorization/Core/src/Policy/AuthorizationEndpointConventionBuilderExtensions.cs b/src/Security/Authorization/Policy/src/AuthorizationEndpointConventionBuilderExtensions.cs similarity index 100% rename from src/Security/Authorization/Core/src/Policy/AuthorizationEndpointConventionBuilderExtensions.cs rename to src/Security/Authorization/Policy/src/AuthorizationEndpointConventionBuilderExtensions.cs diff --git a/src/Security/Authorization/Core/src/AuthorizationMiddleware.cs b/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs similarity index 100% rename from src/Security/Authorization/Core/src/AuthorizationMiddleware.cs rename to src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs diff --git a/src/Security/Authorization/Core/src/AuthorizationMarkerService.cs b/src/Security/Authorization/Policy/src/AuthorizationPolicyMarkerService.cs similarity index 81% rename from src/Security/Authorization/Core/src/AuthorizationMarkerService.cs rename to src/Security/Authorization/Policy/src/AuthorizationPolicyMarkerService.cs index 122d237e8c..9061891e43 100644 --- a/src/Security/Authorization/Core/src/AuthorizationMarkerService.cs +++ b/src/Security/Authorization/Policy/src/AuthorizationPolicyMarkerService.cs @@ -3,7 +3,7 @@ namespace Microsoft.AspNetCore.Authorization.Policy { - internal class AuthorizationMarkerService + internal class AuthorizationPolicyMarkerService { } } diff --git a/src/Security/Authorization/Core/src/Policy/IPolicyEvaluator.cs b/src/Security/Authorization/Policy/src/IPolicyEvaluator.cs similarity index 100% rename from src/Security/Authorization/Core/src/Policy/IPolicyEvaluator.cs rename to src/Security/Authorization/Policy/src/IPolicyEvaluator.cs diff --git a/src/Security/Authorization/Policy/src/Microsoft.AspNetCore.Authorization.Policy.csproj b/src/Security/Authorization/Policy/src/Microsoft.AspNetCore.Authorization.Policy.csproj index 2954d1564a..ecd83793a4 100644 --- a/src/Security/Authorization/Policy/src/Microsoft.AspNetCore.Authorization.Policy.csproj +++ b/src/Security/Authorization/Policy/src/Microsoft.AspNetCore.Authorization.Policy.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core authorization policy helper classes. @@ -9,8 +9,15 @@ aspnetcore;authorization + + + + + + + diff --git a/src/Security/Authorization/Core/src/Policy/PolicyAuthorizationResult.cs b/src/Security/Authorization/Policy/src/PolicyAuthorizationResult.cs similarity index 100% rename from src/Security/Authorization/Core/src/Policy/PolicyAuthorizationResult.cs rename to src/Security/Authorization/Policy/src/PolicyAuthorizationResult.cs diff --git a/src/Security/Authorization/Core/src/Policy/PolicyEvaluator.cs b/src/Security/Authorization/Policy/src/PolicyEvaluator.cs similarity index 100% rename from src/Security/Authorization/Core/src/Policy/PolicyEvaluator.cs rename to src/Security/Authorization/Policy/src/PolicyEvaluator.cs diff --git a/src/Security/Authorization/Policy/src/PolicyServiceCollectionExtensions.cs b/src/Security/Authorization/Policy/src/PolicyServiceCollectionExtensions.cs index a4eb53ba21..d24a5a243f 100644 --- a/src/Security/Authorization/Policy/src/PolicyServiceCollectionExtensions.cs +++ b/src/Security/Authorization/Policy/src/PolicyServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Policy; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -13,11 +14,10 @@ namespace Microsoft.Extensions.DependencyInjection public static class PolicyServiceCollectionExtensions { /// - /// Adds authorization policy services to the specified . + /// Adds the authorization policy evaluator service to the specified . /// /// The to add services to. /// The so that additional calls can be chained. - [Obsolete("AddAuthorizationPolicyEvaluator is obsolete and will be removed in a future release. Use AddAuthorization instead.")] public static IServiceCollection AddAuthorizationPolicyEvaluator(this IServiceCollection services) { if (services == null) @@ -25,7 +25,34 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(services)); } - services.AddAuthorization(); + services.TryAddSingleton(); + services.TryAdd(ServiceDescriptor.Transient()); + return services; + } + + /// + /// Adds authorization policy services to the specified . + /// + /// The to add services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddAuthorization(this IServiceCollection services) + => services.AddAuthorization(configure: null); + + /// + /// Adds authorization policy services to the specified . + /// + /// The to add services to. + /// An action delegate to configure the provided . + /// The so that additional calls can be chained. + public static IServiceCollection AddAuthorization(this IServiceCollection services, Action configure) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddAuthorizationCore(configure); + services.AddAuthorizationPolicyEvaluator(); return services; } } diff --git a/src/Security/Authorization/Policy/src/Properties/AssemblyInfo.cs b/src/Security/Authorization/Policy/src/Properties/AssemblyInfo.cs deleted file mode 100644 index 890d26fd78..0000000000 --- a/src/Security/Authorization/Policy/src/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// 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; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Authorization.Policy; - -// Microsoft.AspNetCore.Authorization -[assembly: TypeForwardedTo(typeof(IPolicyEvaluator))] -[assembly: TypeForwardedTo(typeof(PolicyAuthorizationResult))] -[assembly: TypeForwardedTo(typeof(PolicyEvaluator))] diff --git a/src/Security/Authorization/Policy/src/Properties/Resources.Designer.cs b/src/Security/Authorization/Policy/src/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..d4e50a8631 --- /dev/null +++ b/src/Security/Authorization/Policy/src/Properties/Resources.Designer.cs @@ -0,0 +1,44 @@ +// +namespace Microsoft.AspNetCore.Authorization.Policy +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Authorization.Policy.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. + /// + internal static string Exception_UnableToFindServices + { + get => GetString("Exception_UnableToFindServices"); + } + + /// + /// Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. + /// + internal static string FormatException_UnableToFindServices(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_UnableToFindServices"), p0, p1, p2); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Security/Authorization/Policy/src/Resources.resx b/src/Security/Authorization/Policy/src/Resources.resx new file mode 100644 index 0000000000..15d6f7d53c --- /dev/null +++ b/src/Security/Authorization/Policy/src/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 + + + Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. + + \ No newline at end of file diff --git a/src/Security/Authorization/test/AuthorizationAppBuilderExtensionsTests.cs b/src/Security/Authorization/test/AuthorizationAppBuilderExtensionsTests.cs index 1431364b1f..aa888daf80 100644 --- a/src/Security/Authorization/test/AuthorizationAppBuilderExtensionsTests.cs +++ b/src/Security/Authorization/test/AuthorizationAppBuilderExtensionsTests.cs @@ -2,8 +2,6 @@ // 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; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization.Test.TestObjects; @@ -73,9 +71,7 @@ namespace Microsoft.AspNetCore.Authorization.Test { var services = new ServiceCollection(); -#pragma warning disable CS0618 // Type or member is obsolete - services.AddAuthorizationPolicyEvaluator(); -#pragma warning restore CS0618 // Type or member is obsolete + services.AddAuthorization(); services.AddLogging(); services.AddSingleton(authenticationService); diff --git a/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs b/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs index 289a9d0d07..86858f4fe1 100644 --- a/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs +++ b/src/Security/Authorization/test/AuthorizationMiddlewareTests.cs @@ -2,18 +2,13 @@ // 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.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Authorization.Infrastructure; using Microsoft.AspNetCore.Authorization.Test.TestObjects; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using Moq; using Xunit; diff --git a/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs b/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs index 1d048dc164..d0fe9a62e9 100644 --- a/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs +++ b/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs @@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Authorization.Test private IAuthorizationService BuildAuthorizationService(Action setupServices = null) { var services = new ServiceCollection(); - services.AddAuthorization(); + services.AddAuthorizationCore(); services.AddLogging(); services.AddOptions(); setupServices?.Invoke(services); diff --git a/src/Security/Security.sln b/src/Security/Security.sln index cf56b85091..0e10f4be5e 100644 --- a/src/Security/Security.sln +++ b/src/Security/Security.sln @@ -136,6 +136,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routin EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IISIntegration", "..\Servers\IIS\IISIntegration\src\Microsoft.AspNetCore.Server.IISIntegration.csproj", "{FD3AB895-2AF6-447D-82CF-DB002B491D23}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Negotiate", "Negotiate", "{A482E4FD-51C2-4061-8357-1E4757D6CF27}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NegotiateAuthSample", "Authentication\Negotiate\Samples\NegotiateAuthSample\NegotiateAuthSample.csproj", "{473D25BB-9F02-4BA4-A47A-729E239C06FD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Negotiate", "Authentication\Negotiate\src\Microsoft.AspNetCore.Authentication.Negotiate.csproj", "{B7EA3B80-3A38-402A-BC3F-986907CA657C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Negotiate.Test", "Authentication\Negotiate\test\Negotiate.Test\Microsoft.AspNetCore.Authentication.Negotiate.Test.csproj", "{903E6CD4-7503-4BBB-86A1-96E0C73F0A90}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Negotiate.FunctionalTest", "Authentication\Negotiate\test\Negotiate.FunctionalTest\Microsoft.AspNetCore.Authentication.Negotiate.FunctionalTest.csproj", "{8991AEC8-49F3-4DF1-ADA9-00C13737E005}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Negotiate.Client", "Authentication\Negotiate\test\testassets\Negotiate.Client\Negotiate.Client.csproj", "{57DCE828-241E-437C-BEFC-AF4B6EB06D62}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Negotiate.Server", "Authentication\Negotiate\test\testassets\Negotiate.Server\Negotiate.Server.csproj", "{8771B5C8-4B96-4A40-A3FC-8CC7E16D7A82}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc", "..\Mvc\Mvc\src\Microsoft.AspNetCore.Mvc.csproj", "{27B5D7B5-75A6-4BE6-BD09-597044D06970}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Core", "..\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj", "{553F8C79-13AF-4993-99C1-D70F2143AD8E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -350,6 +368,38 @@ Global {FD3AB895-2AF6-447D-82CF-DB002B491D23}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD3AB895-2AF6-447D-82CF-DB002B491D23}.Release|Any CPU.ActiveCfg = Release|Any CPU {FD3AB895-2AF6-447D-82CF-DB002B491D23}.Release|Any CPU.Build.0 = Release|Any CPU + {473D25BB-9F02-4BA4-A47A-729E239C06FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {473D25BB-9F02-4BA4-A47A-729E239C06FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {473D25BB-9F02-4BA4-A47A-729E239C06FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {473D25BB-9F02-4BA4-A47A-729E239C06FD}.Release|Any CPU.Build.0 = Release|Any CPU + {B7EA3B80-3A38-402A-BC3F-986907CA657C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7EA3B80-3A38-402A-BC3F-986907CA657C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7EA3B80-3A38-402A-BC3F-986907CA657C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7EA3B80-3A38-402A-BC3F-986907CA657C}.Release|Any CPU.Build.0 = Release|Any CPU + {903E6CD4-7503-4BBB-86A1-96E0C73F0A90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {903E6CD4-7503-4BBB-86A1-96E0C73F0A90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {903E6CD4-7503-4BBB-86A1-96E0C73F0A90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {903E6CD4-7503-4BBB-86A1-96E0C73F0A90}.Release|Any CPU.Build.0 = Release|Any CPU + {8991AEC8-49F3-4DF1-ADA9-00C13737E005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8991AEC8-49F3-4DF1-ADA9-00C13737E005}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8991AEC8-49F3-4DF1-ADA9-00C13737E005}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8991AEC8-49F3-4DF1-ADA9-00C13737E005}.Release|Any CPU.Build.0 = Release|Any CPU + {57DCE828-241E-437C-BEFC-AF4B6EB06D62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57DCE828-241E-437C-BEFC-AF4B6EB06D62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {57DCE828-241E-437C-BEFC-AF4B6EB06D62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57DCE828-241E-437C-BEFC-AF4B6EB06D62}.Release|Any CPU.Build.0 = Release|Any CPU + {8771B5C8-4B96-4A40-A3FC-8CC7E16D7A82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8771B5C8-4B96-4A40-A3FC-8CC7E16D7A82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8771B5C8-4B96-4A40-A3FC-8CC7E16D7A82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8771B5C8-4B96-4A40-A3FC-8CC7E16D7A82}.Release|Any CPU.Build.0 = Release|Any CPU + {27B5D7B5-75A6-4BE6-BD09-597044D06970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27B5D7B5-75A6-4BE6-BD09-597044D06970}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27B5D7B5-75A6-4BE6-BD09-597044D06970}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27B5D7B5-75A6-4BE6-BD09-597044D06970}.Release|Any CPU.Build.0 = Release|Any CPU + {553F8C79-13AF-4993-99C1-D70F2143AD8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {553F8C79-13AF-4993-99C1-D70F2143AD8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {553F8C79-13AF-4993-99C1-D70F2143AD8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {553F8C79-13AF-4993-99C1-D70F2143AD8E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -417,6 +467,15 @@ Global {5B2F3890-198E-4BE8-8464-10B4D97F976A} = {A3766414-EB5C-40F7-B031-121804ED5D0A} {71961A8D-B26F-46AE-A475-D00425D875A0} = {A3766414-EB5C-40F7-B031-121804ED5D0A} {FD3AB895-2AF6-447D-82CF-DB002B491D23} = {A3766414-EB5C-40F7-B031-121804ED5D0A} + {A482E4FD-51C2-4061-8357-1E4757D6CF27} = {79C549BA-2932-450A-B87D-635879361343} + {473D25BB-9F02-4BA4-A47A-729E239C06FD} = {A482E4FD-51C2-4061-8357-1E4757D6CF27} + {B7EA3B80-3A38-402A-BC3F-986907CA657C} = {A482E4FD-51C2-4061-8357-1E4757D6CF27} + {903E6CD4-7503-4BBB-86A1-96E0C73F0A90} = {A482E4FD-51C2-4061-8357-1E4757D6CF27} + {8991AEC8-49F3-4DF1-ADA9-00C13737E005} = {A482E4FD-51C2-4061-8357-1E4757D6CF27} + {57DCE828-241E-437C-BEFC-AF4B6EB06D62} = {A482E4FD-51C2-4061-8357-1E4757D6CF27} + {8771B5C8-4B96-4A40-A3FC-8CC7E16D7A82} = {A482E4FD-51C2-4061-8357-1E4757D6CF27} + {27B5D7B5-75A6-4BE6-BD09-597044D06970} = {A3766414-EB5C-40F7-B031-121804ED5D0A} + {553F8C79-13AF-4993-99C1-D70F2143AD8E} = {A3766414-EB5C-40F7-B031-121804ED5D0A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {ABF8089E-43D0-4010-84A7-7A9DCFE49357} diff --git a/src/Servers/IIS/AspNetCoreModuleV2/Symbols/Microsoft.AspNetCore.ANCMSymbols.csproj b/src/Servers/IIS/AspNetCoreModuleV2/Symbols/Microsoft.AspNetCore.ANCMSymbols.csproj new file mode 100644 index 0000000000..8a17d1970b --- /dev/null +++ b/src/Servers/IIS/AspNetCoreModuleV2/Symbols/Microsoft.AspNetCore.ANCMSymbols.csproj @@ -0,0 +1,37 @@ + + + + + netcoreapp3.0 + true + $(PackNativeAssets) + false + runtimes/$(TargetRuntimeIdentifier)/native/ + + $(TargetsForTfmSpecificBuildOutput); + AddPackNativeComponents + + $(MSBuildProjectName).$(TargetRuntimeIdentifier) + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/scss/custom.scss b/src/Servers/IIS/AspNetCoreModuleV2/Symbols/_._ similarity index 100% rename from src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/scss/custom.scss rename to src/Servers/IIS/AspNetCoreModuleV2/Symbols/_._ diff --git a/src/Servers/IIS/IIS/benchmarks/IIS.Performance/StartupTimeBenchmark.cs b/src/Servers/IIS/IIS/benchmarks/IIS.Performance/StartupTimeBenchmark.cs index b1da8f1093..3cb8d3a1c8 100644 --- a/src/Servers/IIS/IIS/benchmarks/IIS.Performance/StartupTimeBenchmark.cs +++ b/src/Servers/IIS/IIS/benchmarks/IIS.Performance/StartupTimeBenchmark.cs @@ -20,11 +20,15 @@ namespace Microsoft.AspNetCore.Server.IIS.Performance [IterationSetup] public void Setup() { +// Deployers do not work in distributed environments +// see https://github.com/aspnet/AspNetCore/issues/10268 and https://github.com/aspnet/Extensions/issues/1697 +#pragma warning disable 0618 var deploymentParameters = new DeploymentParameters(Path.Combine(TestPathUtilities.GetSolutionRootDirectory("IISIntegration"), "test/testassets/InProcessWebSite"), ServerType.IISExpress, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64) { +#pragma warning restore 0618 ServerConfigTemplateContent = File.ReadAllText("IISExpress.config"), SiteName = "HttpTestSite", TargetFramework = "netcoreapp2.1", diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs index d55fc4bf4b..009ecd8319 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs @@ -113,8 +113,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests [SkipIfNotAdmin] [RequiresNewShim] [RequiresIIS(IISCapability.PoolEnvironmentVariables)] - [SkipOnHelix] - [Flaky("https://github.com/aspnet/AspNetCore-Internal/issues/2221", FlakyOn.Helix.All)] + [SkipOnHelix("https://github.com/aspnet/AspNetCore-Internal/issues/2221")] public async Task StartsWithDotnetInstallLocation(RuntimeArchitecture runtimeArchitecture) { var deploymentParameters = Fixture.GetBaseDeploymentParameters(); diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/PublishedApplicationPublisher.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/PublishedApplicationPublisher.cs index 3e09818933..d9aac2dac5 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/PublishedApplicationPublisher.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/PublishedApplicationPublisher.cs @@ -42,7 +42,11 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests private string GetProjectReferencePublishLocation(DeploymentParameters deploymentParameters) { +// Deployers do not work in distributed environments +// see https://github.com/aspnet/AspNetCore/issues/10268 and https://github.com/aspnet/Extensions/issues/1697 +#pragma warning disable 0618 var testAssetsBasePath = Path.Combine(TestPathUtilities.GetSolutionRootDirectory("IISIntegration"), "IIS", "test", "testassets", _applicationName); +#pragma warning restore 0618 var configuration = this.GetType().GetTypeInfo().Assembly.GetCustomAttribute().Configuration; var path = Path.Combine(testAssetsBasePath, "bin", configuration, deploymentParameters.TargetFramework, "publish", GetProfileName(deploymentParameters)); return path; diff --git a/src/Servers/IIS/IIS/test/IIS.Shared.FunctionalTests/MofFileTests.cs b/src/Servers/IIS/IIS/test/IIS.Shared.FunctionalTests/MofFileTests.cs index 8cc6e8c9ac..df0e867854 100644 --- a/src/Servers/IIS/IIS/test/IIS.Shared.FunctionalTests/MofFileTests.cs +++ b/src/Servers/IIS/IIS/test/IIS.Shared.FunctionalTests/MofFileTests.cs @@ -15,11 +15,14 @@ namespace IIS.FunctionalTests [ConditionalFact] [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] [RequiresIIS(IISCapability.TracingModule)] - [Flaky("https://github.com/aspnet/AspNetCore-Internal/issues/2222", FlakyOn.Helix.All)] - [SkipOnHelix] + [SkipOnHelix("https://github.com/aspnet/AspNetCore-Internal/issues/2222")] public void CheckMofFile() { +// This test code needs to be updated to support distributed testing. +// See https://github.com/aspnet/AspNetCore-Internal/issues/2222 +#pragma warning disable 0618 var path = Path.Combine(TestPathUtilities.GetSolutionRootDirectory("IISIntegration"), "aspnetcoremodulev2", "aspnetcore", "ancm.mof"); +#pragma warning restore 0618 var process = Process.Start("mofcomp.exe", path); process.WaitForExit(); Assert.Equal(0, process.ExitCode); diff --git a/src/Servers/IIS/build/assets.props b/src/Servers/IIS/build/assets.props index f6a7360235..4015e2338c 100644 --- a/src/Servers/IIS/build/assets.props +++ b/src/Servers/IIS/build/assets.props @@ -2,6 +2,7 @@ true + false x64 $(Platform) Win32 diff --git a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp3.0.cs b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp3.0.cs index 8aa67fc8e9..1c23c2dc98 100644 --- a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp3.0.cs +++ b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp3.0.cs @@ -276,6 +276,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure public static string GetAsciiOrUTF8StringNonNullCharacters(this System.Span span) { throw null; } public static string GetAsciiStringEscaped(this System.Span span, int maxChars) { throw null; } public static string GetAsciiStringNonNullCharacters(this System.Span span) { throw null; } + public static string GetHeaderName(this System.Span span) { throw null; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]public static bool GetKnownHttpScheme(this System.Span span, out Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpScheme knownScheme) { throw null; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]public static bool GetKnownMethod(this System.Span span, out Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod method, out int length) { throw null; } public static Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod GetKnownMethod(string value) { throw null; } diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index 4b41f63c44..7ae053cb15 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -602,4 +602,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l This feature is not supported for HTTP/2 requests except to disable it entirely by setting the rate to null. + + The request trailers are not available yet. They may not be available until the full request body is read. + \ No newline at end of file diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs index fdc8edb0cf..7c1327cd73 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs @@ -35,7 +35,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http : base(context) { RequestKeepAlive = keepAlive; - _requestBodyPipe = CreateRequestBodyPipe(context); } @@ -301,7 +300,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // _consumedBytes aren't tracked for trailer headers, since headers have separate limits. if (_mode == Mode.TrailerHeaders) { - if (_context.TakeMessageHeaders(readableBuffer, out consumed, out examined)) + if (_context.TakeMessageHeaders(readableBuffer, trailers: true, out consumed, out examined)) { _mode = Mode.Complete; } @@ -489,6 +488,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http consumed = trailerBuffer.End; AddAndCheckConsumedBytes(2); _mode = Mode.Complete; + // No trailers + _context.OnTrailersComplete(); } else { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index d657d70ee0..6a54f47e28 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs @@ -163,7 +163,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http break; } case RequestProcessingStatus.ParsingHeaders: - if (TakeMessageHeaders(buffer, out consumed, out examined)) + if (TakeMessageHeaders(buffer, trailers: false, out consumed, out examined)) { _requestProcessingStatus = RequestProcessingStatus.AppStarted; } @@ -189,7 +189,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return result; } - public bool TakeMessageHeaders(ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined) + public bool TakeMessageHeaders(ReadOnlySequence buffer, bool trailers, out SequencePosition consumed, out SequencePosition examined) { // Make sure the buffer is limited bool overLength = false; @@ -202,7 +202,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http overLength = true; } - var result = _parser.ParseHeaders(new Http1ParsingHandler(this), buffer, out consumed, out examined, out var consumedBytes); + var result = _parser.ParseHeaders(new Http1ParsingHandler(this, trailers), buffer, out consumed, out examined, out var consumedBytes); _remainingRequestHeadersBytesAllowed -= consumedBytes; if (!result && overLength) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs index 66d97819ba..2d13e68680 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs @@ -212,6 +212,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { _context.Input.AdvanceTo(consumed); _finalAdvanceCalled = true; + _context.OnTrailersComplete(); } return; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs index a661946a62..0691c842d7 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs @@ -129,6 +129,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http BadHttpRequestException.Throw(RequestRejectionReason.UpgradeRequestCannotHavePayload); } + context.OnTrailersComplete(); // No trailers for these. return new Http1UpgradeMessageBody(context); } @@ -173,6 +174,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http BadHttpRequestException.Throw(requestRejectionReason, context.Method); } + context.OnTrailersComplete(); // No trailers for these. return keepAlive ? MessageBody.ZeroContentLengthKeepAlive : MessageBody.ZeroContentLengthClose; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs index bcc905cab9..b4c67c13a3 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs @@ -8,17 +8,43 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http internal readonly struct Http1ParsingHandler : IHttpRequestLineHandler, IHttpHeadersHandler { public readonly Http1Connection Connection; + public readonly bool Trailers; public Http1ParsingHandler(Http1Connection connection) { Connection = connection; + Trailers = false; + } + + public Http1ParsingHandler(Http1Connection connection, bool trailers) + { + Connection = connection; + Trailers = trailers; } public void OnHeader(Span name, Span value) - => Connection.OnHeader(name, value); + { + if (Trailers) + { + Connection.OnTrailer(name, value); + } + else + { + Connection.OnHeader(name, value); + } + } public void OnHeadersComplete() - => Connection.OnHeadersComplete(); + { + if (Trailers) + { + Connection.OnTrailersComplete(); + } + else + { + Connection.OnHeadersComplete(); + } + } public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded) => Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs index 637d6e086f..11fcadc074 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs @@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http IHttpConnectionFeature, IHttpRequestLifetimeFeature, IHttpRequestIdentifierFeature, + IHttpRequestTrailersFeature, IHttpBodyControlFeature, IHttpMaxRequestBodySizeFeature, IHttpResponseStartFeature, @@ -133,6 +134,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } + bool IHttpRequestTrailersFeature.Available => RequestTrailersAvailable; + + IHeaderDictionary IHttpRequestTrailersFeature.Trailers + { + get + { + if (!RequestTrailersAvailable) + { + throw new InvalidOperationException(CoreStrings.RequestTrailersNotAvailable); + } + return RequestTrailers; + } + } + int IHttpResponseFeature.StatusCode { get => StatusCode; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs index 8ba4e4b050..b9b3e26905 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs @@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private static readonly Type IRouteValuesFeatureType = typeof(IRouteValuesFeature); private static readonly Type IEndpointFeatureType = typeof(IEndpointFeature); private static readonly Type IHttpAuthenticationFeatureType = typeof(IHttpAuthenticationFeature); + private static readonly Type IHttpRequestTrailersFeatureType = typeof(IHttpRequestTrailersFeature); private static readonly Type IQueryFeatureType = typeof(IQueryFeature); private static readonly Type IFormFeatureType = typeof(IFormFeature); private static readonly Type IHttpUpgradeFeatureType = typeof(IHttpUpgradeFeature); @@ -52,6 +53,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private object _currentIRouteValuesFeature; private object _currentIEndpointFeature; private object _currentIHttpAuthenticationFeature; + private object _currentIHttpRequestTrailersFeature; private object _currentIQueryFeature; private object _currentIFormFeature; private object _currentIHttpUpgradeFeature; @@ -82,6 +84,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _currentIHttpUpgradeFeature = this; _currentIHttpRequestIdentifierFeature = this; _currentIHttpRequestLifetimeFeature = this; + _currentIHttpRequestTrailersFeature = this; _currentIHttpConnectionFeature = this; _currentIHttpMaxRequestBodySizeFeature = this; _currentIHttpMinRequestBodyDataRateFeature = this; @@ -201,6 +204,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { feature = _currentIHttpAuthenticationFeature; } + else if (key == IHttpRequestTrailersFeatureType) + { + feature = _currentIHttpRequestTrailersFeature; + } else if (key == IQueryFeatureType) { feature = _currentIQueryFeature; @@ -321,6 +328,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { _currentIHttpAuthenticationFeature = value; } + else if (key == IHttpRequestTrailersFeatureType) + { + _currentIHttpRequestTrailersFeature = value; + } else if (key == IQueryFeatureType) { _currentIQueryFeature = value; @@ -439,6 +450,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { feature = (TFeature)_currentIHttpAuthenticationFeature; } + else if (typeof(TFeature) == typeof(IHttpRequestTrailersFeature)) + { + feature = (TFeature)_currentIHttpRequestTrailersFeature; + } else if (typeof(TFeature) == typeof(IQueryFeature)) { feature = (TFeature)_currentIQueryFeature; @@ -563,6 +578,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { _currentIHttpAuthenticationFeature = feature; } + else if (typeof(TFeature) == typeof(IHttpRequestTrailersFeature)) + { + _currentIHttpRequestTrailersFeature = feature; + } else if (typeof(TFeature) == typeof(IQueryFeature)) { _currentIQueryFeature = feature; @@ -679,6 +698,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { yield return new KeyValuePair(IHttpAuthenticationFeatureType, _currentIHttpAuthenticationFeature); } + if (_currentIHttpRequestTrailersFeature != null) + { + yield return new KeyValuePair(IHttpRequestTrailersFeatureType, _currentIHttpRequestTrailersFeature); + } if (_currentIQueryFeature != null) { yield return new KeyValuePair(IQueryFeatureType, _currentIQueryFeature); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 78701c5571..ea8e77cd3b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -206,6 +206,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } public IHeaderDictionary RequestHeaders { get; set; } + public IHeaderDictionary RequestTrailers { get; } = new HeaderDictionary(); + public bool RequestTrailersAvailable { get; set; } public Stream RequestBody { get; set; } public PipeReader RequestBodyPipeReader { get; set; } @@ -369,6 +371,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http HttpResponseHeaders.Reset(); RequestHeaders = HttpRequestHeaders; ResponseHeaders = HttpResponseHeaders; + RequestTrailers.Clear(); + RequestTrailersAvailable = false; _isLeasedMemoryInvalid = true; _hasAdvanced = false; @@ -524,11 +528,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http HttpRequestHeaders.Append(name, value); } + public void OnTrailer(Span name, Span value) + { + // Trailers still count towards the limit. + _requestHeadersParsed++; + if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount) + { + BadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); + } + + string key = name.GetHeaderName(); + var valueStr = value.GetAsciiOrUTF8StringNonNullCharacters(); + RequestTrailers.Append(key, valueStr); + } + public void OnHeadersComplete() { HttpRequestHeaders.OnHeadersComplete(); } + public void OnTrailersComplete() + { + RequestTrailersAvailable = true; + } + public async Task ProcessRequestsAsync(IHttpApplication application) { try diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs index ea3adc24ba..a6bf25ecbb 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs @@ -103,16 +103,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http [MethodImpl(MethodImplOptions.NoInlining)] private unsafe void AppendUnknownHeaders(Span name, string valueString) { - string key = new string('\0', name.Length); - fixed (byte* pKeyBytes = name) - fixed (char* keyBuffer = key) - { - if (!StringUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, name.Length)) - { - BadHttpRequestException.Throw(RequestRejectionReason.InvalidCharactersInHeaderName); - } - } - + string key = name.GetHeaderName(); Unknown.TryGetValue(key, out var existing); Unknown[key] = AppendValue(existing, valueString); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index fed15bdf83..fd5728b8e2 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -1073,9 +1073,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 ValidateHeader(name, value); try { - // Drop trailers for now. Adding them to the request headers is not thread safe. - // https://github.com/aspnet/KestrelHttpServer/issues/2051 - if (_requestHeaderParsingState != RequestHeaderParsingState.Trailers) + if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) + { + _currentHeadersStream.OnTrailer(name, value); + } + else { // Throws BadRequest for header count limit breaches. // Throws InvalidOperation for bad encoding. diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs index dfefd1a3d1..5a27a5641f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs @@ -403,6 +403,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } + OnTrailersComplete(); RequestBodyPipe.Writer.Complete(); _inputFlowControl.StopWindowUpdates(); diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs index 7c5276259e..5c266d85df 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs @@ -84,6 +84,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure } } + // The same as GetAsciiStringNonNullCharacters but throws BadRequest + public static unsafe string GetHeaderName(this Span span) + { + if (span.IsEmpty) + { + return string.Empty; + } + + var asciiString = new string('\0', span.Length); + + fixed (char* output = asciiString) + fixed (byte* buffer = span) + { + // This version if AsciiUtilities returns null if there are any null (0 byte) characters + // in the string + if (!StringUtilities.TryGetAsciiString(buffer, output, span.Length)) + { + BadHttpRequestException.Throw(RequestRejectionReason.InvalidCharactersInHeaderName); + } + } + + return asciiString; + } + public static unsafe string GetAsciiStringNonNullCharacters(this Span span) { if (span.IsEmpty) diff --git a/src/Servers/Kestrel/Core/src/Properties/CoreStrings.Designer.cs b/src/Servers/Kestrel/Core/src/Properties/CoreStrings.Designer.cs index a63a15c259..80f4cdecd6 100644 --- a/src/Servers/Kestrel/Core/src/Properties/CoreStrings.Designer.cs +++ b/src/Servers/Kestrel/Core/src/Properties/CoreStrings.Designer.cs @@ -2268,6 +2268,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatHttp2MinDataRateNotSupported() => GetString("Http2MinDataRateNotSupported"); + /// + /// The request trailers are not available yet. They may not be available until the full request body is read. + /// + internal static string RequestTrailersNotAvailable + { + get => GetString("RequestTrailersNotAvailable"); + } + + /// + /// The request trailers are not available yet. They may not be available until the full request body is read. + /// + internal static string FormatRequestTrailersNotAvailable() + => GetString("RequestTrailersNotAvailable"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs b/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs index 4c3cb758e9..494e4531e6 100644 --- a/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs +++ b/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs @@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.UTF8.GetBytes("\r\n\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined); + _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined); _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(headerValue, _http1Connection.RequestHeaders[headerName]); @@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(extendedAsciiEncoding.GetBytes("\r\n\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined)); + var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined)); } [Fact] @@ -129,7 +129,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine}\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined)); + var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, exception.Message); @@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLines}\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined)); + var exception = Assert.Throws(() => _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined)); _transport.Input.AdvanceTo(_consumed, _examined); Assert.Equal(CoreStrings.BadRequest_TooManyHeaders, exception.Message); @@ -250,7 +250,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine1}\r\n")); var readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - var takeMessageHeaders = _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined); + var takeMessageHeaders = _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined); _transport.Input.AdvanceTo(_consumed, _examined); Assert.True(takeMessageHeaders); @@ -262,7 +262,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine2}\r\n")); readableBuffer = (await _transport.Input.ReadAsync()).Buffer; - takeMessageHeaders = _http1Connection.TakeMessageHeaders(readableBuffer, out _consumed, out _examined); + takeMessageHeaders = _http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out _consumed, out _examined); _transport.Input.AdvanceTo(_consumed, _examined); Assert.True(takeMessageHeaders); diff --git a/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs b/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs index c8fb802885..53f5cfcc1e 100644 --- a/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs @@ -121,6 +121,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _collection[typeof(IRequestBodyPipeFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpRequestIdentifierFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpRequestLifetimeFeature)] = CreateHttp1Connection(); + _collection[typeof(IHttpRequestTrailersFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpConnectionFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpMaxRequestBodySizeFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpMinRequestBodyDataRateFeature)] = CreateHttp1Connection(); @@ -144,6 +145,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); + _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs index 75bc420343..84ac57e630 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs @@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance ErrorUtilities.ThrowInvalidRequestLine(); } - if (!_http1Connection.TakeMessageHeaders(_buffer, out consumed, out examined)) + if (!_http1Connection.TakeMessageHeaders(_buffer, trailers: false, out consumed, out examined)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } @@ -103,7 +103,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance { _http1Connection.Reset(); - if (!_http1Connection.TakeMessageHeaders(_buffer, out var consumed, out var examined)) + if (!_http1Connection.TakeMessageHeaders(_buffer, trailers: false, out var consumed, out var examined)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs index 33ae90daeb..da9f0f7faf 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs @@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance readableBuffer = readableBuffer.Slice(consumed); - if (!Http1Connection.TakeMessageHeaders(readableBuffer, out consumed, out examined)) + if (!Http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out consumed, out examined)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } @@ -196,7 +196,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance result = Pipe.Reader.ReadAsync().GetAwaiter().GetResult(); readableBuffer = result.Buffer; - if (!Http1Connection.TakeMessageHeaders(readableBuffer, out consumed, out examined)) + if (!Http1Connection.TakeMessageHeaders(readableBuffer, trailers: false, out consumed, out examined)) { ErrorUtilities.ThrowInvalidRequestHeaders(); } diff --git a/src/Servers/Kestrel/test/BindTests/AddressRegistrationTests.cs b/src/Servers/Kestrel/test/BindTests/AddressRegistrationTests.cs index 654b7b6fea..79c436a4ca 100644 --- a/src/Servers/Kestrel/test/BindTests/AddressRegistrationTests.cs +++ b/src/Servers/Kestrel/test/BindTests/AddressRegistrationTests.cs @@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [ConditionalFact] [HostNameIsReachable] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/7267 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/7267")] public async Task RegisterAddresses_HostName_Success() { var hostName = Dns.GetHostName(); @@ -335,7 +335,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [ConditionalFact] [HostNameIsReachable] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/7267 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/7267")] public async Task ListenAnyIP_HostName_Success() { var hostName = Dns.GetHostName(); diff --git a/src/Servers/Kestrel/test/FunctionalTests/Http2/HandshakeTests.cs b/src/Servers/Kestrel/test/FunctionalTests/Http2/HandshakeTests.cs index 3c392b00ce..59cfbbc2c9 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/Http2/HandshakeTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/Http2/HandshakeTests.cs @@ -20,7 +20,7 @@ using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2 { [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")] - [SkipOnHelix(Queues = "Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/10428", Queues = "Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2 [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)] public class HandshakeTests : LoggedTest { diff --git a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs index 9175c4cf79..3e587da067 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2 { [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)] - [SkipOnHelix(Queues = "Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/10428", Queues = "Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2 public class ShutdownTests : TestApplicationErrorLoggerLoggedTest { private static X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate(); @@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2 } [ConditionalFact] - [SkipOnHelix(Queues = "Fedora.28.Amd64.Open")] // https://github.com/aspnet/AspNetCore/issues/9985 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/9985", Queues = "Fedora.28.Amd64.Open")] // https://github.com/aspnet/AspNetCore/issues/9985 [Flaky("https://github.com/aspnet/AspNetCore/issues/9985", FlakyOn.All)] public async Task GracefulShutdownWaitsForRequestsToFinish() { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs index 7247e11226..2f3f01a941 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs @@ -232,18 +232,59 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests var buffer = new byte[200]; + // The first request is chunked with no trailers. + if (requestsReceived == 0) + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet + Assert.Throws(() => request.GetTrailer("X-Trailer-Header")); // Not yet + } + // The middle requests are chunked with trailers. + else if (requestsReceived < requestCount) + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet + Assert.Throws(() => request.GetTrailer("X-Trailer-Header")); // Not yet + Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString()); + } + // The last request is content-length with no trailers. + else + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Throws(() => request.GetTrailer("X-Trailer-Header")); + } + while (await request.Body.ReadAsync(buffer, 0, buffer.Length) != 0) { ;// read to end } - if (requestsReceived < requestCount) + Assert.False(request.Headers.ContainsKey("X-Trailer-Header")); + + // The first request is chunked with no trailers. + if (requestsReceived == 0) { - Assert.Equal(new string('a', requestsReceived), request.Headers["X-Trailer-Header"].ToString()); + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString()); + Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString()); } + // The middle requests are chunked with trailers. + else if (requestsReceived < requestCount) + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString()); + Assert.Equal(new string('a', requestsReceived), request.GetTrailer("X-Trailer-Header").ToString()); + } + // The last request is content-length with no trailers. else { - Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"])); + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString()); + Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString()); } requestsReceived++; @@ -278,6 +319,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests "POST / HTTP/1.1", "Host:", "Transfer-Encoding: chunked", + "Trailer: X-Trailer-Header", "", "C", $"HelloChunk{i:00}", @@ -315,6 +357,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests var response = httpContext.Response; var request = httpContext.Request; + // The first request is chunked with no trailers. + if (requestsReceived == 0) + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet + Assert.Throws(() => request.GetTrailer("X-Trailer-Header")); // Not yet + } + // The middle requests are chunked with trailers. + else if (requestsReceived < requestCount) + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet + Assert.Throws(() => request.GetTrailer("X-Trailer-Header")); // Not yet + Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString()); + } + // The last request is content-length with no trailers. + else + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Throws(() => request.GetTrailer("X-Trailer-Header")); + } + while (true) { var result = await request.BodyReader.ReadAsync(); @@ -325,13 +390,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests } } - if (requestsReceived < requestCount) + Assert.False(request.Headers.ContainsKey("X-Trailer-Header")); + + // The first request is chunked with no trailers. + if (requestsReceived == 0) { - Assert.Equal(new string('a', requestsReceived), request.Headers["X-Trailer-Header"].ToString()); + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString()); + Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString()); } + // The middle requests are chunked with trailers. + else if (requestsReceived < requestCount) + { + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString()); + Assert.Equal(new string('a', requestsReceived), request.GetTrailer("X-Trailer-Header").ToString()); + } + // The last request is content-length with no trailers. else { - Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"])); + Assert.True(request.SupportsTrailers(), "SupportsTrailers"); + Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); + Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString()); + Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString()); } requestsReceived++; @@ -366,6 +449,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests "POST / HTTP/1.1", "Host:", "Transfer-Encoding: chunked", + "Trailer: X-Trailer-Header", "", "C", $"HelloChunk{i:00}", @@ -495,13 +579,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests ;// read to end } + Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"])); + if (requestsReceived < requestCount) { - Assert.Equal(new string('a', requestsReceived), request.Headers["X-Trailer-Header"].ToString()); - } - else - { - Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"])); + Assert.Equal(new string('a', requestsReceived), request.GetTrailer("X-Trailer-Header").ToString()); } requestsReceived++; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 2081227840..593f3bc644 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -1174,7 +1174,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Theory] [InlineData(true)] [InlineData(false)] - public async Task HEADERS_Received_WithTrailers_Discarded(bool sendData) + public async Task HEADERS_Received_WithTrailers_Available(bool sendData) { await InitializeConnectionAsync(_readTrailersApplication); @@ -1206,10 +1206,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests VerifyDecodedRequestHeaders(_browserRequestHeaders); - // Make sure the trailers are missing. https://github.com/aspnet/KestrelHttpServer/issues/2630 + // Make sure the trailers are in the trailers collection. foreach (var header in _requestTrailers) { Assert.False(_receivedHeaders.ContainsKey(header.Key)); + Assert.True(_receivedTrailers.ContainsKey(header.Key)); + Assert.Equal(header.Value, _receivedTrailers[header.Key]); } await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); @@ -3288,7 +3290,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Theory] [InlineData(true)] [InlineData(false)] - public async Task CONTINUATION_Received_WithTrailers_Discarded(bool sendData) + public async Task CONTINUATION_Received_WithTrailers_Available(bool sendData) { await InitializeConnectionAsync(_readTrailersApplication); @@ -3331,9 +3333,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests VerifyDecodedRequestHeaders(_browserRequestHeaders); - // Make sure the trailers are missing. https://github.com/aspnet/KestrelHttpServer/issues/2630 + // Make sure the trailers are in the trailers collection. Assert.False(_receivedHeaders.ContainsKey("trailer-1")); Assert.False(_receivedHeaders.ContainsKey("trailer-2")); + Assert.True(_receivedTrailers.ContainsKey("trailer-1")); + Assert.True(_receivedTrailers.ContainsKey("trailer-2")); + Assert.Equal("1", _receivedTrailers["trailer-1"]); + Assert.Equal("2", _receivedTrailers["trailer-2"]); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index 3e024a9bce..6751565db1 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -130,6 +130,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected readonly ConcurrentDictionary> _runningStreams = new ConcurrentDictionary>(); protected readonly Dictionary _receivedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + protected readonly Dictionary _receivedTrailers = new Dictionary(StringComparer.OrdinalIgnoreCase); protected readonly Dictionary _decodedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); protected readonly HashSet _abortedStreamIds = new HashSet(); protected readonly object _abortedStreamIdsLock = new object(); @@ -199,16 +200,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _readTrailersApplication = async context => { + Assert.True(context.Request.SupportsTrailers(), "SupportsTrailers"); + Assert.False(context.Request.CheckTrailersAvailable(), "SupportsTrailers"); + using (var ms = new MemoryStream()) { // Consuming the entire request body guarantees trailers will be available await context.Request.Body.CopyToAsync(ms); } + Assert.True(context.Request.SupportsTrailers(), "SupportsTrailers"); + Assert.True(context.Request.CheckTrailersAvailable(), "SupportsTrailers"); + foreach (var header in context.Request.Headers) { _receivedHeaders[header.Key] = header.Value.ToString(); } + + var trailers = context.Features.Get().Trailers; + + foreach (var header in trailers) + { + _receivedTrailers[header.Key] = header.Value.ToString(); + } }; _bufferingApplication = async context => diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs index a0214a49d0..283d0c2e2d 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.Http2 private static X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate(); [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/7000 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/7000 ")] public async Task TlsHandshakeRejectsTlsLessThan12() { using (var server = new TestServer(context => diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs index 6feeeef83a..ef5e141063 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; @@ -360,7 +361,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests } while (count != 0); Assert.Equal("Hello World", Encoding.ASCII.GetString(buffer)); - Assert.Equal("trailing-value", context.Request.Headers["Trailing-Header"].ToString()); + Assert.Equal("trailing-value", context.Request.GetTrailer("Trailing-Header").ToString()); }, new TestServiceContext(LoggerFactory) { ServerOptions = { Limits = { MaxRequestBodySize = globalMaxRequestBodySize } } })) { diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/H2SpecTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/H2SpecTests.cs index 3e1636490a..98dd378202 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/H2SpecTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/H2SpecTests.cs @@ -21,7 +21,7 @@ namespace Interop.FunctionalTests [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81, SkipReason = "Missing Windows ALPN support: https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation#Support")] - [SkipOnHelix(Queues = "Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/10428", Queues = "Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2 public class H2SpecTests : LoggedTest { [ConditionalTheory] diff --git a/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs b/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs index c8bd5f3d3e..8266770719 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs +++ b/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs @@ -26,6 +26,7 @@ namespace CodeGenerator var commonFeatures = new[] { "IHttpAuthenticationFeature", + "IHttpRequestTrailersFeature", "IQueryFeature", "IFormFeature", }; @@ -69,6 +70,7 @@ namespace CodeGenerator "IHttpUpgradeFeature", "IHttpRequestIdentifierFeature", "IHttpRequestLifetimeFeature", + "IHttpRequestTrailersFeature", "IHttpConnectionFeature", "IHttpMaxRequestBodySizeFeature", "IHttpMinRequestBodyDataRateFeature", diff --git a/src/Servers/build.cmd b/src/Servers/build.cmd new file mode 100644 index 0000000000..033fe6f614 --- /dev/null +++ b/src/Servers/build.cmd @@ -0,0 +1,3 @@ +@ECHO OFF +SET RepoRoot=%~dp0..\.. +%RepoRoot%\build.cmd -projects %~dp0\**\*.*proj %* diff --git a/src/Servers/test/FunctionalTests/NtlmAuthenticationTest.cs b/src/Servers/test/FunctionalTests/NtlmAuthenticationTest.cs index 254418ca23..2c0d6e15de 100644 --- a/src/Servers/test/FunctionalTests/NtlmAuthenticationTest.cs +++ b/src/Servers/test/FunctionalTests/NtlmAuthenticationTest.cs @@ -21,12 +21,14 @@ namespace ServerComparison.FunctionalTests } public static TestMatrix TestVariants - => TestMatrix.ForServers(ServerType.IISExpress, ServerType.HttpSys) + => TestMatrix.ForServers(ServerType.IISExpress, ServerType.HttpSys, ServerType.Kestrel) .WithTfms(Tfm.NetCoreApp30) .WithAllHostingModels(); [ConditionalTheory] [MemberData(nameof(TestVariants))] + // In theory it could work on these platforms but the client would need non-default credentials. + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] public async Task NtlmAuthentication(TestVariant variant) { var testName = $"NtlmAuthentication_{variant.Server}_{variant.Tfm}_{variant.Architecture}_{variant.ApplicationType}"; @@ -65,7 +67,14 @@ namespace ServerComparison.FunctionalTests response = await httpClient.GetAsync("/Restricted"); responseText = await response.Content.ReadAsStringAsync(); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString()); + if (variant.Server == ServerType.Kestrel) + { + Assert.DoesNotContain("NTLM", response.Headers.WwwAuthenticate.ToString()); + } + else + { + Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString()); + } Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString()); logger.LogInformation("Testing /Forbidden"); @@ -97,4 +106,4 @@ namespace ServerComparison.FunctionalTests } } } -} \ No newline at end of file +} diff --git a/src/Servers/testassets/ServerComparison.TestSites/ServerComparison.TestSites.csproj b/src/Servers/testassets/ServerComparison.TestSites/ServerComparison.TestSites.csproj index 73eec76d86..eb8d5f9d98 100644 --- a/src/Servers/testassets/ServerComparison.TestSites/ServerComparison.TestSites.csproj +++ b/src/Servers/testassets/ServerComparison.TestSites/ServerComparison.TestSites.csproj @@ -1,4 +1,4 @@ - + @@ -9,6 +9,7 @@ + diff --git a/src/Servers/testassets/ServerComparison.TestSites/StartupNtlmAuthentication.cs b/src/Servers/testassets/ServerComparison.TestSites/StartupNtlmAuthentication.cs index c4e837db58..73810a6b80 100644 --- a/src/Servers/testassets/ServerComparison.TestSites/StartupNtlmAuthentication.cs +++ b/src/Servers/testassets/ServerComparison.TestSites/StartupNtlmAuthentication.cs @@ -3,14 +3,39 @@ using System; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Negotiate; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.IISIntegration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace ServerComparison.TestSites { public class StartupNtlmAuthentication { + public IConfiguration Configuration { get; } + public bool IsKestrel => string.Equals(Configuration["server"], "Microsoft.AspNetCore.Server.Kestrel"); + + public StartupNtlmAuthentication(IConfiguration configuration) + { + Configuration = configuration; + } + + public void ConfigureServices(IServiceCollection services) + { + if (IsKestrel) + { + services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) + .AddNegotiate(); + } + else + { + services.AddAuthentication(IISDefaults.AuthenticationScheme); + } + } + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { app.Use(async (context, next) => @@ -31,6 +56,7 @@ namespace ServerComparison.TestSites } }); + app.UseAuthentication(); app.Use((context, next) => { if (context.Request.Path.Equals("/Anonymous")) @@ -46,17 +72,17 @@ namespace ServerComparison.TestSites } else { - return context.ChallengeAsync("Windows"); + return context.ChallengeAsync(); } } if (context.Request.Path.Equals("/Forbidden")) { - return context.ForbidAsync("Windows"); + return context.ForbidAsync(); } return context.Response.WriteAsync("Hello World"); }); } } -} \ No newline at end of file +} diff --git a/src/Shared/test/SkipOnHelixAttribute.cs b/src/Shared/test/SkipOnHelixAttribute.cs index 2ad4018acc..e1a74467bc 100644 --- a/src/Shared/test/SkipOnHelixAttribute.cs +++ b/src/Shared/test/SkipOnHelixAttribute.cs @@ -12,6 +12,17 @@ namespace Microsoft.AspNetCore.Testing.xunit [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] public class SkipOnHelixAttribute : Attribute, ITestCondition { + public SkipOnHelixAttribute(string issueUrl) + { + if (string.IsNullOrEmpty(issueUrl)) + { + throw new ArgumentException(); + } + IssueUrl = issueUrl; + } + + public string IssueUrl { get; } + public bool IsMet { get @@ -33,7 +44,7 @@ namespace Microsoft.AspNetCore.Testing.xunit } public static bool OnHelix() => !string.IsNullOrEmpty(GetTargetHelixQueue()); - + public static string GetTargetHelixQueue() => Environment.GetEnvironmentVariable("helix"); } } diff --git a/src/SignalR/build.cmd b/src/SignalR/build.cmd index b7a7ab4c70..a36772c967 100644 --- a/src/SignalR/build.cmd +++ b/src/SignalR/build.cmd @@ -1,3 +1,3 @@ @ECHO OFF SET RepoRoot=%~dp0..\..\ -%RepoRoot%build.cmd -projects %~dp0**\*.*proj %* +%RepoRoot%build.cmd -buildJava -projects %~dp0**\*.*proj %* diff --git a/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs b/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs index 560d32978e..13ddb56792 100644 --- a/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs +++ b/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs @@ -253,7 +253,10 @@ namespace Microsoft.AspNetCore.SignalR.Client throw new InvalidOperationException($"The {nameof(HubConnection)} cannot be started while {nameof(StopAsync)} is running."); } - await StartAsyncCore(cancellationToken); + using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _state.StopCts.Token)) + { + await StartAsyncCore(cancellationToken); + } _state.ChangeState(HubConnectionState.Connecting, HubConnectionState.Connected); } @@ -422,7 +425,7 @@ namespace Microsoft.AspNetCore.SignalR.Client Log.Starting(_logger); // Start the connection - var connection = await _connectionFactory.ConnectAsync(_protocol.TransferFormat); + var connection = await _connectionFactory.ConnectAsync(_protocol.TransferFormat, cancellationToken); var startingConnectionState = new ConnectionState(connection, this); // From here on, if an error occurs we need to shut down the connection because @@ -1023,7 +1026,8 @@ namespace Microsoft.AspNetCore.SignalR.Client try { using (var handshakeCts = new CancellationTokenSource(HandshakeTimeout)) - using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, handshakeCts.Token, _state.StopCts.Token)) + // cancellationToken already contains _state.StopCts.Token, so we don't have to link it again + using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, handshakeCts.Token)) { while (true) { @@ -1287,7 +1291,7 @@ namespace Microsoft.AspNetCore.SignalR.Client var reconnectStartTime = DateTime.UtcNow; var retryReason = closeException; var nextRetryDelay = GetNextRetryDelay(previousReconnectAttempts++, TimeSpan.Zero, retryReason); - + // We still have the connection lock from the caller, HandleConnectionClose. _state.AssertInConnectionLock(); @@ -1347,8 +1351,7 @@ namespace Microsoft.AspNetCore.SignalR.Client SafeAssert(ReferenceEquals(_state.CurrentConnectionStateUnsynchronized, null), "Someone other than Reconnect set the connection state!"); - // HandshakeAsync already checks ReconnectingConnectionState.StopCts.Token. - await StartAsyncCore(CancellationToken.None); + await StartAsyncCore(_state.StopCts.Token); Log.Reconnected(_logger, previousReconnectAttempts, DateTime.UtcNow - reconnectStartTime); diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/TestTransport.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/TestTransport.cs index e6395692c5..ae1249dba7 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/TestTransport.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/TestTransport.cs @@ -1,5 +1,6 @@ using System; using System.IO.Pipelines; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Connections.Client; @@ -29,13 +30,13 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests Format = transferFormat; } - public async Task StartAsync(Uri url, TransferFormat transferFormat) + public async Task StartAsync(Uri url, TransferFormat transferFormat, CancellationToken cancellationToken = default) { if ((Format & transferFormat) == 0) { throw new InvalidOperationException($"The '{transferFormat}' transfer format is not supported by this transport."); } - + var options = ClientPipeOptions.DefaultOptions; var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netcoreapp3.0.cs b/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netcoreapp3.0.cs index df268f20ba..dd78ecdf90 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netcoreapp3.0.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netcoreapp3.0.cs @@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal { public partial interface ITransport : System.IO.Pipelines.IDuplexPipe { - System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat); + System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); System.Threading.Tasks.Task StopAsync(); } public partial interface ITransportFactory @@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal public System.IO.Pipelines.PipeReader Input { get { throw null; } } public System.IO.Pipelines.PipeWriter Output { get { throw null; } } [System.Diagnostics.DebuggerStepThroughAttribute] - public System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat) { throw null; } + public System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task StopAsync() { throw null; } } @@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal public System.IO.Pipelines.PipeReader Input { get { throw null; } } public System.IO.Pipelines.PipeWriter Output { get { throw null; } } [System.Diagnostics.DebuggerStepThroughAttribute] - public System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat) { throw null; } + public System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task StopAsync() { throw null; } } @@ -86,7 +86,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal public System.IO.Pipelines.PipeReader Input { get { throw null; } } public System.IO.Pipelines.PipeWriter Output { get { throw null; } } [System.Diagnostics.DebuggerStepThroughAttribute] - public System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat) { throw null; } + public System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task StopAsync() { throw null; } } diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netstandard2.0.cs b/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netstandard2.0.cs index df268f20ba..dd78ecdf90 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netstandard2.0.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netstandard2.0.cs @@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal { public partial interface ITransport : System.IO.Pipelines.IDuplexPipe { - System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat); + System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); System.Threading.Tasks.Task StopAsync(); } public partial interface ITransportFactory @@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal public System.IO.Pipelines.PipeReader Input { get { throw null; } } public System.IO.Pipelines.PipeWriter Output { get { throw null; } } [System.Diagnostics.DebuggerStepThroughAttribute] - public System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat) { throw null; } + public System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task StopAsync() { throw null; } } @@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal public System.IO.Pipelines.PipeReader Input { get { throw null; } } public System.IO.Pipelines.PipeWriter Output { get { throw null; } } [System.Diagnostics.DebuggerStepThroughAttribute] - public System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat) { throw null; } + public System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task StopAsync() { throw null; } } @@ -86,7 +86,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal public System.IO.Pipelines.PipeReader Input { get { throw null; } } public System.IO.Pipelines.PipeWriter Output { get { throw null; } } [System.Diagnostics.DebuggerStepThroughAttribute] - public System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat) { throw null; } + public System.Threading.Tasks.Task StartAsync(System.Uri url, Microsoft.AspNetCore.Connections.TransferFormat transferFormat, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task StopAsync() { throw null; } } diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs index 232c8c5c3b..fdbecdef64 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs @@ -188,11 +188,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Client { using (_logger.BeginScope(_logScope)) { - await StartAsyncCore(transferFormat).ForceAsync(); + await StartAsyncCore(transferFormat, cancellationToken).ForceAsync(); } } - private async Task StartAsyncCore(TransferFormat transferFormat) + private async Task StartAsyncCore(TransferFormat transferFormat, CancellationToken cancellationToken) { CheckDisposed(); @@ -215,7 +215,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client Log.Starting(_logger); - await SelectAndStartTransport(transferFormat); + await SelectAndStartTransport(transferFormat, cancellationToken); _started = true; Log.Started(_logger); @@ -288,7 +288,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client } } - private async Task SelectAndStartTransport(TransferFormat transferFormat) + private async Task SelectAndStartTransport(TransferFormat transferFormat, CancellationToken cancellationToken) { var uri = _httpConnectionOptions.Url; // Set the initial access token provider back to the original one from options @@ -301,7 +301,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client if (_httpConnectionOptions.Transports == HttpTransportType.WebSockets) { Log.StartingTransport(_logger, _httpConnectionOptions.Transports, uri); - await StartTransport(uri, _httpConnectionOptions.Transports, transferFormat); + await StartTransport(uri, _httpConnectionOptions.Transports, transferFormat, cancellationToken); } else { @@ -315,7 +315,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client do { - negotiationResponse = await GetNegotiationResponseAsync(uri); + negotiationResponse = await GetNegotiationResponseAsync(uri, cancellationToken); if (negotiationResponse.Url != null) { @@ -379,12 +379,12 @@ namespace Microsoft.AspNetCore.Http.Connections.Client // The negotiation response gets cleared in the fallback scenario. if (negotiationResponse == null) { - negotiationResponse = await GetNegotiationResponseAsync(uri); + negotiationResponse = await GetNegotiationResponseAsync(uri, cancellationToken); connectUrl = CreateConnectUrl(uri, negotiationResponse.ConnectionId); } Log.StartingTransport(_logger, transportType, connectUrl); - await StartTransport(connectUrl, transportType, transferFormat); + await StartTransport(connectUrl, transportType, transferFormat, cancellationToken); break; } } @@ -414,7 +414,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client } } - private async Task NegotiateAsync(Uri url, HttpClient httpClient, ILogger logger) + private async Task NegotiateAsync(Uri url, HttpClient httpClient, ILogger logger, CancellationToken cancellationToken) { try { @@ -436,7 +436,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client // rather than buffer the entire response. This gives a small perf boost. // Note that it is important to dispose of the response when doing this to // avoid leaving the connection open. - using (var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) + using (var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken)) { response.EnsureSuccessStatusCode(); var responseBuffer = await response.Content.ReadAsByteArrayAsync(); @@ -467,7 +467,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client return Utils.AppendQueryString(url, "id=" + connectionId); } - private async Task StartTransport(Uri connectUrl, HttpTransportType transportType, TransferFormat transferFormat) + private async Task StartTransport(Uri connectUrl, HttpTransportType transportType, TransferFormat transferFormat, CancellationToken cancellationToken) { // Construct the transport var transport = _transportFactory.CreateTransport(transportType); @@ -475,7 +475,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client // Start the transport, giving it one end of the pipe try { - await transport.StartAsync(connectUrl, transferFormat); + await transport.StartAsync(connectUrl, transferFormat, cancellationToken); } catch (Exception ex) { @@ -604,9 +604,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Client #endif } - private async Task GetNegotiationResponseAsync(Uri uri) + private async Task GetNegotiationResponseAsync(Uri uri, CancellationToken cancellationToken) { - var negotiationResponse = await NegotiateAsync(uri, _httpClient, _logger); + var negotiationResponse = await NegotiateAsync(uri, _httpClient, _logger, cancellationToken); _connectionId = negotiationResponse.ConnectionId; _logScope.ConnectionId = _connectionId; return negotiationResponse; diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ITransport.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ITransport.cs index 8f133a2d5d..fe9d94c8bf 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ITransport.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ITransport.cs @@ -3,6 +3,7 @@ using System; using System.IO.Pipelines; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; @@ -10,7 +11,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal { public interface ITransport : IDuplexPipe { - Task StartAsync(Uri url, TransferFormat transferFormat); + Task StartAsync(Uri url, TransferFormat transferFormat, CancellationToken cancellationToken = default); Task StopAsync(); } } diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/LongPollingTransport.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/LongPollingTransport.cs index f2a92e71d3..9d953f0114 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/LongPollingTransport.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/LongPollingTransport.cs @@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); } - public async Task StartAsync(Uri url, TransferFormat transferFormat) + public async Task StartAsync(Uri url, TransferFormat transferFormat, CancellationToken cancellationToken = default) { if (transferFormat != TransferFormat.Binary && transferFormat != TransferFormat.Text) { @@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal // Make initial long polling request // Server uses first long polling request to finish initializing connection and it returns without data var request = new HttpRequestMessage(HttpMethod.Get, url); - using (var response = await _httpClient.SendAsync(request)) + using (var response = await _httpClient.SendAsync(request, cancellationToken)) { response.EnsureSuccessStatusCode(); } diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ServerSentEventsTransport.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ServerSentEventsTransport.cs index b0f3477a2b..d3bebe3160 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ServerSentEventsTransport.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ServerSentEventsTransport.cs @@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); } - public async Task StartAsync(Uri url, TransferFormat transferFormat) + public async Task StartAsync(Uri url, TransferFormat transferFormat, CancellationToken cancellationToken = default) { if (transferFormat != TransferFormat.Text) { @@ -62,7 +62,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal try { - response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None); + response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); response.EnsureSuccessStatusCode(); } catch diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs index 4fe1e61b73..749c85d8d2 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs @@ -89,7 +89,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal _accessTokenProvider = accessTokenProvider; } - public async Task StartAsync(Uri url, TransferFormat transferFormat) + public async Task StartAsync(Uri url, TransferFormat transferFormat, CancellationToken cancellationToken = default) { if (url == null) { @@ -121,7 +121,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal try { - await _webSocket.ConnectAsync(resolvedUrl, CancellationToken.None); + await _webSocket.ConnectAsync(resolvedUrl, cancellationToken); } catch { diff --git a/src/SignalR/common/Shared/AsyncEnumerableAdapters.cs b/src/SignalR/common/Shared/AsyncEnumerableAdapters.cs index 6ebd1dcb25..d997233e43 100644 --- a/src/SignalR/common/Shared/AsyncEnumerableAdapters.cs +++ b/src/SignalR/common/Shared/AsyncEnumerableAdapters.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -23,7 +24,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal return new CancelableTypedAsyncEnumerable(asyncEnumerable, cts); } - public static async IAsyncEnumerable MakeAsyncEnumerableFromChannel(ChannelReader channel, CancellationToken cancellationToken = default) + public static async IAsyncEnumerable MakeAsyncEnumerableFromChannel(ChannelReader channel, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var item in channel.ReadAllAsync(cancellationToken)) { diff --git a/src/SignalR/perf/benchmarkapps/Directory.Build.props b/src/SignalR/perf/benchmarkapps/BenchmarkServer/Directory.Build.props similarity index 100% rename from src/SignalR/perf/benchmarkapps/Directory.Build.props rename to src/SignalR/perf/benchmarkapps/BenchmarkServer/Directory.Build.props diff --git a/src/SignalR/perf/benchmarkapps/Directory.Build.targets b/src/SignalR/perf/benchmarkapps/BenchmarkServer/Directory.Build.targets similarity index 100% rename from src/SignalR/perf/benchmarkapps/Directory.Build.targets rename to src/SignalR/perf/benchmarkapps/BenchmarkServer/Directory.Build.targets diff --git a/src/SignalR/perf/benchmarkapps/BenchmarkServer/Startup.cs b/src/SignalR/perf/benchmarkapps/BenchmarkServer/Startup.cs index 040e73ae0f..139ec09d47 100644 --- a/src/SignalR/perf/benchmarkapps/BenchmarkServer/Startup.cs +++ b/src/SignalR/perf/benchmarkapps/BenchmarkServer/Startup.cs @@ -33,7 +33,7 @@ namespace BenchmarkServer } } - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseSignalR(routes => { diff --git a/src/SignalR/server/Core/ref/Microsoft.AspNetCore.SignalR.Core.csproj b/src/SignalR/server/Core/ref/Microsoft.AspNetCore.SignalR.Core.csproj index c09bfa6d9a..b49f53e40d 100644 --- a/src/SignalR/server/Core/ref/Microsoft.AspNetCore.SignalR.Core.csproj +++ b/src/SignalR/server/Core/ref/Microsoft.AspNetCore.SignalR.Core.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj index 03ffc0d44b..8d8194fbc7 100644 --- a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj +++ b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/SignalR/server/SignalR/test/EndToEndTests.cs b/src/SignalR/server/SignalR/test/EndToEndTests.cs index f1c951c57c..ae59219a94 100644 --- a/src/SignalR/server/SignalR/test/EndToEndTests.cs +++ b/src/SignalR/server/SignalR/test/EndToEndTests.cs @@ -568,7 +568,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } - public Task StartAsync(Uri url, TransferFormat transferFormat) + public Task StartAsync(Uri url, TransferFormat transferFormat, CancellationToken cancellationToken = default) { var options = ClientPipeOptions.DefaultOptions; var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs index 68d02497de..f99cb6c76e 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Channels; @@ -851,7 +852,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests return channel.Reader; } - public async IAsyncEnumerable CancelableStreamGeneratedAsyncEnumerable(CancellationToken token) + public async IAsyncEnumerable CancelableStreamGeneratedAsyncEnumerable([EnumeratorCancellation] CancellationToken token) { _tcsService.StartedMethod.SetResult(null); await token.WaitForCancellationAsync(); diff --git a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs index e1849607d3..b5cf8cfaa2 100644 --- a/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs +++ b/src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs @@ -109,7 +109,7 @@ namespace Microsoft.AspNetCore.Certificates.Generation.Tests } [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/6721 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/6721")] public void EnsureCreateHttpsCertificate2_CreatesACertificate_WhenThereAreNoHttpsCertificates() { try diff --git a/src/Tools/dotnet-watch/test/DotNetWatcherTests.cs b/src/Tools/dotnet-watch/test/DotNetWatcherTests.cs index 34cc22373f..5eb12ccb19 100644 --- a/src/Tools/dotnet-watch/test/DotNetWatcherTests.cs +++ b/src/Tools/dotnet-watch/test/DotNetWatcherTests.cs @@ -24,7 +24,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests } [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/8267 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/8267")] public async Task RunsWithDotnetWatchEnvVariable() { Assert.True(string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DOTNET_WATCH")), "DOTNET_WATCH cannot be set already when this test is running"); diff --git a/src/Tools/dotnet-watch/test/GlobbingAppTests.cs b/src/Tools/dotnet-watch/test/GlobbingAppTests.cs index c964bfe8c0..f85ef3bfcf 100644 --- a/src/Tools/dotnet-watch/test/GlobbingAppTests.cs +++ b/src/Tools/dotnet-watch/test/GlobbingAppTests.cs @@ -87,7 +87,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests } [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/8267 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/8267")] public async Task ChangeExcludedFile() { await _app.StartWatcherAsync(); @@ -101,7 +101,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests } [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/8267 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/8267")] public async Task ListsFiles() { await _app.PrepareAsync(); diff --git a/src/Tools/dotnet-watch/test/NoDepsAppTests.cs b/src/Tools/dotnet-watch/test/NoDepsAppTests.cs index 29c2ed8739..eb5bd6ff7e 100644 --- a/src/Tools/dotnet-watch/test/NoDepsAppTests.cs +++ b/src/Tools/dotnet-watch/test/NoDepsAppTests.cs @@ -44,7 +44,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests } [ConditionalFact] - [SkipOnHelix] // https://github.com/aspnet/AspNetCore/issues/8267 + [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/8267")] public async Task RestartProcessThatTerminatesAfterFileChange() { await _app.StartWatcherAsync();