diff --git a/.azure/pipelines/fast-pr-validation.yml b/.azure/pipelines/fast-pr-validation.yml index 72b06367b6..44029cecb3 100644 --- a/.azure/pipelines/fast-pr-validation.yml +++ b/.azure/pipelines/fast-pr-validation.yml @@ -14,15 +14,22 @@ resources: phases: - template: .vsts-pipelines/templates/project-ci.yml@buildtools parameters: - buildArgs: "/t:CheckUniverse" -- phase: DataProtection - queue: Hosted VS2017 + buildArgs: "/t:FastCheck" +- phase: RepoBuilds + queue: + name: Hosted VS2017 + parallel: 2 + matrix: + DataProtection: + _FolderName: DataProtection + WebSockets: + _FolderName: WebSockets steps: - - script: src/DataProtection/build.cmd -ci - displayName: Run src/DataProtection/build.cmd + - script: src/$(_FolderName)/build.cmd -ci + displayName: Run src/$(_FolderName)/build.cmd - task: PublishTestResults@2 displayName: Publish test results condition: always() inputs: testRunner: vstest - testResultsFiles: 'src/DataProtection/artifacts/logs/**/*.trx' + testResultsFiles: 'src/$(_FolderName)/artifacts/logs/**/*.trx' diff --git a/.gitmodules b/.gitmodules index afcd3794ac..219a68ff6f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -174,7 +174,3 @@ path = modules/Templating url = https://github.com/aspnet/Templating.git branch = release/2.2 -[submodule "modules/WebSockets"] - path = modules/WebSockets - url = https://github.com/aspnet/WebSockets.git - branch = release/2.2 diff --git a/Directory.Build.props b/Directory.Build.props index 531b5d8745..813f3db1f2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ Microsoft ASP.NET Core $(MSBuildThisFileDirectory) - https://github.com/aspnet/Universe + https://github.com/aspnet/AspNetCore git $(MSBuildThisFileDirectory)eng\AspNetCore.snk true diff --git a/README.md b/README.md index 149678d69e..37b5b7eb18 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Universe +ASP.NET Core ======== Build infrastructure used to produce the whole ASP.NET Core stack. @@ -102,8 +102,8 @@ RedHat/Fedora (x64) | [Installer (rpm)][redhat-x64-rpm] ## Building from source ``` -git clone --recursive https://github.com/aspnet/Universe.git -cd Universe +git clone --recursive https://github.com/aspnet/AspNetCore.git +cd AspNetCore ./build.cmd ``` diff --git a/build/PackageArchive.targets b/build/PackageArchive.targets index bb892d102c..5ba1cadcf8 100644 --- a/build/PackageArchive.targets +++ b/build/PackageArchive.targets @@ -1,5 +1,5 @@ - + diff --git a/build/RepositoryBuild.targets b/build/RepositoryBuild.targets index fe2cca84d6..ce72b5543e 100644 --- a/build/RepositoryBuild.targets +++ b/build/RepositoryBuild.targets @@ -104,7 +104,7 @@ - + diff --git a/build/buildorder.props b/build/buildorder.props index 8fe02de59a..f40a356312 100644 --- a/build/buildorder.props +++ b/build/buildorder.props @@ -39,7 +39,7 @@ - + diff --git a/build/lineups/Internal.AspNetCore.Universe.Lineup.nuspec b/build/lineups/Internal.AspNetCore.Universe.Lineup.nuspec index cc35b59699..1209016b04 100644 --- a/build/lineups/Internal.AspNetCore.Universe.Lineup.nuspec +++ b/build/lineups/Internal.AspNetCore.Universe.Lineup.nuspec @@ -4,7 +4,7 @@ Internal.AspNetCore.Universe.Lineup $version$ Microsoft - This package used to unify ASP.NET Core package versions across all Universe repos. Internal use only. + This package used to unify ASP.NET Core package versions across all ASP.NET Core repos. Internal use only. diff --git a/build/repo.beforecommon.props b/build/repo.beforecommon.props index 0550358981..f470e9b2a6 100644 --- a/build/repo.beforecommon.props +++ b/build/repo.beforecommon.props @@ -2,7 +2,7 @@ true diff --git a/build/repo.targets b/build/repo.targets index 74cab3b281..d6c00978c3 100644 --- a/build/repo.targets +++ b/build/repo.targets @@ -13,7 +13,7 @@ $(IntermediateDir)branding.g.props SetTeamCityBuildNumberToVersion;$(PrepareDependsOn);VerifyPackageArtifactConfig;VerifyExternalDependencyConfig;PrepareOutputPaths - $(CleanDependsOn);CleanArtifacts;CleanUniverseArtifacts + $(CleanDependsOn);CleanArtifacts;CleanRepoArtifacts $(RestoreDependsOn);InstallDotNet $(CompileDependsOn);BuildRepositories $(PackageDependsOn);BuildMetapackages;CheckExpectedPackagesExist @@ -78,7 +78,7 @@ - + - + @@ -261,7 +261,7 @@ Condition=" @(ExternalDependency->WithMetadataValue('Version', '')->Count()) != 0 " /> - diff --git a/build/submodules.props b/build/submodules.props index ce2e4d2b30..a772043618 100644 --- a/build/submodules.props +++ b/build/submodules.props @@ -78,7 +78,7 @@ - + diff --git a/dockerbuild.sh b/dockerbuild.sh index d401522921..ccc55b8852 100755 --- a/dockerbuild.sh +++ b/dockerbuild.sh @@ -88,7 +88,7 @@ if ! __machine_has docker; then fi dockerfile="$DIR/build/docker/$image.Dockerfile" -tagname="universe-build-$image" +tagname="aspnetcore-build-$image" docker build "$(dirname "$dockerfile")" \ --build-arg "USER=$(whoami)" \ diff --git a/docs/Submodules.md b/docs/Submodules.md index 501e7884d3..8d24c40ff5 100644 --- a/docs/Submodules.md +++ b/docs/Submodules.md @@ -9,10 +9,10 @@ For full information, see the [official docs for git submodules](https://git-scm ## Fundamental concept -The parent repo (aspnet/Universe) stores two pieces of info about each submodule. +The parent repo (aspnet/AspNetCore) stores two pieces of info about each submodule. 1. Where to clone the submodule from. This is stored in the .gitmodules file -2. The commit hash of the submodule to use. +2. The commit hash of the submodule to use. This means you cannot commit a submodule's branch or a tag to the parent repo. Other info may appear in the .gitmodules file, but it is only used when attempting to @@ -22,7 +22,7 @@ Other info may appear in the .gitmodules file, but it is only used when attempti By default, submodules will not be present. Use `--recursive` to clone all submodules. - git clone https://github.com/aspnet/Universe.git --recursive + git clone https://github.com/aspnet/AspNetCore.git --recursive If you have already cloned, run this to initialize all submodules. @@ -53,7 +53,7 @@ Updating all submodules to newer versions can be done like this. Updating just one subumodule. git submodule update --remote modules/EntityFrameworkCore/ - + This uses the remote url and branch info configuration stored in .gitmodules to pull new commits. This does not guarantee the commit is going to be a fast-forward commit. @@ -72,7 +72,7 @@ that contains the new commit. git add modules/KestrelhttpServer/ git commit -m "Update Kestrel to latest version" -## PowerShell is slow in aspnet/Universe +## PowerShell is slow in aspnet/AspNetCore Many users have post-git, and extension that shows git status on the prompt line. Because `git status` with submodules on Windows is very slow, it can make PowerShell unbearable to use. diff --git a/modules/WebSockets b/modules/WebSockets deleted file mode 160000 index a036f920b8..0000000000 --- a/modules/WebSockets +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a036f920b8f32446e3524d3eb5a10fbd02f2a37c diff --git a/scripts/GenerateTags.ps1 b/scripts/GenerateTags.ps1 index a0dc354f44..e797f15ad4 100755 --- a/scripts/GenerateTags.ps1 +++ b/scripts/GenerateTags.ps1 @@ -87,12 +87,12 @@ if (-not $PSCmdlet.ShouldContinue("Continue?", "This will apply tags to all subm } -$universeTag = Get-PackageVersion $repoRoot -New-GitTag $repoRoot $universeTag -WhatIf:$WhatIfPreference +$repoTag = Get-PackageVersion $repoRoot +New-GitTag $repoRoot $repoTag -WhatIf:$WhatIfPreference $tags = @([pscustomobject] @{ repo = $(git config remote.origin.url) - tag = $universeTag + tag = $repoTag commit = $(git rev-parse HEAD) }) @@ -106,8 +106,8 @@ Get-Submodules $repoRoot | ForEach-Object { try { $tag = Get-PackageVersion $_.path - if ($tag -ne $universeTag) { - Write-Warning "${module}: version ($tag) does not match universe ($universeTag)" + if ($tag -ne $repoTag) { + Write-Warning "${module}: version ($tag) does not match repo ($repoTag)" } $tags += [pscustomobject] @{ repo = $_.remote diff --git a/scripts/UpdateRepos.ps1 b/scripts/UpdateRepos.ps1 index 69d23232d8..6db78658b2 100755 --- a/scripts/UpdateRepos.ps1 +++ b/scripts/UpdateRepos.ps1 @@ -2,7 +2,7 @@ <# .SYNOPSIS - Updates each repo Universe builds to new dependencies.props. + Updates each submodule this repo builds to new dependencies.props. .PARAMETER Source The NuGet package source to find the lineup on. .PARAMETER LineupID @@ -71,10 +71,10 @@ try { $koreBuildLock = "korebuild-lock.txt" - $universeKoreBuildLock = (Join-Path $RepoRoot $koreBuildLock) + $repoKoreBuildLock = (Join-Path $RepoRoot $koreBuildLock) $submoduleKoreBuildLock = (Join-Path $submodule.path $koreBuildLock) - Copy-Item $universeKoreBuildLock $submoduleKoreBuildLock -Force + Copy-Item $repoKoreBuildLock $submoduleKoreBuildLock -Force Write-Verbose "About to update dependencies.props for $($submodule.module)" & .\run.ps1 upgrade deps --source $Source --id $LineupID --version $LineupVersion --deps-file $depsFile diff --git a/scripts/common.psm1 b/scripts/common.psm1 index 5225c6d7b8..c547cba9a9 100644 --- a/scripts/common.psm1 +++ b/scripts/common.psm1 @@ -161,7 +161,7 @@ function CreatePR( [string]$gitHubToken) { $hubLocation = Ensure-Hub - Invoke-Block { git push -f https://$gitHubToken@github.com/$headFork/Universe.git $destinationBranch } + Invoke-Block { git push -f https://$gitHubToken@github.com/$headFork/AspNetCore.git $destinationBranch } & $hubLocation pull-request -f -b "${baseFork}:$baseBranch" -h "${headFork}:$destinationBranch" -m $body } diff --git a/src/WebSockets/.gitignore b/src/WebSockets/.gitignore new file mode 100644 index 0000000000..0e10f4aa54 --- /dev/null +++ b/src/WebSockets/.gitignore @@ -0,0 +1,31 @@ +[Oo]bj/ +[Bb]in/ +TestResults/ +.nuget/ +_ReSharper.*/ +packages/ +artifacts/ +PublishProfiles/ +*.user +*.suo +*.cache +*.docstates +_ReSharper.* +nuget.exe +*net45.csproj +*k10.csproj +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution +*.*sdf +*.ipch +*.sln.ide +/.vs/ +.testPublish/ +.build/ +autobahnreports/ +.vscode/ +global.json diff --git a/src/WebSockets/Directory.Build.props b/src/WebSockets/Directory.Build.props new file mode 100644 index 0000000000..08744cc42e --- /dev/null +++ b/src/WebSockets/Directory.Build.props @@ -0,0 +1,19 @@ + + + + + + + + + Microsoft ASP.NET Core + https://github.com/aspnet/WebSockets + git + $(MSBuildThisFileDirectory) + $(MSBuildThisFileDirectory)build\Key.snk + true + true + + diff --git a/src/WebSockets/Directory.Build.targets b/src/WebSockets/Directory.Build.targets new file mode 100644 index 0000000000..78626b773e --- /dev/null +++ b/src/WebSockets/Directory.Build.targets @@ -0,0 +1,10 @@ + + + $(MicrosoftNETCoreApp20PackageVersion) + $(MicrosoftNETCoreApp21PackageVersion) + $(MicrosoftNETCoreApp22PackageVersion) + $(NETStandardLibrary20PackageVersion) + + 99.9 + + diff --git a/src/WebSockets/NuGetPackageVerifier.json b/src/WebSockets/NuGetPackageVerifier.json new file mode 100644 index 0000000000..b153ab1515 --- /dev/null +++ b/src/WebSockets/NuGetPackageVerifier.json @@ -0,0 +1,7 @@ +{ + "Default": { + "rules": [ + "DefaultCompositeRule" + ] + } +} \ No newline at end of file diff --git a/src/WebSockets/README.md b/src/WebSockets/README.md new file mode 100644 index 0000000000..c7c69ee89b --- /dev/null +++ b/src/WebSockets/README.md @@ -0,0 +1,19 @@ +WebSockets +================ + +AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/lk5hyg6gki03hdqe/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/WebSockets/branch/dev) + +Travis: [![Travis](https://travis-ci.org/aspnet/WebSockets.svg?branch=dev)](https://travis-ci.org/aspnet/WebSockets) + +Contains a managed implementation of the WebSocket protocol, along with server integration components. + +This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo. + + +## System Requirements + +This repo has a few special system requirements/prerequisites. + +1. Windows IIS Express tests require IIS Express 10 and Windows 8 for WebSockets support +2. HttpListener/ASP.NET 4.6 samples require at least Windows 8 +3. Autobahn Test Suite requires special installation see the README.md in test/AutobahnTestApp diff --git a/src/WebSockets/WebSockets.sln b/src/WebSockets/WebSockets.sln new file mode 100644 index 0000000000..6977d2812e --- /dev/null +++ b/src/WebSockets/WebSockets.sln @@ -0,0 +1,145 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.10 +MinimumVisualStudioVersion = 15.0.26730.03 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2C7947A5-9FBD-4267-97C1-2D726D7B3BAF}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C45106D0-76C8-4776-A140-F7DD83CA2958}" + ProjectSection(SolutionItems) = preProject + test\Directory.Build.props = test\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{9E55FC5B-FD9C-4266-AB24-F3AA649D7C8B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestServer", "samples\TestServer\TestServer.csproj", "{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{19595D64-E42E-46FD-AB2E-BDC870724EE7}" + ProjectSection(SolutionItems) = preProject + scripts\UpdateCoreFxCode.ps1 = scripts\UpdateCoreFxCode.ps1 + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebSockets", "src\Microsoft.AspNetCore.WebSockets\Microsoft.AspNetCore.WebSockets.csproj", "{CDE16880-0374-46FA-8896-99F1B90B4B6F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebSockets.Test", "test\Microsoft.AspNetCore.WebSockets.Test\Microsoft.AspNetCore.WebSockets.Test.csproj", "{5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebSockets.ConformanceTest", "test\Microsoft.AspNetCore.WebSockets.ConformanceTest\Microsoft.AspNetCore.WebSockets.ConformanceTest.csproj", "{74F45408-1959-4FEE-9511-25D40F4913FD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EchoApp", "samples\EchoApp\EchoApp.csproj", "{421954B0-5C6B-4092-8D4D-EACA4CE60AFB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutobahnTestApp", "test\AutobahnTestApp\AutobahnTestApp.csproj", "{150DF5A8-87C6-42F7-8886-CE07BFD02FD2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{92CE12E6-E127-433B-96D3-164C0113EA17}" + ProjectSection(SolutionItems) = preProject + build\dependencies.props = build\dependencies.props + build\Key.snk = build\Key.snk + build\repo.props = build\repo.props + build\repo.targets = build\repo.targets + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7A963B09-471B-4D67-B5C0-6039AF0C39EE}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|x64.ActiveCfg = Debug|Any CPU + {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|x64.Build.0 = Debug|Any CPU + {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|x86.ActiveCfg = Debug|Any CPU + {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|x86.Build.0 = Debug|Any CPU + {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|Any CPU.Build.0 = Release|Any CPU + {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|x64.ActiveCfg = Release|Any CPU + {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|x64.Build.0 = Release|Any CPU + {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|x86.ActiveCfg = Release|Any CPU + {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|x86.Build.0 = Release|Any CPU + {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Debug|x64.ActiveCfg = Debug|Any CPU + {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Debug|x64.Build.0 = Debug|Any CPU + {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Debug|x86.ActiveCfg = Debug|Any CPU + {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Debug|x86.Build.0 = Debug|Any CPU + {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Release|Any CPU.Build.0 = Release|Any CPU + {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Release|x64.ActiveCfg = Release|Any CPU + {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Release|x64.Build.0 = Release|Any CPU + {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Release|x86.ActiveCfg = Release|Any CPU + {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Release|x86.Build.0 = Release|Any CPU + {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Debug|x64.Build.0 = Debug|Any CPU + {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Debug|x86.Build.0 = Debug|Any CPU + {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Release|Any CPU.Build.0 = Release|Any CPU + {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Release|x64.ActiveCfg = Release|Any CPU + {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Release|x64.Build.0 = Release|Any CPU + {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Release|x86.ActiveCfg = Release|Any CPU + {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Release|x86.Build.0 = Release|Any CPU + {74F45408-1959-4FEE-9511-25D40F4913FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74F45408-1959-4FEE-9511-25D40F4913FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74F45408-1959-4FEE-9511-25D40F4913FD}.Debug|x64.ActiveCfg = Debug|Any CPU + {74F45408-1959-4FEE-9511-25D40F4913FD}.Debug|x64.Build.0 = Debug|Any CPU + {74F45408-1959-4FEE-9511-25D40F4913FD}.Debug|x86.ActiveCfg = Debug|Any CPU + {74F45408-1959-4FEE-9511-25D40F4913FD}.Debug|x86.Build.0 = Debug|Any CPU + {74F45408-1959-4FEE-9511-25D40F4913FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74F45408-1959-4FEE-9511-25D40F4913FD}.Release|Any CPU.Build.0 = Release|Any CPU + {74F45408-1959-4FEE-9511-25D40F4913FD}.Release|x64.ActiveCfg = Release|Any CPU + {74F45408-1959-4FEE-9511-25D40F4913FD}.Release|x64.Build.0 = Release|Any CPU + {74F45408-1959-4FEE-9511-25D40F4913FD}.Release|x86.ActiveCfg = Release|Any CPU + {74F45408-1959-4FEE-9511-25D40F4913FD}.Release|x86.Build.0 = Release|Any CPU + {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Debug|x64.ActiveCfg = Debug|Any CPU + {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Debug|x64.Build.0 = Debug|Any CPU + {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Debug|x86.ActiveCfg = Debug|Any CPU + {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Debug|x86.Build.0 = Debug|Any CPU + {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Release|Any CPU.Build.0 = Release|Any CPU + {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Release|x64.ActiveCfg = Release|Any CPU + {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Release|x64.Build.0 = Release|Any CPU + {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Release|x86.ActiveCfg = Release|Any CPU + {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Release|x86.Build.0 = Release|Any CPU + {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Debug|x64.ActiveCfg = Debug|Any CPU + {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Debug|x64.Build.0 = Debug|Any CPU + {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Debug|x86.ActiveCfg = Debug|Any CPU + {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Debug|x86.Build.0 = Debug|Any CPU + {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Release|Any CPU.Build.0 = Release|Any CPU + {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Release|x64.ActiveCfg = Release|Any CPU + {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Release|x64.Build.0 = Release|Any CPU + {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Release|x86.ActiveCfg = Release|Any CPU + {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B} = {9E55FC5B-FD9C-4266-AB24-F3AA649D7C8B} + {CDE16880-0374-46FA-8896-99F1B90B4B6F} = {2C7947A5-9FBD-4267-97C1-2D726D7B3BAF} + {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF} = {C45106D0-76C8-4776-A140-F7DD83CA2958} + {74F45408-1959-4FEE-9511-25D40F4913FD} = {C45106D0-76C8-4776-A140-F7DD83CA2958} + {421954B0-5C6B-4092-8D4D-EACA4CE60AFB} = {9E55FC5B-FD9C-4266-AB24-F3AA649D7C8B} + {150DF5A8-87C6-42F7-8886-CE07BFD02FD2} = {C45106D0-76C8-4776-A140-F7DD83CA2958} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D3542868-F8C6-401B-8071-37FE3C981604} + EndGlobalSection +EndGlobal diff --git a/src/WebSockets/build.cmd b/src/WebSockets/build.cmd new file mode 100644 index 0000000000..f4169ea5e4 --- /dev/null +++ b/src/WebSockets/build.cmd @@ -0,0 +1,3 @@ +@ECHO OFF +SET RepoRoot="%~dp0..\.." +%RepoRoot%\build.cmd -LockFile %RepoRoot%\korebuild-lock.txt -Path %~dp0 %* diff --git a/src/WebSockets/build.sh b/src/WebSockets/build.sh new file mode 100755 index 0000000000..d5bb0cf631 --- /dev/null +++ b/src/WebSockets/build.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +repo_root="$DIR/../.." +"$repo_root/build.sh" --path "$DIR" --lockfile "$repo_root/korebuild-lock.txt" "$@" diff --git a/src/WebSockets/build/Key.snk b/src/WebSockets/build/Key.snk new file mode 100644 index 0000000000..e10e4889c1 Binary files /dev/null and b/src/WebSockets/build/Key.snk differ diff --git a/src/WebSockets/build/dependencies.props b/src/WebSockets/build/dependencies.props new file mode 100644 index 0000000000..6a1e9ab585 --- /dev/null +++ b/src/WebSockets/build/dependencies.props @@ -0,0 +1,34 @@ + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + 2.2.0-preview2-20181004.6 + 2.2.0-preview3-35425 + 2.2.0-preview3-35425 + 2.2.0-preview3-35425 + 2.2.0-preview3-35425 + 2.2.0-preview3-35425 + 0.6.0-preview3-35425 + 2.2.0-preview3-35425 + 2.2.0-preview3-35425 + 2.2.0-preview3-35425 + 2.2.0-preview3-35425 + 2.2.0-preview3-35425 + 2.2.0-preview3-35425 + 2.2.0-preview3-35425 + 2.2.0-preview3-35425 + 2.2.0-preview3-35425 + 2.2.0-preview3-35425 + 2.0.9 + 2.1.3 + 2.2.0-preview3-27001-02 + 15.6.1 + 2.0.3 + 4.5.1 + 2.3.1 + 2.4.0 + + + + diff --git a/src/WebSockets/build/repo.props b/src/WebSockets/build/repo.props new file mode 100644 index 0000000000..feea5ac9e6 --- /dev/null +++ b/src/WebSockets/build/repo.props @@ -0,0 +1,19 @@ + + + + + + + + + Internal.AspNetCore.Universe.Lineup + 2.2.0-* + https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json + + + + + + + + diff --git a/src/WebSockets/build/repo.targets b/src/WebSockets/build/repo.targets new file mode 100644 index 0000000000..1767331014 --- /dev/null +++ b/src/WebSockets/build/repo.targets @@ -0,0 +1,9 @@ + + + $(ArtifactsDir)autobahnreports\ + + + + + + diff --git a/src/WebSockets/build/setup-wstest.ps1 b/src/WebSockets/build/setup-wstest.ps1 new file mode 100644 index 0000000000..8571e77055 --- /dev/null +++ b/src/WebSockets/build/setup-wstest.ps1 @@ -0,0 +1,52 @@ +function has($cmd) { !!(Get-Command $cmd -ErrorAction SilentlyContinue) } + +# Download VCForPython27 if necessary +$VendorDir = Join-Path (Get-Location) "vendor" + +if(!(Test-Path $VendorDir)) { + mkdir $VendorDir +} + +$VirtualEnvDir = Join-Path $VendorDir "virtualenv"; +$ScriptsDir = Join-Path $VirtualEnvDir "Scripts" +$WsTest = Join-Path $ScriptsDir "wstest.exe" + +$VCPythonMsi = Join-Path $VendorDir "VCForPython27.msi" +if(!(Test-Path $VCPythonMsi)) { + Write-Host "Downloading VCForPython27.msi" + Invoke-WebRequest -Uri https://download.microsoft.com/download/7/9/6/796EF2E4-801B-4FC4-AB28-B59FBF6D907B/VCForPython27.msi -OutFile "$VCPythonMsi" +} +else { + Write-Host "Using VCForPython27.msi from Cache" +} + +# Install VCForPython27 +Write-Host "Installing VCForPython27" + +# Launch this way to ensure we wait for msiexec to complete. It's a Windows app so it won't block the console by default. +Start-Process msiexec "/i","$VCPythonMsi","/qn","/quiet","/norestart" -Wait + +Write-Host "Installed VCForPython27" + +# Install Python +if(!(has python)) { + choco install python2 +} + +if(!(has python)) { + throw "Failed to install python2" +} + +# Install virtualenv +pip install virtualenv + +# Make a virtualenv in .virtualenv +virtualenv $VirtualEnvDir + +& "$ScriptsDir\python" --version +& "$ScriptsDir\pip" --version + +# Install autobahn into the virtualenv +& "$ScriptsDir\pip" install autobahntestsuite + +Write-Host "Using wstest from: '$WsTest'" \ No newline at end of file diff --git a/src/WebSockets/build/setup-wstest.sh b/src/WebSockets/build/setup-wstest.sh new file mode 100755 index 0000000000..6c8800e23f --- /dev/null +++ b/src/WebSockets/build/setup-wstest.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +type -p python +python --version + +# Install local virtualenv +mkdir .python +cd .python +curl -OL https://pypi.python.org/packages/d4/0c/9840c08189e030873387a73b90ada981885010dd9aea134d6de30cd24cb8/virtualenv-15.1.0.tar.gz +tar xf virtualenv-15.1.0.tar.gz +cd .. + +# Make a virtualenv +python ./.python/virtualenv-15.1.0/virtualenv.py .virtualenv + +.virtualenv/bin/python --version +.virtualenv/bin/pip --version + +# Install autobahn into the virtualenv +.virtualenv/bin/pip install autobahntestsuite + +# We're done. The travis config has already established the path to WSTest should be within the virtualenv. +ls -l .virtualenv/bin +.virtualenv/bin/wstest --version \ No newline at end of file diff --git a/src/WebSockets/build/sources.props b/src/WebSockets/build/sources.props new file mode 100644 index 0000000000..9215df9751 --- /dev/null +++ b/src/WebSockets/build/sources.props @@ -0,0 +1,17 @@ + + + + + $(DotNetRestoreSources) + + $(RestoreSources); + https://dotnet.myget.org/F/dotnet-core/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; + + + $(RestoreSources); + https://api.nuget.org/v3/index.json; + + + diff --git a/src/WebSockets/samples/AutobahnTestAppAspNet4/AutobahnTestAppAspNet4.csproj.aspnet4 b/src/WebSockets/samples/AutobahnTestAppAspNet4/AutobahnTestAppAspNet4.csproj.aspnet4 new file mode 100644 index 0000000000..e778fb466b --- /dev/null +++ b/src/WebSockets/samples/AutobahnTestAppAspNet4/AutobahnTestAppAspNet4.csproj.aspnet4 @@ -0,0 +1,129 @@ + + + + + + + Debug + AnyCPU + + + 2.0 + {72E3AB32-682F-42AF-B7C7-0B777244FF11} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + AutobahnTestAppAspNet4 + AutobahnTestAppAspNet4 + v4.6.1 + true + + + + + + + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.1.0.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + Web.config + + + Web.config + + + + + + + + EchoSocket.ashx + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + + True + True + 29392 + / + http://localhost:29392 + True + http://localhost:29392/EchoSocket.ashx + False + False + + + False + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/src/WebSockets/samples/AutobahnTestAppAspNet4/EchoSocket.ashx b/src/WebSockets/samples/AutobahnTestAppAspNet4/EchoSocket.ashx new file mode 100644 index 0000000000..7d018e48ed --- /dev/null +++ b/src/WebSockets/samples/AutobahnTestAppAspNet4/EchoSocket.ashx @@ -0,0 +1 @@ +<%@ WebHandler Language="C#" CodeBehind="EchoSocket.ashx.cs" Class="AutobahnTestAppAspNet4.EchoSocket" %> diff --git a/src/WebSockets/samples/AutobahnTestAppAspNet4/EchoSocket.ashx.cs b/src/WebSockets/samples/AutobahnTestAppAspNet4/EchoSocket.ashx.cs new file mode 100644 index 0000000000..0b001f8715 --- /dev/null +++ b/src/WebSockets/samples/AutobahnTestAppAspNet4/EchoSocket.ashx.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using System.Web; + +namespace AutobahnTestAppAspNet4 +{ + /// + /// Summary description for EchoSocket + /// + public class EchoSocket : IHttpHandler + { + public bool IsReusable => false; + + public void ProcessRequest(HttpContext context) + { + if (context.IsWebSocketRequest) + { + context.AcceptWebSocketRequest(async socketContext => + { + await Echo(socketContext.WebSocket); + }); + } + else + { + context.Response.Write("Ready to accept WebSocket request at: " + context.Request.Url.ToString().Replace("https://", "wss://").Replace("http://", "ws://")); + context.Response.Flush(); + } + } + + private async Task Echo(WebSocket webSocket) + { + var buffer = new byte[1024 * 4]; + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + } +} \ No newline at end of file diff --git a/src/WebSockets/samples/AutobahnTestAppAspNet4/Properties/AssemblyInfo.cs b/src/WebSockets/samples/AutobahnTestAppAspNet4/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..22e5a4f43d --- /dev/null +++ b/src/WebSockets/samples/AutobahnTestAppAspNet4/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AutobahnTestAppAspNet4")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AutobahnTestAppAspNet4")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("72e3ab32-682f-42af-b7c7-0b777244ff11")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/WebSockets/samples/AutobahnTestAppAspNet4/Web.Debug.config b/src/WebSockets/samples/AutobahnTestAppAspNet4/Web.Debug.config new file mode 100644 index 0000000000..2e302f9f95 --- /dev/null +++ b/src/WebSockets/samples/AutobahnTestAppAspNet4/Web.Debug.config @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/WebSockets/samples/AutobahnTestAppAspNet4/Web.Release.config b/src/WebSockets/samples/AutobahnTestAppAspNet4/Web.Release.config new file mode 100644 index 0000000000..c35844462b --- /dev/null +++ b/src/WebSockets/samples/AutobahnTestAppAspNet4/Web.Release.config @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/WebSockets/samples/AutobahnTestAppAspNet4/Web.config b/src/WebSockets/samples/AutobahnTestAppAspNet4/Web.config new file mode 100644 index 0000000000..9500f90d67 --- /dev/null +++ b/src/WebSockets/samples/AutobahnTestAppAspNet4/Web.config @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/WebSockets/samples/AutobahnTestAppAspNet4/packages.config b/src/WebSockets/samples/AutobahnTestAppAspNet4/packages.config new file mode 100644 index 0000000000..5f629e51b2 --- /dev/null +++ b/src/WebSockets/samples/AutobahnTestAppAspNet4/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/WebSockets/samples/AutobahnTestAppAspNet4/wstest-spec.json b/src/WebSockets/samples/AutobahnTestAppAspNet4/wstest-spec.json new file mode 100644 index 0000000000..d11521b04b --- /dev/null +++ b/src/WebSockets/samples/AutobahnTestAppAspNet4/wstest-spec.json @@ -0,0 +1,14 @@ +{ + "options": { "failByDrop": false }, + "outdir": "./bin/wstest-report", + "servers": [ + { + "agent": "ASP.NET 4", + "url": "ws://localhost:29392/EchoSocket.ashx", + "options": { "version": 18 } + } + ], + "cases": ["*"], + "exclude-cases": ["12.*", "13.*"], + "exclude-agent-cases": {} +} \ No newline at end of file diff --git a/src/WebSockets/samples/AutobahnTestAppHttpListener/App.config b/src/WebSockets/samples/AutobahnTestAppHttpListener/App.config new file mode 100644 index 0000000000..731f6de6c2 --- /dev/null +++ b/src/WebSockets/samples/AutobahnTestAppHttpListener/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/WebSockets/samples/AutobahnTestAppHttpListener/AutobahnTestAppHttpListener.csproj.net461 b/src/WebSockets/samples/AutobahnTestAppHttpListener/AutobahnTestAppHttpListener.csproj.net461 new file mode 100644 index 0000000000..943e30e4c3 --- /dev/null +++ b/src/WebSockets/samples/AutobahnTestAppHttpListener/AutobahnTestAppHttpListener.csproj.net461 @@ -0,0 +1,60 @@ + + + + + Debug + AnyCPU + {B7246F23-6A4B-492F-AB61-292AA1A9E9D5} + Exe + Properties + AutobahnTestAppHttpListener + AutobahnTestAppHttpListener + v4.6.1 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/WebSockets/samples/AutobahnTestAppHttpListener/Program.cs b/src/WebSockets/samples/AutobahnTestAppHttpListener/Program.cs new file mode 100644 index 0000000000..3c965d1be6 --- /dev/null +++ b/src/WebSockets/samples/AutobahnTestAppHttpListener/Program.cs @@ -0,0 +1,106 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +namespace AutobahnTestAppHttpListener +{ + class Program + { + // This app only works on Windows 8+ + static int Main(string[] args) + { + using (var listener = StartListener()) + { + if (listener == null) + { + return 1; + } + + var httpUrl = listener.Prefixes.Single(); + var wsUrl = httpUrl.Replace("http://", "ws://"); + + var stopTokenSource = new CancellationTokenSource(); + var task = Run(listener, wsUrl, stopTokenSource.Token); + + Console.CancelKeyPress += (sender, a) => + { + a.Cancel = true; + stopTokenSource.Cancel(); + }; + + Console.WriteLine($"HTTP: {httpUrl}"); + Console.WriteLine($"WS : {wsUrl}"); + Console.WriteLine("Press Ctrl-C to stop..."); + + task.Wait(); + } + return 0; + } + + private static async Task Run(HttpListener listener, string wsUrl, CancellationToken stopToken) + { + while (!stopToken.IsCancellationRequested) + { + try + { + var context = await listener.GetContextAsync(); + + if (context.Request.IsWebSocketRequest) + { + var socket = await context.AcceptWebSocketAsync(null); + await Echo(socket.WebSocket); + } + else + { + using (var writer = new StreamWriter(context.Response.OutputStream)) + { + await writer.WriteLineAsync($"Ready to accept WebSocket request at: {wsUrl}"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Request failed: {ex}"); + } + } + } + + private static async Task Echo(WebSocket webSocket) + { + var buffer = new byte[1024 * 4]; + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + + static HttpListener StartListener() + { + var port = 49152; // IANA recommends starting at port 49152 for dynamic ports + while (port < 65535) + { + HttpListener listener = new HttpListener(); + listener.Prefixes.Add($"http://localhost:{port}/"); + try + { + listener.Start(); + return listener; + } + catch + { + port++; + } + } + + Console.Error.WriteLine("Failed to find a free port!"); + return null; + } + } +} diff --git a/src/WebSockets/samples/AutobahnTestAppHttpListener/Properties/AssemblyInfo.cs b/src/WebSockets/samples/AutobahnTestAppHttpListener/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f442cae407 --- /dev/null +++ b/src/WebSockets/samples/AutobahnTestAppHttpListener/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AutobahnTestAppHttpListener")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AutobahnTestAppHttpListener")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b7246f23-6a4b-492f-ab61-292aa1a9e9d5")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/WebSockets/samples/EchoApp/EchoApp.csproj b/src/WebSockets/samples/EchoApp/EchoApp.csproj new file mode 100644 index 0000000000..fc19ef1f0c --- /dev/null +++ b/src/WebSockets/samples/EchoApp/EchoApp.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp2.2;net461 + + + + + + + + + + + + + + + diff --git a/src/WebSockets/samples/EchoApp/Program.cs b/src/WebSockets/samples/EchoApp/Program.cs new file mode 100644 index 0000000000..2775a095ce --- /dev/null +++ b/src/WebSockets/samples/EchoApp/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; + +namespace EchoApp +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/src/WebSockets/samples/EchoApp/Properties/launchSettings.json b/src/WebSockets/samples/EchoApp/Properties/launchSettings.json new file mode 100644 index 0000000000..fba441af3d --- /dev/null +++ b/src/WebSockets/samples/EchoApp/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:62225/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "EchoApp": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/WebSockets/samples/EchoApp/Startup.cs b/src/WebSockets/samples/EchoApp/Startup.cs new file mode 100644 index 0000000000..729716693c --- /dev/null +++ b/src/WebSockets/samples/EchoApp/Startup.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace EchoApp +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(LogLevel.Debug); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseWebSockets(); + + app.Use(async (context, next) => + { + if (context.WebSockets.IsWebSocketRequest) + { + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await Echo(context, webSocket, loggerFactory.CreateLogger("Echo")); + } + else + { + await next(); + } + }); + + app.UseFileServer(); + } + + private async Task Echo(HttpContext context, WebSocket webSocket, ILogger logger) + { + var buffer = new byte[1024 * 4]; + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + LogFrame(logger, result, buffer); + while (!result.CloseStatus.HasValue) + { + // If the client send "ServerClose", then they want a server-originated close to occur + string content = "<>"; + if (result.MessageType == WebSocketMessageType.Text) + { + content = Encoding.UTF8.GetString(buffer, 0, result.Count); + if (content.Equals("ServerClose")) + { + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing from Server", CancellationToken.None); + logger.LogDebug($"Sent Frame Close: {WebSocketCloseStatus.NormalClosure} Closing from Server"); + return; + } + else if (content.Equals("ServerAbort")) + { + context.Abort(); + } + } + + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + logger.LogDebug($"Sent Frame {result.MessageType}: Len={result.Count}, Fin={result.EndOfMessage}: {content}"); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + LogFrame(logger, result, buffer); + } + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + + private void LogFrame(ILogger logger, WebSocketReceiveResult frame, byte[] buffer) + { + var close = frame.CloseStatus != null; + string message; + if (close) + { + message = $"Close: {frame.CloseStatus.Value} {frame.CloseStatusDescription}"; + } + else + { + string content = "<>"; + if (frame.MessageType == WebSocketMessageType.Text) + { + content = Encoding.UTF8.GetString(buffer, 0, frame.Count); + } + message = $"{frame.MessageType}: Len={frame.Count}, Fin={frame.EndOfMessage}: {content}"; + } + logger.LogDebug("Received Frame " + message); + } + } +} diff --git a/src/WebSockets/samples/EchoApp/wwwroot/index.html b/src/WebSockets/samples/EchoApp/wwwroot/index.html new file mode 100644 index 0000000000..1663600a5e --- /dev/null +++ b/src/WebSockets/samples/EchoApp/wwwroot/index.html @@ -0,0 +1,151 @@ + + + + + + + + +

WebSocket Test Page

+

Ready to connect...

+
+ + + +
+
+ + + + +
+ +

Note: When connected to the default server (i.e. the server in the address bar ;)), the message "ServerClose" will cause the server to close the connection. Similarly, the message "ServerAbort" will cause the server to forcibly terminate the connection without a closing handshake

+ +

Communication Log

+ + + + + + + + + + +
FromToData
+ + + + \ No newline at end of file diff --git a/src/WebSockets/samples/TestServer/App.config b/src/WebSockets/samples/TestServer/App.config new file mode 100644 index 0000000000..731f6de6c2 --- /dev/null +++ b/src/WebSockets/samples/TestServer/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/WebSockets/samples/TestServer/Program.cs b/src/WebSockets/samples/TestServer/Program.cs new file mode 100644 index 0000000000..68e62bfdf6 --- /dev/null +++ b/src/WebSockets/samples/TestServer/Program.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace TestServer +{ + class Program + { + static void Main(string[] args) + { + RunEchoServer().Wait(); + } + + private static async Task RunEchoServer() + { + HttpListener listener = new HttpListener(); + listener.Prefixes.Add("http://localhost:12345/"); + listener.Start(); + Console.WriteLine("Started"); + + while (true) + { + HttpListenerContext context = listener.GetContext(); + if (!context.Request.IsWebSocketRequest) + { + context.Response.Close(); + continue; + } + Console.WriteLine("Accepted"); + + var wsContext = await context.AcceptWebSocketAsync(null); + var webSocket = wsContext.WebSocket; + + byte[] buffer = new byte[1024]; + WebSocketReceiveResult received = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + while (received.MessageType != WebSocketMessageType.Close) + { + Console.WriteLine($"Echoing {received.Count} bytes received in a {received.MessageType} message; Fin={received.EndOfMessage}"); + // Echo anything we receive + await webSocket.SendAsync(new ArraySegment(buffer, 0, received.Count), received.MessageType, received.EndOfMessage, CancellationToken.None); + + received = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + + await webSocket.CloseAsync(received.CloseStatus.Value, received.CloseStatusDescription, CancellationToken.None); + + webSocket.Dispose(); + Console.WriteLine("Finished"); + } + } + } +} diff --git a/src/WebSockets/samples/TestServer/Properties/AssemblyInfo.cs b/src/WebSockets/samples/TestServer/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..1a5999f7d8 --- /dev/null +++ b/src/WebSockets/samples/TestServer/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("TestServer")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TestServer")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ffe69337-e3b4-4625-8244-36bd609742ba")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/WebSockets/samples/TestServer/TestServer.csproj b/src/WebSockets/samples/TestServer/TestServer.csproj new file mode 100644 index 0000000000..b3e359eaef --- /dev/null +++ b/src/WebSockets/samples/TestServer/TestServer.csproj @@ -0,0 +1,58 @@ + + + + + Debug + AnyCPU + {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B} + Exe + Properties + TestServer + TestServer + v4.6.1 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + diff --git a/src/WebSockets/src/Directory.Build.props b/src/WebSockets/src/Directory.Build.props new file mode 100644 index 0000000000..1e0980f663 --- /dev/null +++ b/src/WebSockets/src/Directory.Build.props @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/ExtendedWebSocketAcceptContext.cs b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/ExtendedWebSocketAcceptContext.cs new file mode 100644 index 0000000000..23186d7eb5 --- /dev/null +++ b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/ExtendedWebSocketAcceptContext.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. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.WebSockets +{ + public class ExtendedWebSocketAcceptContext : WebSocketAcceptContext + { + public override string SubProtocol { get; set; } + public int? ReceiveBufferSize { get; set; } + public TimeSpan? KeepAliveInterval { get; set; } + } +} \ No newline at end of file diff --git a/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/Internal/Constants.cs b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/Internal/Constants.cs new file mode 100644 index 0000000000..ffc5db2c14 --- /dev/null +++ b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/Internal/Constants.cs @@ -0,0 +1,21 @@ +// 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.WebSockets.Internal +{ + public static class Constants + { + public static class Headers + { + public const string Upgrade = "Upgrade"; + public const string UpgradeWebSocket = "websocket"; + public const string Connection = "Connection"; + public const string ConnectionUpgrade = "Upgrade"; + public const string SecWebSocketKey = "Sec-WebSocket-Key"; + public const string SecWebSocketVersion = "Sec-WebSocket-Version"; + public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; + public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; + public const string SupportedVersion = "13"; + } + } +} diff --git a/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/Internal/HandshakeHelpers.cs b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/Internal/HandshakeHelpers.cs new file mode 100644 index 0000000000..c001e9318e --- /dev/null +++ b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/Internal/HandshakeHelpers.cs @@ -0,0 +1,121 @@ +// 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.Cryptography; +using System.Text; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.WebSockets.Internal +{ + internal static class HandshakeHelpers + { + /// + /// Gets request headers needed process the handshake on the server. + /// + public static readonly IEnumerable NeededHeaders = new[] + { + Constants.Headers.Upgrade, + Constants.Headers.Connection, + Constants.Headers.SecWebSocketKey, + Constants.Headers.SecWebSocketVersion + }; + + // Verify Method, Upgrade, Connection, version, key, etc.. + public static bool CheckSupportedWebSocketRequest(string method, IEnumerable> headers) + { + bool validUpgrade = false, validConnection = false, validKey = false, validVersion = false; + + if (!string.Equals("GET", method, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + foreach (var pair in headers) + { + if (string.Equals(Constants.Headers.Connection, pair.Key, StringComparison.OrdinalIgnoreCase)) + { + if (string.Equals(Constants.Headers.ConnectionUpgrade, pair.Value, StringComparison.OrdinalIgnoreCase)) + { + validConnection = true; + } + } + else if (string.Equals(Constants.Headers.Upgrade, pair.Key, StringComparison.OrdinalIgnoreCase)) + { + if (string.Equals(Constants.Headers.UpgradeWebSocket, pair.Value, StringComparison.OrdinalIgnoreCase)) + { + validUpgrade = true; + } + } + else if (string.Equals(Constants.Headers.SecWebSocketVersion, pair.Key, StringComparison.OrdinalIgnoreCase)) + { + if (string.Equals(Constants.Headers.SupportedVersion, pair.Value, StringComparison.OrdinalIgnoreCase)) + { + validVersion = true; + } + } + else if (string.Equals(Constants.Headers.SecWebSocketKey, pair.Key, StringComparison.OrdinalIgnoreCase)) + { + validKey = IsRequestKeyValid(pair.Value); + } + } + + return validConnection && validUpgrade && validVersion && validKey; + } + + public static void GenerateResponseHeaders(string key, string subProtocol, IHeaderDictionary headers) + { + headers[Constants.Headers.Connection] = Constants.Headers.ConnectionUpgrade; + headers[Constants.Headers.Upgrade] = Constants.Headers.UpgradeWebSocket; + headers[Constants.Headers.SecWebSocketAccept] = CreateResponseKey(key); + if (!string.IsNullOrWhiteSpace(subProtocol)) + { + headers[Constants.Headers.SecWebSocketProtocol] = subProtocol; + } + } + + /// + /// Validates the Sec-WebSocket-Key request header + /// + /// + /// + public static bool IsRequestKeyValid(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + try + { + byte[] data = Convert.FromBase64String(value); + return data.Length == 16; + } + catch (Exception) + { + return false; + } + } + + public static string CreateResponseKey(string requestKey) + { + // "The value of this header field is constructed by concatenating /key/, defined above in step 4 + // in Section 4.2.2, with the string "258EAFA5- E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of + // this concatenated value to obtain a 20-byte value and base64-encoding" + // https://tools.ietf.org/html/rfc6455#section-4.2.2 + + if (requestKey == null) + { + throw new ArgumentNullException(nameof(requestKey)); + } + + using (var algorithm = SHA1.Create()) + { + string merged = requestKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + byte[] mergedBytes = Encoding.UTF8.GetBytes(merged); + byte[] hashedBytes = algorithm.ComputeHash(mergedBytes); + return Convert.ToBase64String(hashedBytes); + } + } + } +} \ No newline at end of file diff --git a/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/Microsoft.AspNetCore.WebSockets.csproj b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/Microsoft.AspNetCore.WebSockets.csproj new file mode 100644 index 0000000000..277c0cc9fe --- /dev/null +++ b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/Microsoft.AspNetCore.WebSockets.csproj @@ -0,0 +1,19 @@ + + + + ASP.NET Core web socket middleware for use on top of opaque servers. + netstandard2.0 + $(NoWarn);CS1591 + true + true + aspnetcore + + + + + + + + + + diff --git a/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/WebSocketMiddleware.cs b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/WebSocketMiddleware.cs new file mode 100644 index 0000000000..35e7f99019 --- /dev/null +++ b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/WebSocketMiddleware.cs @@ -0,0 +1,166 @@ +// 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; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.WebSockets.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.WebSockets +{ + public class WebSocketMiddleware + { + private readonly RequestDelegate _next; + private readonly WebSocketOptions _options; + private readonly ILogger _logger; + private readonly bool _anyOriginAllowed; + private readonly List _allowedOrigins; + + public WebSocketMiddleware(RequestDelegate next, IOptions options, ILoggerFactory loggerFactory) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _next = next; + _options = options.Value; + _allowedOrigins = _options.AllowedOrigins.Select(o => o.ToLowerInvariant()).ToList(); + _anyOriginAllowed = _options.AllowedOrigins.Count == 0 || _options.AllowedOrigins.Contains("*", StringComparer.Ordinal); + + _logger = loggerFactory.CreateLogger(); + + // TODO: validate options. + } + + [Obsolete("This constructor has been replaced with an equivalent constructor which requires an ILoggerFactory.")] + public WebSocketMiddleware(RequestDelegate next, IOptions options) + : this(next, options, NullLoggerFactory.Instance) + { + } + + public Task Invoke(HttpContext context) + { + // Detect if an opaque upgrade is available. If so, add a websocket upgrade. + var upgradeFeature = context.Features.Get(); + if (upgradeFeature != null && context.Features.Get() == null) + { + var webSocketFeature = new UpgradeHandshake(context, upgradeFeature, _options); + context.Features.Set(webSocketFeature); + + if (!_anyOriginAllowed) + { + // Check for Origin header + var originHeader = context.Request.Headers[HeaderNames.Origin]; + + if (!StringValues.IsNullOrEmpty(originHeader) && webSocketFeature.IsWebSocketRequest) + { + // Check allowed origins to see if request is allowed + if (!_allowedOrigins.Contains(originHeader.ToString(), StringComparer.Ordinal)) + { + _logger.LogDebug("Request origin {Origin} is not in the list of allowed origins.", originHeader); + context.Response.StatusCode = StatusCodes.Status403Forbidden; + return Task.CompletedTask; + } + } + } + } + + return _next(context); + } + + private class UpgradeHandshake : IHttpWebSocketFeature + { + private readonly HttpContext _context; + private readonly IHttpUpgradeFeature _upgradeFeature; + private readonly WebSocketOptions _options; + private bool? _isWebSocketRequest; + + public UpgradeHandshake(HttpContext context, IHttpUpgradeFeature upgradeFeature, WebSocketOptions options) + { + _context = context; + _upgradeFeature = upgradeFeature; + _options = options; + } + + public bool IsWebSocketRequest + { + get + { + if (_isWebSocketRequest == null) + { + if (!_upgradeFeature.IsUpgradableRequest) + { + _isWebSocketRequest = false; + } + else + { + var headers = new List>(); + foreach (string headerName in HandshakeHelpers.NeededHeaders) + { + foreach (var value in _context.Request.Headers.GetCommaSeparatedValues(headerName)) + { + headers.Add(new KeyValuePair(headerName, value)); + } + } + _isWebSocketRequest = HandshakeHelpers.CheckSupportedWebSocketRequest(_context.Request.Method, headers); + } + } + return _isWebSocketRequest.Value; + } + } + + public async Task AcceptAsync(WebSocketAcceptContext acceptContext) + { + if (!IsWebSocketRequest) + { + throw new InvalidOperationException("Not a WebSocket request."); // TODO: LOC + } + + string subProtocol = null; + if (acceptContext != null) + { + subProtocol = acceptContext.SubProtocol; + } + + TimeSpan keepAliveInterval = _options.KeepAliveInterval; + int receiveBufferSize = _options.ReceiveBufferSize; + var advancedAcceptContext = acceptContext as ExtendedWebSocketAcceptContext; + if (advancedAcceptContext != null) + { + if (advancedAcceptContext.ReceiveBufferSize.HasValue) + { + receiveBufferSize = advancedAcceptContext.ReceiveBufferSize.Value; + } + if (advancedAcceptContext.KeepAliveInterval.HasValue) + { + keepAliveInterval = advancedAcceptContext.KeepAliveInterval.Value; + } + } + + string key = string.Join(", ", _context.Request.Headers[Constants.Headers.SecWebSocketKey]); + + HandshakeHelpers.GenerateResponseHeaders(key, subProtocol, _context.Response.Headers); + + Stream opaqueTransport = await _upgradeFeature.UpgradeAsync(); // Sets status code to 101 + + return WebSocketProtocol.CreateFromStream(opaqueTransport, isServer: true, subProtocol: subProtocol, keepAliveInterval: keepAliveInterval); + } + } + } +} diff --git a/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/WebSocketMiddlewareExtensions.cs b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/WebSocketMiddlewareExtensions.cs new file mode 100644 index 0000000000..403271f09a --- /dev/null +++ b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/WebSocketMiddlewareExtensions.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.WebSockets; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Builder +{ + public static class WebSocketMiddlewareExtensions + { + public static IApplicationBuilder UseWebSockets(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + + public static IApplicationBuilder UseWebSockets(this IApplicationBuilder app, WebSocketOptions options) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return app.UseMiddleware(Options.Create(options)); + } + } +} \ No newline at end of file diff --git a/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/WebSocketOptions.cs b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/WebSocketOptions.cs new file mode 100644 index 0000000000..da5f630d62 --- /dev/null +++ b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/WebSocketOptions.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 System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Configuration options for the WebSocketMiddleware + /// + public class WebSocketOptions + { + public WebSocketOptions() + { + KeepAliveInterval = TimeSpan.FromMinutes(2); + ReceiveBufferSize = 4 * 1024; + AllowedOrigins = new List(); + } + + /// + /// Gets or sets the frequency at which to send Ping/Pong keep-alive control frames. + /// The default is two minutes. + /// + public TimeSpan KeepAliveInterval { get; set; } + + /// + /// Gets or sets the size of the protocol buffer used to receive and parse frames. + /// The default is 4kb. + /// + public int ReceiveBufferSize { get; set; } + + /// + /// Set the Origin header values allowed for WebSocket requests to prevent Cross-Site WebSocket Hijacking. + /// By default all Origins are allowed. + /// + public IList AllowedOrigins { get; } + } +} \ No newline at end of file diff --git a/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/WebSocketsDependencyInjectionExtensions.cs b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/WebSocketsDependencyInjectionExtensions.cs new file mode 100644 index 0000000000..69b2da7eb0 --- /dev/null +++ b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/WebSocketsDependencyInjectionExtensions.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 Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.WebSockets +{ + public static class WebSocketsDependencyInjectionExtensions + { + public static IServiceCollection AddWebSockets(this IServiceCollection services, Action configure) + { + return services.Configure(configure); + } + } +} diff --git a/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/baseline.netcore.json b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/baseline.netcore.json new file mode 100644 index 0000000000..4a3d74e18c --- /dev/null +++ b/src/WebSockets/src/Microsoft.AspNetCore.WebSockets/baseline.netcore.json @@ -0,0 +1,231 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.WebSockets, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Builder.WebSocketMiddlewareExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseWebSockets", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseWebSockets", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Builder.WebSocketOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.WebSocketOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_KeepAliveInterval", + "Parameters": [], + "ReturnType": "System.TimeSpan", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_KeepAliveInterval", + "Parameters": [ + { + "Name": "value", + "Type": "System.TimeSpan" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ReceiveBufferSize", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ReceiveBufferSize", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebSockets.ExtendedWebSocketAcceptContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Http.WebSocketAcceptContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_SubProtocol", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SubProtocol", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ReceiveBufferSize", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ReceiveBufferSize", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_KeepAliveInterval", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_KeepAliveInterval", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.WebSockets.WebSocketMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/WebSockets/test/AutobahnTestApp/AutobahnTestApp.csproj b/src/WebSockets/test/AutobahnTestApp/AutobahnTestApp.csproj new file mode 100644 index 0000000000..8f470696bc --- /dev/null +++ b/src/WebSockets/test/AutobahnTestApp/AutobahnTestApp.csproj @@ -0,0 +1,29 @@ + + + + netcoreapp2.2 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/WebSockets/test/AutobahnTestApp/Program.cs b/src/WebSockets/test/AutobahnTestApp/Program.cs new file mode 100644 index 0000000000..ddd340a6e0 --- /dev/null +++ b/src/WebSockets/test/AutobahnTestApp/Program.cs @@ -0,0 +1,86 @@ +using System; +using System.IO; +using System.Net; +using System.Runtime.Loader; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace AutobahnTestApp +{ + public class Program + { + public static void Main(string[] args) + { + var scenarioName = "Unknown"; + var config = new ConfigurationBuilder() + .AddCommandLine(args) + .Build(); + + var builder = new WebHostBuilder() + .ConfigureLogging(loggingBuilder => loggingBuilder.AddConsole()) + .UseConfiguration(config) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup(); + + if (string.Equals(builder.GetSetting("server"), "Microsoft.AspNetCore.Server.HttpSys", System.StringComparison.Ordinal)) + { + scenarioName = "HttpSysServer"; + Console.WriteLine("Using HttpSys server"); + builder.UseHttpSys(); + } + else if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_PORT"))) + { + // ANCM is hosting the process. + // The port will not yet be configured at this point, but will also not require HTTPS. + scenarioName = "AspNetCoreModule"; + Console.WriteLine("Detected ANCM, using Kestrel"); + builder.UseKestrel(); + } + else + { + // Also check "server.urls" for back-compat. + var urls = builder.GetSetting(WebHostDefaults.ServerUrlsKey) ?? builder.GetSetting("server.urls"); + builder.UseSetting(WebHostDefaults.ServerUrlsKey, string.Empty); + + Console.WriteLine($"Using Kestrel, URL: {urls}"); + + if (urls.Contains(";")) + { + throw new NotSupportedException("This test app does not support multiple endpoints."); + } + + var uri = new Uri(urls); + + builder.UseKestrel(options => + { + options.Listen(IPAddress.Loopback, uri.Port, listenOptions => + { + if (uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + { + scenarioName = "Kestrel(SSL)"; + var certPath = Path.Combine(AppContext.BaseDirectory, "TestResources", "testCert.pfx"); + Console.WriteLine($"Using SSL with certificate: {certPath}"); + listenOptions.UseHttps(certPath, "testPassword"); + } + else + { + scenarioName = "Kestrel(NonSSL)"; + } + }); + }); + } + + var host = builder.Build(); + + AppDomain.CurrentDomain.UnhandledException += (_, a) => + { + Console.WriteLine($"Unhandled exception (Scenario: {scenarioName}): {a.ExceptionObject.ToString()}"); + }; + + Console.WriteLine($"Starting Server for Scenario: {scenarioName}"); + host.Run(); + } + } +} diff --git a/src/WebSockets/test/AutobahnTestApp/Properties/launchSettings.json b/src/WebSockets/test/AutobahnTestApp/Properties/launchSettings.json new file mode 100644 index 0000000000..85a2609ccc --- /dev/null +++ b/src/WebSockets/test/AutobahnTestApp/Properties/launchSettings.json @@ -0,0 +1,42 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:6155/", + "sslPort": 44371 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "ManagedSockets" + } + }, + "AutobahnTestApp": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "ManagedSockets" + } + }, + "AutobahnTestApp (SSL)": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "https://localhost:5443", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "ManagedSockets" + } + }, + "WebListener": { + "commandName": "Project", + "commandLineArgs": "--server Microsoft.AspNetCore.Server.HttpSys", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "ManagedSockets" + } + } + } +} \ No newline at end of file diff --git a/src/WebSockets/test/AutobahnTestApp/README.md b/src/WebSockets/test/AutobahnTestApp/README.md new file mode 100644 index 0000000000..6d76d57e64 --- /dev/null +++ b/src/WebSockets/test/AutobahnTestApp/README.md @@ -0,0 +1,14 @@ +# Autobahn Testing + +This application is used to provide the server for the [Autobahn Test Suite](http://autobahn.ws/testsuite) 'fuzzingclient' mode to test. It is a simple echo server that echos each frame received back to the client. + +In order to run these tests you must install CPython 2.7, Pip, and the test suite modules. You must also have +the `wstest` executable provided by the Autobahn Suite on the `PATH`. See http://autobahn.ws/testsuite/installation.html#installation for more info + +Once Autobahn is installed, launch this application in the desired configuration (in IIS Express, or using Kestrel directly) from Visual Studio and get the WebSocket URL from the HTTP response. Use that URL in place of `ws://server:1234` and invoke the `scripts\RunAutobahnTests.ps1` script in this project like so: + +``` +> .\scripts\RunAutobahnTests.ps1 -ServerUrl ws://server:1234 +``` + +By default, all cases are run and the report is written to the `autobahnreports` sub-directory of the directory in which you run the script. You can change either by using the `-Cases` and `-OutputDir` switches, use `.\script\RunAutobahnTests.ps1 -?` for help. diff --git a/src/WebSockets/test/AutobahnTestApp/Startup.cs b/src/WebSockets/test/AutobahnTestApp/Startup.cs new file mode 100644 index 0000000000..f244e76358 --- /dev/null +++ b/src/WebSockets/test/AutobahnTestApp/Startup.cs @@ -0,0 +1,52 @@ +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace AutobahnTestApp +{ + public class Startup + { + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + app.UseWebSockets(); + + var logger = loggerFactory.CreateLogger(); + app.Use(async (context, next) => + { + if (context.WebSockets.IsWebSocketRequest) + { + logger.LogInformation("Received WebSocket request"); + using (var webSocket = await context.WebSockets.AcceptWebSocketAsync()) + { + await Echo(webSocket, context.RequestAborted); + } + } + else + { + var wsScheme = context.Request.IsHttps ? "wss" : "ws"; + var wsUrl = $"{wsScheme}://{context.Request.Host.Host}:{context.Request.Host.Port}{context.Request.Path}"; + await context.Response.WriteAsync($"Ready to accept a WebSocket request at: {wsUrl}"); + } + }); + + } + + private async Task Echo(WebSocket webSocket, CancellationToken cancellationToken) + { + var buffer = new byte[1024 * 4]; + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancellationToken); + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); + } + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, cancellationToken); + } + } +} diff --git a/src/WebSockets/test/AutobahnTestApp/TestResources/testCert.pfx b/src/WebSockets/test/AutobahnTestApp/TestResources/testCert.pfx new file mode 100644 index 0000000000..7118908c2d Binary files /dev/null and b/src/WebSockets/test/AutobahnTestApp/TestResources/testCert.pfx differ diff --git a/src/WebSockets/test/AutobahnTestApp/TestResources/testCert.txt b/src/WebSockets/test/AutobahnTestApp/TestResources/testCert.txt new file mode 100644 index 0000000000..8771b78318 --- /dev/null +++ b/src/WebSockets/test/AutobahnTestApp/TestResources/testCert.txt @@ -0,0 +1,3 @@ +The password for this is 'testPassword' + +DO NOT EVER TRUST THIS CERT. The private key for it is publicly released. \ No newline at end of file diff --git a/src/WebSockets/test/AutobahnTestApp/scripts/RunAutobahnTests.ps1 b/src/WebSockets/test/AutobahnTestApp/scripts/RunAutobahnTests.ps1 new file mode 100644 index 0000000000..d109182eac --- /dev/null +++ b/src/WebSockets/test/AutobahnTestApp/scripts/RunAutobahnTests.ps1 @@ -0,0 +1,43 @@ +# +# RunAutobahnTests.ps1 +# +param([Parameter(Mandatory=$true)][string]$ServerUrl, [string[]]$Cases = @("*"), [string]$OutputDir, [int]$Iterations = 1) + +if(!(Get-Command wstest -ErrorAction SilentlyContinue)) { + throw "Missing required command 'wstest'. See README.md in Microsoft.AspNetCore.WebSockets.Server.Test project for information on installing Autobahn Test Suite." +} + +if(!$OutputDir) { + $OutputDir = Convert-Path "." + $OutputDir = Join-Path $OutputDir "autobahnreports" +} + +Write-Host "Launching Autobahn Test Suite ($Iterations iteration(s))..." + +0..($Iterations-1) | % { + $iteration = $_ + + $Spec = Convert-Path (Join-Path $PSScriptRoot "autobahn.spec.json") + + $CasesArray = [string]::Join(",", @($Cases | ForEach-Object { "`"$_`"" })) + + $SpecJson = [IO.File]::ReadAllText($Spec).Replace("OUTPUTDIR", $OutputDir.Replace("\", "\\")).Replace("WEBSOCKETURL", $ServerUrl).Replace("`"CASES`"", $CasesArray) + + $TempFile = [IO.Path]::GetTempFileName() + + try { + [IO.File]::WriteAllText($TempFile, $SpecJson) + $wstestOutput = & wstest -m fuzzingclient -s $TempFile + } finally { + if(Test-Path $TempFile) { + rm $TempFile + } + } + + $report = ConvertFrom-Json ([IO.File]::ReadAllText((Convert-Path (Join-Path $OutputDir "index.json")))) + + $report.Server | gm | ? { $_.MemberType -eq "NoteProperty" } | % { + $case = $report.Server."$($_.Name)" + Write-Host "[#$($iteration.ToString().PadRight(2))] [$($case.behavior.PadRight(6))] Case $($_.Name)" + } +} \ No newline at end of file diff --git a/src/WebSockets/test/AutobahnTestApp/scripts/autobahn.spec.json b/src/WebSockets/test/AutobahnTestApp/scripts/autobahn.spec.json new file mode 100644 index 0000000000..aa6d841167 --- /dev/null +++ b/src/WebSockets/test/AutobahnTestApp/scripts/autobahn.spec.json @@ -0,0 +1,14 @@ +{ + "options": { "failByDrop": false }, + "outdir": "OUTPUTDIR", + "servers": [ + { + "agent": "Server", + "url": "WEBSOCKETURL", + "options": { "version": 18 } + } + ], + "cases": ["CASES"], + "exclude-cases": ["12.*", "13.*"], + "exclude-agent-cases": {} +} diff --git a/src/WebSockets/test/Directory.Build.props b/src/WebSockets/test/Directory.Build.props new file mode 100644 index 0000000000..a84a332640 --- /dev/null +++ b/src/WebSockets/test/Directory.Build.props @@ -0,0 +1,22 @@ + + + + + netcoreapp2.2 + $(DeveloperBuildTestTfms) + + $(StandardTestTfms);net461 + + + + + true + + + + + + diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnCaseResult.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnCaseResult.cs new file mode 100644 index 0000000000..2cba13cf25 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnCaseResult.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn +{ + public class AutobahnCaseResult + { + public string Name { get; } + public string ActualBehavior { get; } + + public AutobahnCaseResult(string name, string actualBehavior) + { + Name = name; + ActualBehavior = actualBehavior; + } + + public static AutobahnCaseResult FromJson(JProperty prop) + { + var caseObj = (JObject)prop.Value; + var actualBehavior = (string)caseObj["behavior"]; + return new AutobahnCaseResult(prop.Name, actualBehavior); + } + + public bool BehaviorIs(params string[] behaviors) + { + return behaviors.Any(b => string.Equals(b, ActualBehavior, StringComparison.Ordinal)); + } + } +} \ No newline at end of file diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnExpectations.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnExpectations.cs new file mode 100644 index 0000000000..ea0667cf77 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnExpectations.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Server.IntegrationTesting; + +namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn +{ + public class AutobahnExpectations + { + private Dictionary _expectations = new Dictionary(); + public bool Ssl { get; } + public ServerType Server { get; } + public string Environment { get; } + + public AutobahnExpectations(ServerType server, bool ssl, string environment) + { + Server = server; + Ssl = ssl; + Environment = environment; + } + + public AutobahnExpectations Fail(params string[] caseSpecs) => Expect(Expectation.Fail, caseSpecs); + public AutobahnExpectations NonStrict(params string[] caseSpecs) => Expect(Expectation.NonStrict, caseSpecs); + public AutobahnExpectations OkOrFail(params string[] caseSpecs) => Expect(Expectation.OkOrFail, caseSpecs); + + public AutobahnExpectations Expect(Expectation expectation, params string[] caseSpecs) + { + foreach (var caseSpec in caseSpecs) + { + _expectations[caseSpec] = expectation; + } + return this; + } + + internal void Verify(AutobahnServerResult serverResult, StringBuilder failures) + { + foreach (var caseResult in serverResult.Cases) + { + // If this is an informational test result, we can't compare it to anything + if (!string.Equals(caseResult.ActualBehavior, "INFORMATIONAL", StringComparison.Ordinal)) + { + Expectation expectation; + if (!_expectations.TryGetValue(caseResult.Name, out expectation)) + { + expectation = Expectation.Ok; + } + + switch (expectation) + { + case Expectation.Fail: + if (!caseResult.BehaviorIs("FAILED")) + { + failures.AppendLine($"Case {serverResult.Name}:{caseResult.Name}. Expected 'FAILED', but got '{caseResult.ActualBehavior}'"); + } + break; + case Expectation.NonStrict: + if (!caseResult.BehaviorIs("NON-STRICT")) + { + failures.AppendLine($"Case {serverResult.Name}:{caseResult.Name}. Expected 'NON-STRICT', but got '{caseResult.ActualBehavior}'"); + } + break; + case Expectation.Ok: + if (!caseResult.BehaviorIs("NON-STRICT") && !caseResult.BehaviorIs("OK")) + { + failures.AppendLine($"Case {serverResult.Name}:{caseResult.Name}. Expected 'NON-STRICT' or 'OK', but got '{caseResult.ActualBehavior}'"); + } + break; + case Expectation.OkOrFail: + if (!caseResult.BehaviorIs("NON-STRICT") && !caseResult.BehaviorIs("FAILED") && !caseResult.BehaviorIs("OK")) + { + failures.AppendLine($"Case {serverResult.Name}:{caseResult.Name}. Expected 'FAILED', 'NON-STRICT' or 'OK', but got '{caseResult.ActualBehavior}'"); + } + break; + default: + break; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnResult.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnResult.cs new file mode 100644 index 0000000000..df3c025741 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnResult.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn +{ + public class AutobahnResult + { + public IEnumerable Servers { get; } + + public AutobahnResult(IEnumerable servers) + { + Servers = servers; + } + + public static AutobahnResult FromReportJson(JObject indexJson) + { + // Load the report + return new AutobahnResult(indexJson.Properties().Select(AutobahnServerResult.FromJson)); + } + } +} \ No newline at end of file diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnServerResult.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnServerResult.cs new file mode 100644 index 0000000000..8e53f8091d --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnServerResult.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn +{ + public class AutobahnServerResult + { + public ServerType Server { get; } + public bool Ssl { get; } + public string Environment { get; } + public string Name { get; } + public IEnumerable Cases { get; } + + public AutobahnServerResult(string name, IEnumerable cases) + { + Name = name; + + var splat = name.Split('|'); + if (splat.Length < 3) + { + throw new FormatException("Results incorrectly formatted"); + } + + Server = (ServerType)Enum.Parse(typeof(ServerType), splat[0]); + Ssl = string.Equals(splat[1], "SSL", StringComparison.Ordinal); + Environment = splat[2]; + Cases = cases; + } + + public static AutobahnServerResult FromJson(JProperty prop) + { + var valueObj = ((JObject)prop.Value); + return new AutobahnServerResult(prop.Name, valueObj.Properties().Select(AutobahnCaseResult.FromJson)); + } + } +} \ No newline at end of file diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnSpec.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnSpec.cs new file mode 100644 index 0000000000..0d48db4eab --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnSpec.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn +{ + public class AutobahnSpec + { + public string OutputDirectory { get; } + public IList Servers { get; } = new List(); + public IList Cases { get; } = new List(); + public IList ExcludedCases { get; } = new List(); + + public AutobahnSpec(string outputDirectory) + { + OutputDirectory = outputDirectory; + } + + public AutobahnSpec WithServer(string name, string url) + { + Servers.Add(new ServerSpec(name, url)); + return this; + } + + public AutobahnSpec IncludeCase(params string[] caseSpecs) + { + foreach (var caseSpec in caseSpecs) + { + Cases.Add(caseSpec); + } + return this; + } + + public AutobahnSpec ExcludeCase(params string[] caseSpecs) + { + foreach (var caseSpec in caseSpecs) + { + ExcludedCases.Add(caseSpec); + } + return this; + } + + public void WriteJson(string file) + { + File.WriteAllText(file, GetJson().ToString(Formatting.Indented)); + } + + public JObject GetJson() => new JObject( + new JProperty("options", new JObject( + new JProperty("failByDrop", false))), + new JProperty("outdir", OutputDirectory), + new JProperty("servers", new JArray(Servers.Select(s => s.GetJson()).ToArray())), + new JProperty("cases", new JArray(Cases.ToArray())), + new JProperty("exclude-cases", new JArray(ExcludedCases.ToArray())), + new JProperty("exclude-agent-cases", new JObject())); + } +} diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnTester.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnTester.cs new file mode 100644 index 0000000000..4eb5ee177e --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnTester.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Authentication; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn +{ + public class AutobahnTester : IDisposable + { + private readonly List _deployers = new List(); + private readonly List _deployments = new List(); + private readonly List _expectations = new List(); + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + + public AutobahnSpec Spec { get; } + + public AutobahnTester(ILoggerFactory loggerFactory, AutobahnSpec baseSpec) + { + _loggerFactory = loggerFactory; + _logger = _loggerFactory.CreateLogger("AutobahnTester"); + + Spec = baseSpec; + } + + public async Task Run(CancellationToken cancellationToken) + { + var specFile = Path.GetTempFileName(); + try + { + // Start pinging the servers to see that they're still running + var pingCts = new CancellationTokenSource(); + var pinger = new Timer(state => Pinger((CancellationToken)state), pingCts.Token, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)); + + Spec.WriteJson(specFile); + + // Run the test (write something to the console so people know this will take a while...) + _logger.LogInformation("Using 'wstest' from: {WsTestPath}", Wstest.Default.Location); + _logger.LogInformation("Now launching Autobahn Test Suite. This will take a while."); + var exitCode = await Wstest.Default.ExecAsync("-m fuzzingclient -s " + specFile, cancellationToken, _loggerFactory.CreateLogger("wstest")); + if (exitCode != 0) + { + throw new Exception("wstest failed"); + } + + pingCts.Cancel(); + } + finally + { + if (File.Exists(specFile)) + { + File.Delete(specFile); + } + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Parse the output. + var outputFile = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", Spec.OutputDirectory, "index.json"); + using (var reader = new StreamReader(File.OpenRead(outputFile))) + { + return AutobahnResult.FromReportJson(JObject.Parse(await reader.ReadToEndAsync())); + } + } + + // Async void! It's OK here because we are running in a timer. We're just using async void to chain continuations. + // There's nobody to await this, hence async void. + private async void Pinger(CancellationToken token) + { + try + { + while (!token.IsCancellationRequested) + { + try + { + foreach (var deployment in _deployments) + { + if (token.IsCancellationRequested) + { + return; + } + + var resp = await deployment.HttpClient.GetAsync("/ping", token); + if (!resp.IsSuccessStatusCode) + { + _logger.LogWarning("Non-successful response when pinging {url}: {statusCode} {reasonPhrase}", deployment.ApplicationBaseUri, resp.StatusCode, resp.ReasonPhrase); + } + } + } + catch (OperationCanceledException) + { + // We don't want to throw when the token fires, just stop. + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while pinging servers"); + } + } + + public void Verify(AutobahnResult result) + { + var failures = new StringBuilder(); + foreach (var serverResult in result.Servers) + { + var serverExpectation = _expectations.FirstOrDefault(e => e.Server == serverResult.Server && e.Ssl == serverResult.Ssl); + if (serverExpectation == null) + { + failures.AppendLine($"Expected no results for server: {serverResult.Name} but found results!"); + } + else + { + serverExpectation.Verify(serverResult, failures); + } + } + + Assert.True(failures.Length == 0, "Autobahn results did not meet expectations:" + Environment.NewLine + failures.ToString()); + } + + public async Task DeployTestAndAddToSpec(ServerType server, bool ssl, string environment, CancellationToken cancellationToken, Action expectationConfig = null) + { + var sslNamePart = ssl ? "SSL" : "NoSSL"; + var name = $"{server}|{sslNamePart}|{environment}"; + var logger = _loggerFactory.CreateLogger($"AutobahnTestApp:{server}:{sslNamePart}:{environment}"); + + var appPath = Helpers.GetApplicationPath("AutobahnTestApp"); + var configPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "Http.config"); + var targetFramework = +#if NETCOREAPP2_2 + "netcoreapp2.2"; +#else +#error Target frameworks need to be updated +#endif + var parameters = new DeploymentParameters(appPath, server, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64) + { + Scheme = (ssl ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), + ApplicationType = ApplicationType.Portable, + TargetFramework = targetFramework, + EnvironmentName = environment, + SiteName = "HttpTestSite", // This is configured in the Http.config + ServerConfigTemplateContent = (server == ServerType.IISExpress) ? File.ReadAllText(configPath) : null, + }; + + var deployer = ApplicationDeployerFactory.Create(parameters, _loggerFactory); + var result = await deployer.DeployAsync(); + _deployers.Add(deployer); + _deployments.Add(result); + cancellationToken.ThrowIfCancellationRequested(); + + var handler = new HttpClientHandler(); + // Win7 HttpClient on NetCoreApp2.2 defaults to TLS 1.0 and won't connect to Kestrel. https://github.com/dotnet/corefx/issues/28733 + // Mac HttpClient on NetCoreApp2.0 doesn't alow you to set some combinations. + // https://github.com/dotnet/corefx/blob/586cffcdfdf23ad6c193a4bf37fce88a1bf69508/src/System.Net.Http/src/System/Net/Http/CurlHandler/CurlHandler.SslProvider.OSX.cs#L104-L106 + handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls; + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + var client = result.CreateHttpClient(handler); + + // Make sure the server works + var resp = await RetryHelper.RetryRequest(() => + { + cancellationToken.ThrowIfCancellationRequested(); + return client.GetAsync(result.ApplicationBaseUri); + }, logger, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, result.HostShutdownToken).Token); + resp.EnsureSuccessStatusCode(); + + cancellationToken.ThrowIfCancellationRequested(); + + // Add to the current spec + var wsUrl = result.ApplicationBaseUri.Replace("https://", "wss://").Replace("http://", "ws://"); + Spec.WithServer(name, wsUrl); + + var expectations = new AutobahnExpectations(server, ssl, environment); + expectationConfig?.Invoke(expectations); + _expectations.Add(expectations); + + cancellationToken.ThrowIfCancellationRequested(); + } + + public void Dispose() + { + foreach (var deployer in _deployers) + { + deployer.Dispose(); + } + } + } +} diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Executable.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Executable.cs new file mode 100644 index 0000000000..cdd753bf3e --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Executable.cs @@ -0,0 +1,83 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn +{ + public class Executable + { + private static readonly string _exeSuffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty; + + public string Location { get; } + + protected Executable(string path) + { + Location = path; + } + + public static string Locate(string name) + { + foreach (var dir in Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator)) + { + var candidate = Path.Combine(dir, name + _exeSuffix); + if (File.Exists(candidate)) + { + return candidate; + } + } + return null; + } + + public async Task ExecAsync(string args, CancellationToken cancellationToken, ILogger logger) + { + var process = new Process() + { + StartInfo = new ProcessStartInfo() + { + FileName = Location, + Arguments = args, + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true + }, + EnableRaisingEvents = true + }; + var tcs = new TaskCompletionSource(); + + using (cancellationToken.Register(() => Cancel(process, tcs))) + { + process.Exited += (_, __) => tcs.TrySetResult(process.ExitCode); + process.OutputDataReceived += (_, a) => LogIfNotNull(logger.LogInformation, "stdout: {0}", a.Data); + process.ErrorDataReceived += (_, a) => LogIfNotNull(logger.LogError, "stderr: {0}", a.Data); + + process.Start(); + + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + + return await tcs.Task; + } + } + + private void LogIfNotNull(Action logger, string message, string data) + { + if (!string.IsNullOrEmpty(data)) + { + logger(message, new[] { data }); + } + } + + private static void Cancel(Process process, TaskCompletionSource tcs) + { + if (process != null && !process.HasExited) + { + process.Kill(); + } + tcs.TrySetCanceled(); + } + } +} diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Expectation.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Expectation.cs new file mode 100644 index 0000000000..28ba5005c1 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Expectation.cs @@ -0,0 +1,10 @@ +namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn +{ + public enum Expectation + { + Fail, + NonStrict, + OkOrFail, + Ok + } +} \ No newline at end of file diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/ServerSpec.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/ServerSpec.cs new file mode 100644 index 0000000000..6b149d14d2 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/ServerSpec.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn +{ + public class ServerSpec + { + public string Name { get; } + public string Url { get; } + + public ServerSpec(string name, string url) + { + Name = name; + Url = url; + } + + public JObject GetJson() => new JObject( + new JProperty("agent", Name), + new JProperty("url", Url), + new JProperty("options", new JObject( + new JProperty("version", 18)))); + } +} \ No newline at end of file diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Wstest.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Wstest.cs new file mode 100644 index 0000000000..775b00d4ab --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Wstest.cs @@ -0,0 +1,37 @@ +using System; +using System.IO; + +namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn +{ + /// + /// Wrapper around the Autobahn Test Suite's "wstest" app. + /// + public class Wstest : Executable + { + private static Lazy _instance = new Lazy(Create); + + public static readonly string DefaultLocation = LocateWstest(); + + public static Wstest Default => _instance.Value; + + public Wstest(string path) : base(path) { } + + private static Wstest Create() + { + var location = LocateWstest(); + + return (location == null || !File.Exists(location)) ? null : new Wstest(location); + } + + private static string LocateWstest() + { + var location = Environment.GetEnvironmentVariable("ASPNETCORE_WSTEST_PATH"); + if (string.IsNullOrEmpty(location)) + { + location = Locate("wstest"); + } + + return location; + } + } +} diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/AutobahnTests.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/AutobahnTests.cs new file mode 100644 index 0000000000..e1e86143a6 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/AutobahnTests.cs @@ -0,0 +1,113 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.WebSockets.ConformanceTest +{ + public class AutobahnTests : LoggedTest + { + private static readonly TimeSpan TestTimeout = TimeSpan.FromMinutes(3); + + public AutobahnTests(ITestOutputHelper output) : base(output) + { + } + + // Skip if wstest is not installed for now, see https://github.com/aspnet/WebSockets/issues/95 + // We will enable Wstest on every build once we've gotten the necessary infrastructure sorted out :). + [ConditionalFact] + [SkipIfWsTestNotPresent] + public async Task AutobahnTestSuite() + { + // If we're on CI, we want to actually fail if WsTest isn't installed, rather than just skipping the test + // The SkipIfWsTestNotPresent attribute ensures that this test isn't skipped on CI, so we just need to check that Wstest is present + // And we use Assert.True to provide an error message + Assert.True(Wstest.Default != null, $"The 'wstest' executable (Autobahn WebSockets Test Suite) could not be found at '{Wstest.DefaultLocation}'. Run the Build Agent setup scripts to install it or see https://github.com/crossbario/autobahn-testsuite for instructions on manual installation."); + + using (StartLog(out var loggerFactory)) + { + var logger = loggerFactory.CreateLogger(); + var reportDir = Environment.GetEnvironmentVariable("AUTOBAHN_SUITES_REPORT_DIR"); + var outDir = !string.IsNullOrEmpty(reportDir) ? + reportDir : + Path.Combine(AppContext.BaseDirectory, "autobahnreports"); + + if (Directory.Exists(outDir)) + { + Directory.Delete(outDir, recursive: true); + } + + outDir = outDir.Replace("\\", "\\\\"); + + // 9.* is Limits/Performance which is VERY SLOW; 12.*/13.* are compression which we don't implement + var spec = new AutobahnSpec(outDir) + .IncludeCase("*") + .ExcludeCase("9.*", "12.*", "13.*"); + + var cts = new CancellationTokenSource(); + cts.CancelAfter(TestTimeout); // These tests generally complete in just over 1 minute. + + using (cts.Token.Register(() => logger.LogError("Test run is taking longer than maximum duration of {timeoutMinutes:0.00} minutes. Aborting...", TestTimeout.TotalMinutes))) + { + AutobahnResult result; + using (var tester = new AutobahnTester(loggerFactory, spec)) + { + await tester.DeployTestAndAddToSpec(ServerType.Kestrel, ssl: false, environment: "ManagedSockets", cancellationToken: cts.Token); + await tester.DeployTestAndAddToSpec(ServerType.Kestrel, ssl: true, environment: "ManagedSockets", cancellationToken: cts.Token); + + // Windows-only WebListener tests + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (IsWindows8OrHigher()) + { + // WebListener occasionally gives a non-strict response on 3.2. IIS Express seems to have the same behavior. Wonder if it's related to HttpSys? + // For now, just allow the non-strict response, it's not a failure. + await tester.DeployTestAndAddToSpec(ServerType.HttpSys, ssl: false, environment: "ManagedSockets", cancellationToken: cts.Token); + } + } + + result = await tester.Run(cts.Token); + tester.Verify(result); + } + } + + // If it hasn't been cancelled yet, cancel the token just to be sure + cts.Cancel(); + } + } + + private bool IsWindows8OrHigher() + { + const string WindowsName = "Microsoft Windows "; + const int VersionOffset = 18; + + if (RuntimeInformation.OSDescription.StartsWith(WindowsName)) + { + var versionStr = RuntimeInformation.OSDescription.Substring(VersionOffset); + Version version; + if (Version.TryParse(versionStr, out version)) + { + return version.Major > 6 || (version.Major == 6 && version.Minor >= 2); + } + } + + return false; + } + + private bool IsIISExpress10Installed() + { + var pf = Environment.GetEnvironmentVariable("PROGRAMFILES"); + var iisExpressExe = Path.Combine(pf, "IIS Express", "iisexpress.exe"); + return File.Exists(iisExpressExe) && FileVersionInfo.GetVersionInfo(iisExpressExe).FileMajorPart >= 10; + } + } +} diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Helpers.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Helpers.cs new file mode 100644 index 0000000000..37f65ca082 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Helpers.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; + +namespace Microsoft.AspNetCore.WebSockets.ConformanceTest +{ + public class Helpers + { + public static string GetApplicationPath(string projectName) + { + var applicationBasePath = AppContext.BaseDirectory; + + var directoryInfo = new DirectoryInfo(applicationBasePath); + do + { + var solutionFileInfo = new FileInfo(Path.Combine(directoryInfo.FullName, "WebSockets.sln")); + if (solutionFileInfo.Exists) + { + return Path.GetFullPath(Path.Combine(directoryInfo.FullName, "test", projectName)); + } + + directoryInfo = directoryInfo.Parent; + } + while (directoryInfo.Parent != null); + + throw new Exception($"Solution root could not be found using {applicationBasePath}"); + } + } +} diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Http.config b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Http.config new file mode 100644 index 0000000000..3668f762c8 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Http.config @@ -0,0 +1,1029 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Microsoft.AspNetCore.WebSockets.ConformanceTest.csproj b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Microsoft.AspNetCore.WebSockets.ConformanceTest.csproj new file mode 100644 index 0000000000..539a46f002 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Microsoft.AspNetCore.WebSockets.ConformanceTest.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp2.2 + + + + + + + + + + + + + diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/SkipIfWsTestNotPresentAttribute.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/SkipIfWsTestNotPresentAttribute.cs new file mode 100644 index 0000000000..be2d7729a2 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.ConformanceTest/SkipIfWsTestNotPresentAttribute.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn; + +namespace Microsoft.AspNetCore.WebSockets.ConformanceTest +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class SkipIfWsTestNotPresentAttribute : Attribute, ITestCondition + { + public bool IsMet => IsOnCi || Wstest.Default != null; + public string SkipReason => "Autobahn Test Suite is not installed on the host machine."; + + private static bool IsOnCi => + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TEAMCITY_VERSION")) || + string.Equals(Environment.GetEnvironmentVariable("TRAVIS"), "true", StringComparison.OrdinalIgnoreCase) || + string.Equals(Environment.GetEnvironmentVariable("APPVEYOR"), "true", StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/AddWebSocketsTests.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/AddWebSocketsTests.cs new file mode 100644 index 0000000000..255d17f24f --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/AddWebSocketsTests.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.WebSockets.Test +{ + public class AddWebSocketsTests + { + [Fact] + public void AddWebSocketsConfiguresOptions() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddWebSockets(o => + { + o.KeepAliveInterval = TimeSpan.FromSeconds(1000); + o.AllowedOrigins.Add("someString"); + }); + + var services = serviceCollection.BuildServiceProvider(); + var socketOptions = services.GetRequiredService>().Value; + + Assert.Equal(TimeSpan.FromSeconds(1000), socketOptions.KeepAliveInterval); + Assert.Single(socketOptions.AllowedOrigins); + Assert.Equal("someString", socketOptions.AllowedOrigins[0]); + } + } +} diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/BufferStream.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/BufferStream.cs new file mode 100644 index 0000000000..1916bf8f4b --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/BufferStream.cs @@ -0,0 +1,350 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebSockets.Test +{ + // This steam accepts writes from one side, buffers them internally, and returns the data via Reads + // when requested on the other side. + public class BufferStream : Stream + { + private bool _disposed; + private bool _aborted; + private bool _terminated; + private Exception _abortException; + private ConcurrentQueue _bufferedData; + private ArraySegment _topBuffer; + private SemaphoreSlim _readLock; + private SemaphoreSlim _writeLock; + private TaskCompletionSource _readWaitingForData; + + internal BufferStream() + { + _readLock = new SemaphoreSlim(1, 1); + _writeLock = new SemaphoreSlim(1, 1); + _bufferedData = new ConcurrentQueue(); + _readWaitingForData = new TaskCompletionSource(); + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return true; } + } + + #region NotSupported + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + #endregion NotSupported + + /// + /// Ends the stream, meaning all future reads will return '0'. + /// + public void End() + { + _terminated = true; + } + + public override void Flush() + { + CheckDisposed(); + // TODO: Wait for data to drain? + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.TrySetCanceled(); + return tcs.Task; + } + + Flush(); + + // TODO: Wait for data to drain? + + return Task.FromResult(0); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if(_terminated) + { + return 0; + } + + VerifyBuffer(buffer, offset, count, allowEmpty: false); + _readLock.Wait(); + try + { + int totalRead = 0; + do + { + // Don't drain buffered data when signaling an abort. + CheckAborted(); + if (_topBuffer.Count <= 0) + { + byte[] topBuffer = null; + while (!_bufferedData.TryDequeue(out topBuffer)) + { + if (_disposed) + { + CheckAborted(); + // Graceful close + return totalRead; + } + WaitForDataAsync().Wait(); + } + _topBuffer = new ArraySegment(topBuffer); + } + int actualCount = Math.Min(count, _topBuffer.Count); + Buffer.BlockCopy(_topBuffer.Array, _topBuffer.Offset, buffer, offset, actualCount); + _topBuffer = new ArraySegment(_topBuffer.Array, + _topBuffer.Offset + actualCount, + _topBuffer.Count - actualCount); + totalRead += actualCount; + offset += actualCount; + count -= actualCount; + } + while (count > 0 && (_topBuffer.Count > 0 || _bufferedData.Count > 0)); + // Keep reading while there is more data available and we have more space to put it in. + return totalRead; + } + finally + { + _readLock.Release(); + } + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + // TODO: This option doesn't preserve the state object. + // return ReadAsync(buffer, offset, count); + return base.BeginRead(buffer, offset, count, callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + // return ((Task)asyncResult).Result; + return base.EndRead(asyncResult); + } + + public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_terminated) + { + return 0; + } + + VerifyBuffer(buffer, offset, count, allowEmpty: false); + var registration = cancellationToken.Register(Abort); + await _readLock.WaitAsync(cancellationToken); + try + { + int totalRead = 0; + do + { + // Don't drained buffered data on abort. + CheckAborted(); + if (_topBuffer.Count <= 0) + { + byte[] topBuffer = null; + while (!_bufferedData.TryDequeue(out topBuffer)) + { + if (_disposed) + { + CheckAborted(); + // Graceful close + return totalRead; + } + await WaitForDataAsync(); + } + _topBuffer = new ArraySegment(topBuffer); + } + var actualCount = Math.Min(count, _topBuffer.Count); + Buffer.BlockCopy(_topBuffer.Array, _topBuffer.Offset, buffer, offset, actualCount); + _topBuffer = new ArraySegment(_topBuffer.Array, + _topBuffer.Offset + actualCount, + _topBuffer.Count - actualCount); + totalRead += actualCount; + offset += actualCount; + count -= actualCount; + } + while (count > 0 && (_topBuffer.Count > 0 || _bufferedData.Count > 0)); + // Keep reading while there is more data available and we have more space to put it in. + return totalRead; + } + finally + { + registration.Dispose(); + _readLock.Release(); + } + } + + // Write with count 0 will still trigger OnFirstWrite + public override void Write(byte[] buffer, int offset, int count) + { + VerifyBuffer(buffer, offset, count, allowEmpty: true); + CheckDisposed(); + + _writeLock.Wait(); + try + { + if (count == 0) + { + return; + } + // Copies are necessary because we don't know what the caller is going to do with the buffer afterwards. + var internalBuffer = new byte[count]; + Buffer.BlockCopy(buffer, offset, internalBuffer, 0, count); + _bufferedData.Enqueue(internalBuffer); + + SignalDataAvailable(); + } + finally + { + _writeLock.Release(); + } + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + Write(buffer, offset, count); + var tcs = new TaskCompletionSource(state); + tcs.TrySetResult(null); + var result = tcs.Task; + if (callback != null) + { + callback(result); + } + return result; + } + + public override void EndWrite(IAsyncResult asyncResult) { } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + VerifyBuffer(buffer, offset, count, allowEmpty: true); + if (cancellationToken.IsCancellationRequested) + { + var tcs = new TaskCompletionSource(); + tcs.TrySetCanceled(); + return tcs.Task; + } + + Write(buffer, offset, count); + return Task.FromResult(null); + } + + private static void VerifyBuffer(byte[] buffer, int offset, int count, bool allowEmpty) + { + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); + } + if (count < 0 || count > buffer.Length - offset + || (!allowEmpty && count == 0)) + { + throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty); + } + } + + private void SignalDataAvailable() + { + // Dispatch, as TrySetResult will synchronously execute the waiters callback and block our Write. + Task.Factory.StartNew(() => _readWaitingForData.TrySetResult(null)); + } + + private Task WaitForDataAsync() + { + _readWaitingForData = new TaskCompletionSource(); + + if (!_bufferedData.IsEmpty || _disposed) + { + // Race, data could have arrived before we created the TCS. + _readWaitingForData.TrySetResult(null); + } + + return _readWaitingForData.Task; + } + + internal void Abort() + { + Abort(new OperationCanceledException()); + } + + internal void Abort(Exception innerException) + { + Contract.Requires(innerException != null); + _aborted = true; + _abortException = innerException; + Dispose(); + } + + private void CheckAborted() + { + if (_aborted) + { + throw new IOException(string.Empty, _abortException); + } + } + + [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_writeLock", Justification = "ODEs from the locks would mask IOEs from abort.")] + [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_readLock", Justification = "Data can still be read unless we get aborted.")] + protected override void Dispose(bool disposing) + { + if (disposing) + { + // Throw for further writes, but not reads. Allow reads to drain the buffered data and then return 0 for further reads. + _disposed = true; + _readWaitingForData.TrySetResult(null); + } + + base.Dispose(disposing); + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + } +} diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/DuplexStream.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/DuplexStream.cs new file mode 100644 index 0000000000..03ae00f900 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/DuplexStream.cs @@ -0,0 +1,172 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.WebSockets.Test +{ + // A duplex wrapper around a read and write stream. + public class DuplexStream : Stream + { + public BufferStream ReadStream { get; } + public BufferStream WriteStream { get; } + + public DuplexStream() + : this (new BufferStream(), new BufferStream()) + { + } + + public DuplexStream(BufferStream readStream, BufferStream writeStream) + { + ReadStream = readStream; + WriteStream = writeStream; + } + + public DuplexStream CreateReverseDuplexStream() + { + return new DuplexStream(WriteStream, ReadStream); + } + + +#region Properties + + public override bool CanRead + { + get { return ReadStream.CanRead; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanTimeout + { + get { return ReadStream.CanTimeout || WriteStream.CanTimeout; } + } + + public override bool CanWrite + { + get { return WriteStream.CanWrite; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override int ReadTimeout + { + get { return ReadStream.ReadTimeout; } + set { ReadStream.ReadTimeout = value; } + } + + public override int WriteTimeout + { + get { return WriteStream.WriteTimeout; } + set { WriteStream.WriteTimeout = value; } + } + +#endregion Properties + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override int ReadByte() + { + return ReadStream.ReadByte(); + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return ReadStream.BeginRead(buffer, offset, count, callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + return ReadStream.EndRead(asyncResult); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return ReadStream.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return ReadStream.CopyToAsync(destination, bufferSize, cancellationToken); + } + +#region Read + + public override int Read(byte[] buffer, int offset, int count) + { + return ReadStream.Read(buffer, offset, count); + } + +#endregion Read + +#region Write + + public override void Write(byte[] buffer, int offset, int count) + { + WriteStream.Write(buffer, offset, count); + } + public override void WriteByte(byte value) + { + WriteStream.WriteByte(value); + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return WriteStream.BeginWrite(buffer, offset, count, callback, state); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + WriteStream.EndWrite(asyncResult); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return WriteStream.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return WriteStream.FlushAsync(cancellationToken); + } + + public override void Flush() + { + WriteStream.Flush(); + } + +#endregion Write + + protected override void Dispose(bool disposing) + { + if (disposing) + { + ReadStream.Dispose(); + WriteStream.Dispose(); + } + base.Dispose(disposing); + } + } +} diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/IWebHostPortExtensions.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/IWebHostPortExtensions.cs new file mode 100644 index 0000000000..d3b53681f4 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/IWebHostPortExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Hosting.Server.Features; + +namespace Microsoft.AspNetCore.Hosting +{ + public static class IWebHostPortExtensions + { + public static int GetPort(this IWebHost host) + { + return host.GetPorts().First(); + } + + public static IEnumerable GetPorts(this IWebHost host) + { + return host.GetUris() + .Select(u => u.Port); + } + + public static IEnumerable GetUris(this IWebHost host) + { + return host.ServerFeatures.Get().Addresses + .Select(a => new Uri(a)); + } + } +} \ No newline at end of file diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/KestrelWebSocketHelpers.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/KestrelWebSocketHelpers.cs new file mode 100644 index 0000000000..8f24cb92e7 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/KestrelWebSocketHelpers.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.WebSockets.Test +{ + public class KestrelWebSocketHelpers + { + public static IDisposable CreateServer(ILoggerFactory loggerFactory, out int port, Func app, Action configure = null) + { + configure = configure ?? (o => { }); + Action startup = builder => + { + builder.Use(async (ct, next) => + { + try + { + // Kestrel does not return proper error responses: + // https://github.com/aspnet/KestrelHttpServer/issues/43 + await next(); + } + catch (Exception ex) + { + if (ct.Response.HasStarted) + { + throw; + } + + ct.Response.StatusCode = 500; + ct.Response.Headers.Clear(); + await ct.Response.WriteAsync(ex.ToString()); + } + }); + builder.UseWebSockets(); + builder.Run(c => app(c)); + }; + + var configBuilder = new ConfigurationBuilder(); + configBuilder.AddInMemoryCollection(); + var config = configBuilder.Build(); + config["server.urls"] = $"http://127.0.0.1:0"; + + var host = new WebHostBuilder() + .ConfigureServices(s => + { + s.AddWebSockets(configure); + s.AddSingleton(loggerFactory); + }) + .UseConfiguration(config) + .UseKestrel() + .Configure(startup) + .Build(); + + host.Start(); + port = host.GetPort(); + + return host; + } + } +} + diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/Microsoft.AspNetCore.WebSockets.Test.csproj b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/Microsoft.AspNetCore.WebSockets.Test.csproj new file mode 100644 index 0000000000..5e7f2eb67e --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/Microsoft.AspNetCore.WebSockets.Test.csproj @@ -0,0 +1,20 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + + + + + + diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/SendReceiveTests.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/SendReceiveTests.cs new file mode 100644 index 0000000000..afa9f70741 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/SendReceiveTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.WebSockets.Test +{ + public class SendReceiveTests + { + [Fact] + public async Task ClientToServerTextMessage() + { + const string message = "Hello, World!"; + + var pair = WebSocketPair.Create(); + var sendBuffer = Encoding.UTF8.GetBytes(message); + + await pair.ClientSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Text, endOfMessage: true, cancellationToken: CancellationToken.None); + + var receiveBuffer = new byte[32]; + var result = await pair.ServerSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); + + Assert.Equal(WebSocketMessageType.Text, result.MessageType); + Assert.Equal(message, Encoding.UTF8.GetString(receiveBuffer, 0, result.Count)); + } + + [Fact] + public async Task ServerToClientTextMessage() + { + const string message = "Hello, World!"; + + var pair = WebSocketPair.Create(); + var sendBuffer = Encoding.UTF8.GetBytes(message); + + await pair.ServerSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Text, endOfMessage: true, cancellationToken: CancellationToken.None); + + var receiveBuffer = new byte[32]; + var result = await pair.ClientSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); + + Assert.Equal(WebSocketMessageType.Text, result.MessageType); + Assert.Equal(message, Encoding.UTF8.GetString(receiveBuffer, 0, result.Count)); + } + + [Fact] + public async Task ClientToServerBinaryMessage() + { + var pair = WebSocketPair.Create(); + var sendBuffer = new byte[] { 0xde, 0xad, 0xbe, 0xef }; + + await pair.ClientSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Binary, endOfMessage: true, cancellationToken: CancellationToken.None); + + var receiveBuffer = new byte[32]; + var result = await pair.ServerSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); + + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + Assert.Equal(sendBuffer, receiveBuffer.Take(result.Count).ToArray()); + } + + [Fact] + public async Task ServerToClientBinaryMessage() + { + var pair = WebSocketPair.Create(); + var sendBuffer = new byte[] { 0xde, 0xad, 0xbe, 0xef }; + + await pair.ServerSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Binary, endOfMessage: true, cancellationToken: CancellationToken.None); + + var receiveBuffer = new byte[32]; + var result = await pair.ClientSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); + + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + Assert.Equal(sendBuffer, receiveBuffer.Take(result.Count).ToArray()); + } + + [Fact] + public async Task ThrowsWhenUnderlyingStreamClosed() + { + var pair = WebSocketPair.Create(); + var sendBuffer = new byte[] { 0xde, 0xad, 0xbe, 0xef }; + + await pair.ServerSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Binary, endOfMessage: true, cancellationToken: CancellationToken.None); + + var receiveBuffer = new byte[32]; + var result = await pair.ClientSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); + + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + + // Close the client socket's read end + pair.ClientStream.ReadStream.End(); + + // Assert.Throws doesn't support async :( + try + { + await pair.ClientSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); + + // The exception should prevent this line from running + Assert.False(true, "Expected an exception to be thrown!"); + } + catch (WebSocketException ex) + { + Assert.Equal(WebSocketError.ConnectionClosedPrematurely, ex.WebSocketErrorCode); + } + } + } +} \ No newline at end of file diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/WebSocketMiddlewareTests.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/WebSocketMiddlewareTests.cs new file mode 100644 index 0000000000..116877df61 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/WebSocketMiddlewareTests.cs @@ -0,0 +1,586 @@ +// 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.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.AspNetCore.WebSockets.Internal; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.WebSockets.Test +{ +#if NET461 + // ClientWebSocket does not support WebSockets on these platforms and OS. Kestrel does support it. + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, SkipReason = "No WebSockets Client for this platform")] +#elif NETCOREAPP2_2 + // ClientWebSocket has added support for WebSockets on Win7. +#else +#error Unknown TFM +#endif + public class WebSocketMiddlewareTests : LoggedTest + { + [ConditionalFact] + public async Task Connect_Success() + { + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + } + } + } + + [ConditionalFact] + public async Task NegotiateSubProtocol_Success() + { + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + Assert.Equal("alpha, bravo, charlie", context.Request.Headers["Sec-WebSocket-Protocol"]); + var webSocket = await context.WebSockets.AcceptWebSocketAsync("Bravo"); + })) + { + using (var client = new ClientWebSocket()) + { + client.Options.AddSubProtocol("alpha"); + client.Options.AddSubProtocol("bravo"); + client.Options.AddSubProtocol("charlie"); + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + + // The Windows version of ClientWebSocket uses the casing from the header (Bravo) + // However, the Managed version seems match the header against the list generated by + // the AddSubProtocol calls (case-insensitively) and then use the version from + // that list as the value for SubProtocol. This is fine, but means we need to ignore case here. + // We could update our AddSubProtocols above to the same case but I think it's better to + // ensure this behavior is codified by this test. + Assert.Equal("Bravo", client.SubProtocol, ignoreCase: true); + } + } + } + + [ConditionalFact] + public async Task SendEmptyData_Success() + { + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + var serverBuffer = new byte[0]; + var result = await webSocket.ReceiveAsync(new ArraySegment(serverBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(0, result.Count); + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + var orriginalData = new byte[0]; + await client.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Binary, true, CancellationToken.None); + } + } + } + + [ConditionalFact] + public async Task SendShortData_Success() + { + var orriginalData = Encoding.UTF8.GetBytes("Hello World"); + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + var serverBuffer = new byte[orriginalData.Length]; + var result = await webSocket.ReceiveAsync(new ArraySegment(serverBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(orriginalData.Length, result.Count); + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + Assert.Equal(orriginalData, serverBuffer); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + await client.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Binary, true, CancellationToken.None); + } + } + } + + [ConditionalFact] + public async Task SendMediumData_Success() + { + var orriginalData = Encoding.UTF8.GetBytes(new string('a', 130)); + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + var serverBuffer = new byte[orriginalData.Length]; + var result = await webSocket.ReceiveAsync(new ArraySegment(serverBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(orriginalData.Length, result.Count); + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + Assert.Equal(orriginalData, serverBuffer); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + await client.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Binary, true, CancellationToken.None); + } + } + } + + [ConditionalFact] + public async Task SendLongData_Success() + { + var orriginalData = Encoding.UTF8.GetBytes(new string('a', 0x1FFFF)); + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + var serverBuffer = new byte[orriginalData.Length]; + var result = await webSocket.ReceiveAsync(new ArraySegment(serverBuffer), CancellationToken.None); + int intermediateCount = result.Count; + Assert.False(result.EndOfMessage); + Assert.Equal(WebSocketMessageType.Text, result.MessageType); + + result = await webSocket.ReceiveAsync(new ArraySegment(serverBuffer, intermediateCount, orriginalData.Length - intermediateCount), CancellationToken.None); + intermediateCount += result.Count; + Assert.False(result.EndOfMessage); + Assert.Equal(WebSocketMessageType.Text, result.MessageType); + + result = await webSocket.ReceiveAsync(new ArraySegment(serverBuffer, intermediateCount, orriginalData.Length - intermediateCount), CancellationToken.None); + intermediateCount += result.Count; + Assert.True(result.EndOfMessage); + Assert.Equal(orriginalData.Length, intermediateCount); + Assert.Equal(WebSocketMessageType.Text, result.MessageType); + + Assert.Equal(orriginalData, serverBuffer); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + await client.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Binary, true, CancellationToken.None); + } + } + } + + [ConditionalFact] + public async Task SendFragmentedData_Success() + { + var orriginalData = Encoding.UTF8.GetBytes("Hello World"); + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + var serverBuffer = new byte[orriginalData.Length]; + var result = await webSocket.ReceiveAsync(new ArraySegment(serverBuffer), CancellationToken.None); + Assert.False(result.EndOfMessage); + Assert.Equal(2, result.Count); + int totalReceived = result.Count; + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + + result = await webSocket.ReceiveAsync( + new ArraySegment(serverBuffer, totalReceived, serverBuffer.Length - totalReceived), CancellationToken.None); + Assert.False(result.EndOfMessage); + Assert.Equal(2, result.Count); + totalReceived += result.Count; + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + + result = await webSocket.ReceiveAsync( + new ArraySegment(serverBuffer, totalReceived, serverBuffer.Length - totalReceived), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(7, result.Count); + totalReceived += result.Count; + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + + Assert.Equal(orriginalData, serverBuffer); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + await client.SendAsync(new ArraySegment(orriginalData, 0, 2), WebSocketMessageType.Binary, false, CancellationToken.None); + await client.SendAsync(new ArraySegment(orriginalData, 2, 2), WebSocketMessageType.Binary, false, CancellationToken.None); + await client.SendAsync(new ArraySegment(orriginalData, 4, 7), WebSocketMessageType.Binary, true, CancellationToken.None); + } + } + } + + [ConditionalFact] + public async Task ReceiveShortData_Success() + { + var orriginalData = Encoding.UTF8.GetBytes("Hello World"); + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + await webSocket.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Binary, true, CancellationToken.None); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + var clientBuffer = new byte[orriginalData.Length]; + var result = await client.ReceiveAsync(new ArraySegment(clientBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(orriginalData.Length, result.Count); + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + Assert.Equal(orriginalData, clientBuffer); + } + } + } + + [ConditionalFact] + public async Task ReceiveMediumData_Success() + { + var orriginalData = Encoding.UTF8.GetBytes(new string('a', 130)); + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + await webSocket.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Binary, true, CancellationToken.None); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + var clientBuffer = new byte[orriginalData.Length]; + var result = await client.ReceiveAsync(new ArraySegment(clientBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(orriginalData.Length, result.Count); + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + Assert.Equal(orriginalData, clientBuffer); + } + } + } + + [ConditionalFact] + public async Task ReceiveLongData() + { + var orriginalData = Encoding.UTF8.GetBytes(new string('a', 0x1FFFF)); + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + await webSocket.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Binary, true, CancellationToken.None); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + var clientBuffer = new byte[orriginalData.Length]; + WebSocketReceiveResult result; + int receivedCount = 0; + do + { + result = await client.ReceiveAsync(new ArraySegment(clientBuffer, receivedCount, clientBuffer.Length - receivedCount), CancellationToken.None); + receivedCount += result.Count; + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + } + while (!result.EndOfMessage); + + Assert.Equal(orriginalData.Length, receivedCount); + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + Assert.Equal(orriginalData, clientBuffer); + } + } + } + + [ConditionalFact] + public async Task ReceiveFragmentedData_Success() + { + var orriginalData = Encoding.UTF8.GetBytes("Hello World"); + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + await webSocket.SendAsync(new ArraySegment(orriginalData, 0, 2), WebSocketMessageType.Binary, false, CancellationToken.None); + await webSocket.SendAsync(new ArraySegment(orriginalData, 2, 2), WebSocketMessageType.Binary, false, CancellationToken.None); + await webSocket.SendAsync(new ArraySegment(orriginalData, 4, 7), WebSocketMessageType.Binary, true, CancellationToken.None); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + var clientBuffer = new byte[orriginalData.Length]; + var result = await client.ReceiveAsync(new ArraySegment(clientBuffer), CancellationToken.None); + Assert.False(result.EndOfMessage); + Assert.Equal(2, result.Count); + int totalReceived = result.Count; + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + + result = await client.ReceiveAsync( + new ArraySegment(clientBuffer, totalReceived, clientBuffer.Length - totalReceived), CancellationToken.None); + Assert.False(result.EndOfMessage); + Assert.Equal(2, result.Count); + totalReceived += result.Count; + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + + result = await client.ReceiveAsync( + new ArraySegment(clientBuffer, totalReceived, clientBuffer.Length - totalReceived), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(7, result.Count); + totalReceived += result.Count; + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + + Assert.Equal(orriginalData, clientBuffer); + } + } + } + + [ConditionalFact] + public async Task SendClose_Success() + { + string closeDescription = "Test Closed"; + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + var serverBuffer = new byte[1024]; + var result = await webSocket.ReceiveAsync(new ArraySegment(serverBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(0, result.Count); + Assert.Equal(WebSocketMessageType.Close, result.MessageType); + Assert.Equal(WebSocketCloseStatus.NormalClosure, result.CloseStatus); + Assert.Equal(closeDescription, result.CloseStatusDescription); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, closeDescription, CancellationToken.None); + + Assert.Equal(WebSocketState.CloseSent, client.State); + } + } + } + + [ConditionalFact] + public async Task ReceiveClose_Success() + { + string closeDescription = "Test Closed"; + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, closeDescription, CancellationToken.None); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + var clientBuffer = new byte[1024]; + var result = await client.ReceiveAsync(new ArraySegment(clientBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(0, result.Count); + Assert.Equal(WebSocketMessageType.Close, result.MessageType); + Assert.Equal(WebSocketCloseStatus.NormalClosure, result.CloseStatus); + Assert.Equal(closeDescription, result.CloseStatusDescription); + + Assert.Equal(WebSocketState.CloseReceived, client.State); + } + } + } + + [ConditionalFact] + public async Task CloseFromOpen_Success() + { + string closeDescription = "Test Closed"; + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + var serverBuffer = new byte[1024]; + var result = await webSocket.ReceiveAsync(new ArraySegment(serverBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(0, result.Count); + Assert.Equal(WebSocketMessageType.Close, result.MessageType); + Assert.Equal(WebSocketCloseStatus.NormalClosure, result.CloseStatus); + Assert.Equal(closeDescription, result.CloseStatusDescription); + + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, closeDescription, CancellationToken.None); + + Assert.Equal(WebSocketState.Closed, client.State); + } + } + } + + [ConditionalFact] + public async Task CloseFromCloseSent_Success() + { + string closeDescription = "Test Closed"; + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + var serverBuffer = new byte[1024]; + var result = await webSocket.ReceiveAsync(new ArraySegment(serverBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(0, result.Count); + Assert.Equal(WebSocketMessageType.Close, result.MessageType); + Assert.Equal(WebSocketCloseStatus.NormalClosure, result.CloseStatus); + Assert.Equal(closeDescription, result.CloseStatusDescription); + + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, closeDescription, CancellationToken.None); + Assert.Equal(WebSocketState.CloseSent, client.State); + + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, closeDescription, CancellationToken.None); + Assert.Equal(WebSocketState.Closed, client.State); + } + } + } + + [ConditionalFact] + public async Task CloseFromCloseReceived_Success() + { + string closeDescription = "Test Closed"; + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, async context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, closeDescription, CancellationToken.None); + + var serverBuffer = new byte[1024]; + var result = await webSocket.ReceiveAsync(new ArraySegment(serverBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(0, result.Count); + Assert.Equal(WebSocketMessageType.Close, result.MessageType); + Assert.Equal(WebSocketCloseStatus.NormalClosure, result.CloseStatus); + Assert.Equal(closeDescription, result.CloseStatusDescription); + })) + { + using (var client = new ClientWebSocket()) + { + await client.ConnectAsync(new Uri($"ws://localhost:{port}/"), CancellationToken.None); + var clientBuffer = new byte[1024]; + var result = await client.ReceiveAsync(new ArraySegment(clientBuffer), CancellationToken.None); + Assert.True(result.EndOfMessage); + Assert.Equal(0, result.Count); + Assert.Equal(WebSocketMessageType.Close, result.MessageType); + Assert.Equal(WebSocketCloseStatus.NormalClosure, result.CloseStatus); + Assert.Equal(closeDescription, result.CloseStatusDescription); + + Assert.Equal(WebSocketState.CloseReceived, client.State); + + await client.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + + Assert.Equal(WebSocketState.Closed, client.State); + } + } + } + + [Theory] + [InlineData(HttpStatusCode.OK, null)] + [InlineData(HttpStatusCode.Forbidden, "")] + [InlineData(HttpStatusCode.Forbidden, "http://e.com")] + [InlineData(HttpStatusCode.OK, "http://e.com", "http://example.com")] + [InlineData(HttpStatusCode.OK, "*")] + [InlineData(HttpStatusCode.OK, "http://e.com", "*")] + [InlineData(HttpStatusCode.OK, "http://ExAmPLE.cOm")] + public async Task OriginIsValidatedForWebSocketRequests(HttpStatusCode expectedCode, params string[] origins) + { + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, context => + { + Assert.True(context.WebSockets.IsWebSocketRequest); + return Task.CompletedTask; + }, + o => + { + if (origins != null) + { + foreach (var origin in origins) + { + o.AllowedOrigins.Add(origin); + } + } + })) + { + using (var client = new HttpClient()) + { + var uri = new UriBuilder(new Uri($"ws://localhost:{port}/")); + uri.Scheme = "http"; + + // Craft a valid WebSocket Upgrade request + using (var request = new HttpRequestMessage(HttpMethod.Get, uri.ToString())) + { + request.Headers.Connection.Clear(); + request.Headers.Connection.Add("Upgrade"); + request.Headers.Upgrade.Add(new System.Net.Http.Headers.ProductHeaderValue("websocket")); + request.Headers.Add(Constants.Headers.SecWebSocketVersion, Constants.Headers.SupportedVersion); + // SecWebSocketKey required to be 16 bytes + request.Headers.Add(Constants.Headers.SecWebSocketKey, Convert.ToBase64String(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }, Base64FormattingOptions.None)); + + request.Headers.Add("Origin", "http://example.com"); + + var response = await client.SendAsync(request); + Assert.Equal(expectedCode, response.StatusCode); + } + } + } + } + + [Fact] + public async Task OriginIsNotValidatedForNonWebSocketRequests() + { + using (var server = KestrelWebSocketHelpers.CreateServer(LoggerFactory, out var port, context => + { + Assert.False(context.WebSockets.IsWebSocketRequest); + return Task.CompletedTask; + }, + o => o.AllowedOrigins.Add("http://example.com"))) + { + using (var client = new HttpClient()) + { + var uri = new UriBuilder(new Uri($"ws://localhost:{port}/")); + uri.Scheme = "http"; + + using (var request = new HttpRequestMessage(HttpMethod.Get, uri.ToString())) + { + request.Headers.Add("Origin", "http://notexample.com"); + + var response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + } + } + } + } +} diff --git a/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/WebSocketPair.cs b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/WebSocketPair.cs new file mode 100644 index 0000000000..4a43250566 --- /dev/null +++ b/src/WebSockets/test/Microsoft.AspNetCore.WebSockets.Test/WebSocketPair.cs @@ -0,0 +1,35 @@ +using System; +using System.Net.WebSockets; +using Microsoft.AspNetCore.WebSockets.Internal; + +namespace Microsoft.AspNetCore.WebSockets.Test +{ + internal class WebSocketPair + { + public WebSocket ClientSocket { get; } + public WebSocket ServerSocket { get; } + public DuplexStream ServerStream { get; } + public DuplexStream ClientStream { get; } + + public WebSocketPair(DuplexStream serverStream, DuplexStream clientStream, WebSocket clientSocket, WebSocket serverSocket) + { + ClientStream = clientStream; + ServerStream = serverStream; + ClientSocket = clientSocket; + ServerSocket = serverSocket; + } + + public static WebSocketPair Create() + { + // Create streams + var serverStream = new DuplexStream(); + var clientStream = serverStream.CreateReverseDuplexStream(); + + return new WebSocketPair( + serverStream, + clientStream, + clientSocket: WebSocketProtocol.CreateFromStream(clientStream, isServer: false, subProtocol: null, keepAliveInterval: TimeSpan.FromMinutes(2)), + serverSocket: WebSocketProtocol.CreateFromStream(serverStream, isServer: true, subProtocol: null, keepAliveInterval: TimeSpan.FromMinutes(2))); + } + } +} diff --git a/src/WebSockets/version.props b/src/WebSockets/version.props new file mode 100644 index 0000000000..4889a26987 --- /dev/null +++ b/src/WebSockets/version.props @@ -0,0 +1,12 @@ + + + 2.2.0 + rtm + $(VersionPrefix) + $(VersionPrefix)-$(VersionSuffix)-final + t000 + a- + $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) + $(VersionSuffix)-$(BuildNumber) + +