├── .appveyor.yml ├── .gitattributes ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── .vsts-pipelines └── builds │ ├── ci-internal.yml │ └── ci-public.yml ├── CONTRIBUTING.md ├── Directory.Build.props ├── Directory.Build.targets ├── LICENSE.txt ├── NuGet.config ├── NuGetPackageVerifier.json ├── README.md ├── WebSockets.sln ├── build.cmd ├── build.sh ├── build ├── Key.snk ├── dependencies.props ├── repo.props ├── repo.targets ├── setup-wstest.ps1 ├── setup-wstest.sh └── sources.props ├── korebuild-lock.txt ├── korebuild.json ├── run.cmd ├── run.ps1 ├── run.sh ├── samples ├── AutobahnTestAppAspNet4 │ ├── AutobahnTestAppAspNet4.csproj.aspnet4 │ ├── EchoSocket.ashx │ ├── EchoSocket.ashx.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Web.Debug.config │ ├── Web.Release.config │ ├── Web.config │ ├── packages.config │ └── wstest-spec.json ├── AutobahnTestAppHttpListener │ ├── App.config │ ├── AutobahnTestAppHttpListener.csproj.net461 │ ├── Program.cs │ └── Properties │ │ └── AssemblyInfo.cs ├── EchoApp │ ├── EchoApp.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ └── wwwroot │ │ └── index.html └── TestServer │ ├── App.config │ ├── Program.cs │ ├── Properties │ └── AssemblyInfo.cs │ └── TestServer.csproj ├── src ├── Directory.Build.props └── Microsoft.AspNetCore.WebSockets │ ├── ExtendedWebSocketAcceptContext.cs │ ├── Internal │ ├── Constants.cs │ └── HandshakeHelpers.cs │ ├── Microsoft.AspNetCore.WebSockets.csproj │ ├── WebSocketMiddleware.cs │ ├── WebSocketMiddlewareExtensions.cs │ ├── WebSocketOptions.cs │ ├── WebSocketsDependencyInjectionExtensions.cs │ └── baseline.netcore.json ├── test ├── AutobahnTestApp │ ├── AutobahnTestApp.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── README.md │ ├── Startup.cs │ ├── TestResources │ │ ├── testCert.pfx │ │ └── testCert.txt │ └── scripts │ │ ├── RunAutobahnTests.ps1 │ │ └── autobahn.spec.json ├── Directory.Build.props ├── Microsoft.AspNetCore.WebSockets.ConformanceTest │ ├── Autobahn │ │ ├── AutobahnCaseResult.cs │ │ ├── AutobahnExpectations.cs │ │ ├── AutobahnResult.cs │ │ ├── AutobahnServerResult.cs │ │ ├── AutobahnSpec.cs │ │ ├── AutobahnTester.cs │ │ ├── Executable.cs │ │ ├── Expectation.cs │ │ ├── ServerSpec.cs │ │ └── Wstest.cs │ ├── AutobahnTests.cs │ ├── Helpers.cs │ ├── Http.config │ ├── Microsoft.AspNetCore.WebSockets.ConformanceTest.csproj │ └── SkipIfWsTestNotPresentAttribute.cs └── Microsoft.AspNetCore.WebSockets.Test │ ├── AddWebSocketsTests.cs │ ├── BufferStream.cs │ ├── DuplexStream.cs │ ├── IWebHostPortExtensions.cs │ ├── KestrelWebSocketHelpers.cs │ ├── Microsoft.AspNetCore.WebSockets.Test.csproj │ ├── SendReceiveTests.cs │ ├── WebSocketMiddlewareTests.cs │ └── WebSocketPair.cs └── version.props /.appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf true 3 | environment: 4 | AUTOBAHN_SUITES_LOG: 1 5 | global: 6 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 7 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 8 | ASPNETCORE_WSTEST_PATH: $(APPVEYOR_BUILD_FOLDER)/vendor/virtualenv/Scripts/wstest.exe 9 | cache: 10 | - vendor\VCForPython27.msi 11 | branches: 12 | only: 13 | - master 14 | - /^release\/.*$/ 15 | - /^(.*\/)?ci-.*$/ 16 | install: 17 | - ps: .\build\setup-wstest.ps1 18 | build_script: 19 | - ps: .\run.ps1 default-build 20 | clone_depth: 1 21 | test: 'off' 22 | deploy: 'off' 23 | os: Visual Studio 2017 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.doc diff=astextplain 2 | *.DOC diff=astextplain 3 | *.docx diff=astextplain 4 | *.DOCX diff=astextplain 5 | *.dot diff=astextplain 6 | *.DOT diff=astextplain 7 | *.pdf diff=astextplain 8 | *.PDF diff=astextplain 9 | *.rtf diff=astextplain 10 | *.RTF diff=astextplain 11 | 12 | *.jpg binary 13 | *.png binary 14 | *.gif binary 15 | 16 | *.cs text=auto diff=csharp 17 | *.vb text=auto 18 | *.resx text=auto 19 | *.c text=auto 20 | *.cpp text=auto 21 | *.cxx text=auto 22 | *.h text=auto 23 | *.hxx text=auto 24 | *.py text=auto 25 | *.rb text=auto 26 | *.java text=auto 27 | *.html text=auto 28 | *.htm text=auto 29 | *.css text=auto 30 | *.scss text=auto 31 | *.sass text=auto 32 | *.less text=auto 33 | *.js text=auto 34 | *.lisp text=auto 35 | *.clj text=auto 36 | *.sql text=auto 37 | *.php text=auto 38 | *.lua text=auto 39 | *.m text=auto 40 | *.asm text=auto 41 | *.erl text=auto 42 | *.fs text=auto 43 | *.fsx text=auto 44 | *.hs text=auto 45 | 46 | *.csproj text=auto 47 | *.vbproj text=auto 48 | *.fsproj text=auto 49 | *.dbproj text=auto 50 | *.sln text=auto eol=crlf 51 | 52 | *.sh eol=lf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | THIS ISSUE TRACKER IS CLOSED - please log new issues here: https://github.com/aspnet/Home/issues 2 | 3 | For information about this change, see https://github.com/aspnet/Announcements/issues/283 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | TestResults/ 4 | .nuget/ 5 | _ReSharper.*/ 6 | packages/ 7 | artifacts/ 8 | PublishProfiles/ 9 | *.user 10 | *.suo 11 | *.cache 12 | *.docstates 13 | _ReSharper.* 14 | nuget.exe 15 | *net45.csproj 16 | *k10.csproj 17 | *.psess 18 | *.vsp 19 | *.pidb 20 | *.userprefs 21 | *DS_Store 22 | *.ncrunchsolution 23 | *.*sdf 24 | *.ipch 25 | *.sln.ide 26 | /.vs/ 27 | .testPublish/ 28 | .build/ 29 | autobahnreports/ 30 | .vscode/ 31 | global.json 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | sudo: false 3 | dist: trusty 4 | env: 5 | global: 6 | - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 7 | - DOTNET_CLI_TELEMETRY_OPTOUT: 1 8 | - AUTOBAHN_SUITES_LOG: 1 9 | - ASPNETCORE_WSTEST_PATH: $TRAVIS_BUILD_DIR/.virtualenv/bin/wstest 10 | mono: none 11 | python: 12 | - pypy 13 | os: 14 | - linux 15 | - osx 16 | osx_image: xcode9.3 17 | addons: 18 | apt: 19 | packages: 20 | - libunwind8 21 | branches: 22 | only: 23 | - master 24 | - /^release\/.*$/ 25 | - /^(.*\/)?ci-.*$/ 26 | install: 27 | - ./build/setup-wstest.sh 28 | script: 29 | - ./build.sh 30 | -------------------------------------------------------------------------------- /.vsts-pipelines/builds/ci-internal.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | - release/* 4 | 5 | resources: 6 | repositories: 7 | - repository: buildtools 8 | type: git 9 | name: aspnet-BuildTools 10 | ref: refs/heads/master 11 | 12 | phases: 13 | - template: .vsts-pipelines/templates/project-ci.yml@buildtools 14 | -------------------------------------------------------------------------------- /.vsts-pipelines/builds/ci-public.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | - release/* 4 | 5 | # See https://github.com/aspnet/BuildTools 6 | resources: 7 | repositories: 8 | - repository: buildtools 9 | type: github 10 | endpoint: DotNet-Bot GitHub Connection 11 | name: aspnet/BuildTools 12 | ref: refs/heads/master 13 | 14 | phases: 15 | - template: .vsts-pipelines/templates/project-ci.yml@buildtools 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ====== 3 | 4 | Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/master/CONTRIBUTING.md) in the Home repo. 5 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Microsoft ASP.NET Core 12 | https://github.com/aspnet/WebSockets 13 | git 14 | $(MSBuildThisFileDirectory) 15 | $(MSBuildThisFileDirectory)build\Key.snk 16 | true 17 | true 18 | 19 | 20 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MicrosoftNETCoreApp20PackageVersion) 4 | $(MicrosoftNETCoreApp21PackageVersion) 5 | $(MicrosoftNETCoreApp22PackageVersion) 6 | $(NETStandardLibrary20PackageVersion) 7 | 8 | 99.9 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) .NET Foundation and Contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NuGetPackageVerifier.json: -------------------------------------------------------------------------------- 1 | { 2 | "Default": { 3 | "rules": [ 4 | "DefaultCompositeRule" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WebSockets [Archived] 2 | ====================== 3 | 4 | **This GitHub project has been archived.** Ongoing development on this project can be found in . 5 | 6 | Contains a managed implementation of the WebSocket protocol, along with server integration components. 7 | 8 | This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [AspNetCore](https://github.com/aspnet/AspNetCore) repo. 9 | 10 | 11 | ## System Requirements 12 | 13 | This repo has a few special system requirements/prerequisites. 14 | 15 | 1. Windows IIS Express tests require IIS Express 10 and Windows 8 for WebSockets support 16 | 2. HttpListener/ASP.NET 4.6 samples require at least Windows 8 17 | 3. Autobahn Test Suite requires special installation see the README.md in test/AutobahnTestApp 18 | -------------------------------------------------------------------------------- /WebSockets.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.26730.10 4 | MinimumVisualStudioVersion = 15.0.26730.03 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2C7947A5-9FBD-4267-97C1-2D726D7B3BAF}" 6 | ProjectSection(SolutionItems) = preProject 7 | src\Directory.Build.props = src\Directory.Build.props 8 | EndProjectSection 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C45106D0-76C8-4776-A140-F7DD83CA2958}" 11 | ProjectSection(SolutionItems) = preProject 12 | test\Directory.Build.props = test\Directory.Build.props 13 | EndProjectSection 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{9E55FC5B-FD9C-4266-AB24-F3AA649D7C8B}" 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestServer", "samples\TestServer\TestServer.csproj", "{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{19595D64-E42E-46FD-AB2E-BDC870724EE7}" 20 | ProjectSection(SolutionItems) = preProject 21 | scripts\UpdateCoreFxCode.ps1 = scripts\UpdateCoreFxCode.ps1 22 | EndProjectSection 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebSockets", "src\Microsoft.AspNetCore.WebSockets\Microsoft.AspNetCore.WebSockets.csproj", "{CDE16880-0374-46FA-8896-99F1B90B4B6F}" 25 | EndProject 26 | 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}" 27 | EndProject 28 | 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}" 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EchoApp", "samples\EchoApp\EchoApp.csproj", "{421954B0-5C6B-4092-8D4D-EACA4CE60AFB}" 31 | EndProject 32 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutobahnTestApp", "test\AutobahnTestApp\AutobahnTestApp.csproj", "{150DF5A8-87C6-42F7-8886-CE07BFD02FD2}" 33 | EndProject 34 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{92CE12E6-E127-433B-96D3-164C0113EA17}" 35 | ProjectSection(SolutionItems) = preProject 36 | build\dependencies.props = build\dependencies.props 37 | build\Key.snk = build\Key.snk 38 | build\repo.props = build\repo.props 39 | build\repo.targets = build\repo.targets 40 | EndProjectSection 41 | EndProject 42 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7A963B09-471B-4D67-B5C0-6039AF0C39EE}" 43 | ProjectSection(SolutionItems) = preProject 44 | Directory.Build.props = Directory.Build.props 45 | Directory.Build.targets = Directory.Build.targets 46 | EndProjectSection 47 | EndProject 48 | Global 49 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 50 | Debug|Any CPU = Debug|Any CPU 51 | Debug|x64 = Debug|x64 52 | Debug|x86 = Debug|x86 53 | Release|Any CPU = Release|Any CPU 54 | Release|x64 = Release|x64 55 | Release|x86 = Release|x86 56 | EndGlobalSection 57 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 58 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|x64.ActiveCfg = Debug|Any CPU 61 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|x64.Build.0 = Debug|Any CPU 62 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|x86.ActiveCfg = Debug|Any CPU 63 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|x86.Build.0 = Debug|Any CPU 64 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|Any CPU.Build.0 = Release|Any CPU 66 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|x64.ActiveCfg = Release|Any CPU 67 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|x64.Build.0 = Release|Any CPU 68 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|x86.ActiveCfg = Release|Any CPU 69 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|x86.Build.0 = Release|Any CPU 70 | {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 71 | {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Debug|Any CPU.Build.0 = Debug|Any CPU 72 | {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Debug|x64.ActiveCfg = Debug|Any CPU 73 | {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Debug|x64.Build.0 = Debug|Any CPU 74 | {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Debug|x86.ActiveCfg = Debug|Any CPU 75 | {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Debug|x86.Build.0 = Debug|Any CPU 76 | {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Release|Any CPU.ActiveCfg = Release|Any CPU 77 | {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Release|Any CPU.Build.0 = Release|Any CPU 78 | {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Release|x64.ActiveCfg = Release|Any CPU 79 | {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Release|x64.Build.0 = Release|Any CPU 80 | {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Release|x86.ActiveCfg = Release|Any CPU 81 | {CDE16880-0374-46FA-8896-99F1B90B4B6F}.Release|x86.Build.0 = Release|Any CPU 82 | {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 83 | {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Debug|Any CPU.Build.0 = Debug|Any CPU 84 | {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Debug|x64.ActiveCfg = Debug|Any CPU 85 | {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Debug|x64.Build.0 = Debug|Any CPU 86 | {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Debug|x86.ActiveCfg = Debug|Any CPU 87 | {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Debug|x86.Build.0 = Debug|Any CPU 88 | {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Release|Any CPU.ActiveCfg = Release|Any CPU 89 | {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Release|Any CPU.Build.0 = Release|Any CPU 90 | {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Release|x64.ActiveCfg = Release|Any CPU 91 | {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Release|x64.Build.0 = Release|Any CPU 92 | {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Release|x86.ActiveCfg = Release|Any CPU 93 | {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF}.Release|x86.Build.0 = Release|Any CPU 94 | {74F45408-1959-4FEE-9511-25D40F4913FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 95 | {74F45408-1959-4FEE-9511-25D40F4913FD}.Debug|Any CPU.Build.0 = Debug|Any CPU 96 | {74F45408-1959-4FEE-9511-25D40F4913FD}.Debug|x64.ActiveCfg = Debug|Any CPU 97 | {74F45408-1959-4FEE-9511-25D40F4913FD}.Debug|x64.Build.0 = Debug|Any CPU 98 | {74F45408-1959-4FEE-9511-25D40F4913FD}.Debug|x86.ActiveCfg = Debug|Any CPU 99 | {74F45408-1959-4FEE-9511-25D40F4913FD}.Debug|x86.Build.0 = Debug|Any CPU 100 | {74F45408-1959-4FEE-9511-25D40F4913FD}.Release|Any CPU.ActiveCfg = Release|Any CPU 101 | {74F45408-1959-4FEE-9511-25D40F4913FD}.Release|Any CPU.Build.0 = Release|Any CPU 102 | {74F45408-1959-4FEE-9511-25D40F4913FD}.Release|x64.ActiveCfg = Release|Any CPU 103 | {74F45408-1959-4FEE-9511-25D40F4913FD}.Release|x64.Build.0 = Release|Any CPU 104 | {74F45408-1959-4FEE-9511-25D40F4913FD}.Release|x86.ActiveCfg = Release|Any CPU 105 | {74F45408-1959-4FEE-9511-25D40F4913FD}.Release|x86.Build.0 = Release|Any CPU 106 | {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 107 | {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Debug|Any CPU.Build.0 = Debug|Any CPU 108 | {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Debug|x64.ActiveCfg = Debug|Any CPU 109 | {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Debug|x64.Build.0 = Debug|Any CPU 110 | {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Debug|x86.ActiveCfg = Debug|Any CPU 111 | {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Debug|x86.Build.0 = Debug|Any CPU 112 | {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Release|Any CPU.ActiveCfg = Release|Any CPU 113 | {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Release|Any CPU.Build.0 = Release|Any CPU 114 | {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Release|x64.ActiveCfg = Release|Any CPU 115 | {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Release|x64.Build.0 = Release|Any CPU 116 | {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Release|x86.ActiveCfg = Release|Any CPU 117 | {421954B0-5C6B-4092-8D4D-EACA4CE60AFB}.Release|x86.Build.0 = Release|Any CPU 118 | {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 119 | {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Debug|Any CPU.Build.0 = Debug|Any CPU 120 | {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Debug|x64.ActiveCfg = Debug|Any CPU 121 | {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Debug|x64.Build.0 = Debug|Any CPU 122 | {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Debug|x86.ActiveCfg = Debug|Any CPU 123 | {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Debug|x86.Build.0 = Debug|Any CPU 124 | {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Release|Any CPU.ActiveCfg = Release|Any CPU 125 | {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Release|Any CPU.Build.0 = Release|Any CPU 126 | {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Release|x64.ActiveCfg = Release|Any CPU 127 | {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Release|x64.Build.0 = Release|Any CPU 128 | {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Release|x86.ActiveCfg = Release|Any CPU 129 | {150DF5A8-87C6-42F7-8886-CE07BFD02FD2}.Release|x86.Build.0 = Release|Any CPU 130 | EndGlobalSection 131 | GlobalSection(SolutionProperties) = preSolution 132 | HideSolutionNode = FALSE 133 | EndGlobalSection 134 | GlobalSection(NestedProjects) = preSolution 135 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B} = {9E55FC5B-FD9C-4266-AB24-F3AA649D7C8B} 136 | {CDE16880-0374-46FA-8896-99F1B90B4B6F} = {2C7947A5-9FBD-4267-97C1-2D726D7B3BAF} 137 | {5AFA74F5-9B1D-4FC5-815F-EF471F5AC1EF} = {C45106D0-76C8-4776-A140-F7DD83CA2958} 138 | {74F45408-1959-4FEE-9511-25D40F4913FD} = {C45106D0-76C8-4776-A140-F7DD83CA2958} 139 | {421954B0-5C6B-4092-8D4D-EACA4CE60AFB} = {9E55FC5B-FD9C-4266-AB24-F3AA649D7C8B} 140 | {150DF5A8-87C6-42F7-8886-CE07BFD02FD2} = {C45106D0-76C8-4776-A140-F7DD83CA2958} 141 | EndGlobalSection 142 | GlobalSection(ExtensibilityGlobals) = postSolution 143 | SolutionGuid = {D3542868-F8C6-401B-8071-37FE3C981604} 144 | EndGlobalSection 145 | EndGlobal 146 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' default-build %*; exit $LASTEXITCODE" 3 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | 6 | # Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs) 7 | chmod +x "$DIR/run.sh"; sync 8 | "$DIR/run.sh" default-build "$@" 9 | -------------------------------------------------------------------------------- /build/Key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspnet/WebSockets/afeb7fe49bf1652108e4e0f9e0e3b1eeec84bfd4/build/Key.snk -------------------------------------------------------------------------------- /build/dependencies.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 4 | 5 | 6 | 3.0.0-alpha1-20181004.7 7 | 3.0.0-alpha1-10584 8 | 3.0.0-alpha1-10584 9 | 3.0.0-alpha1-10584 10 | 3.0.0-alpha1-10584 11 | 3.0.0-alpha1-10584 12 | 0.7.0-alpha1-10584 13 | 3.0.0-alpha1-10584 14 | 3.0.0-alpha1-10584 15 | 3.0.0-alpha1-10584 16 | 3.0.0-alpha1-10584 17 | 3.0.0-alpha1-10584 18 | 3.0.0-alpha1-10584 19 | 3.0.0-alpha1-10584 20 | 3.0.0-alpha1-10584 21 | 3.0.0-alpha1-10584 22 | 3.0.0-alpha1-10584 23 | 2.0.9 24 | 2.1.3 25 | 2.2.0-preview2-26905-02 26 | 15.6.1 27 | 2.0.3 28 | 4.6.0-preview1-26907-04 29 | 2.3.1 30 | 2.4.0 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /build/repo.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Internal.AspNetCore.Universe.Lineup 10 | https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /build/repo.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(ArtifactsDir)autobahnreports\ 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /build/setup-wstest.ps1: -------------------------------------------------------------------------------- 1 | function has($cmd) { !!(Get-Command $cmd -ErrorAction SilentlyContinue) } 2 | 3 | # Download VCForPython27 if necessary 4 | $VendorDir = Join-Path (Get-Location) "vendor" 5 | 6 | if(!(Test-Path $VendorDir)) { 7 | mkdir $VendorDir 8 | } 9 | 10 | $VirtualEnvDir = Join-Path $VendorDir "virtualenv"; 11 | $ScriptsDir = Join-Path $VirtualEnvDir "Scripts" 12 | $WsTest = Join-Path $ScriptsDir "wstest.exe" 13 | 14 | $VCPythonMsi = Join-Path $VendorDir "VCForPython27.msi" 15 | if(!(Test-Path $VCPythonMsi)) { 16 | Write-Host "Downloading VCForPython27.msi" 17 | Invoke-WebRequest -Uri https://download.microsoft.com/download/7/9/6/796EF2E4-801B-4FC4-AB28-B59FBF6D907B/VCForPython27.msi -OutFile "$VCPythonMsi" 18 | } 19 | else { 20 | Write-Host "Using VCForPython27.msi from Cache" 21 | } 22 | 23 | # Install VCForPython27 24 | Write-Host "Installing VCForPython27" 25 | 26 | # 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. 27 | Start-Process msiexec "/i","$VCPythonMsi","/qn","/quiet","/norestart" -Wait 28 | 29 | Write-Host "Installed VCForPython27" 30 | 31 | # Install Python 32 | if(!(has python)) { 33 | choco install python2 34 | } 35 | 36 | if(!(has python)) { 37 | throw "Failed to install python2" 38 | } 39 | 40 | # Install virtualenv 41 | pip install virtualenv 42 | 43 | # Make a virtualenv in .virtualenv 44 | virtualenv $VirtualEnvDir 45 | 46 | & "$ScriptsDir\python" --version 47 | & "$ScriptsDir\pip" --version 48 | 49 | # Install autobahn into the virtualenv 50 | & "$ScriptsDir\pip" install autobahntestsuite 51 | 52 | Write-Host "Using wstest from: '$WsTest'" -------------------------------------------------------------------------------- /build/setup-wstest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | type -p python 4 | python --version 5 | 6 | # Install local virtualenv 7 | mkdir .python 8 | cd .python 9 | curl -OL https://pypi.python.org/packages/d4/0c/9840c08189e030873387a73b90ada981885010dd9aea134d6de30cd24cb8/virtualenv-15.1.0.tar.gz 10 | tar xf virtualenv-15.1.0.tar.gz 11 | cd .. 12 | 13 | # Make a virtualenv 14 | python ./.python/virtualenv-15.1.0/virtualenv.py .virtualenv 15 | 16 | .virtualenv/bin/python --version 17 | .virtualenv/bin/pip --version 18 | 19 | # Install autobahn into the virtualenv 20 | .virtualenv/bin/pip install autobahntestsuite 21 | 22 | # We're done. The travis config has already established the path to WSTest should be within the virtualenv. 23 | ls -l .virtualenv/bin 24 | .virtualenv/bin/wstest --version -------------------------------------------------------------------------------- /build/sources.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $(DotNetRestoreSources) 6 | 7 | $(RestoreSources); 8 | https://dotnet.myget.org/F/dotnet-core/api/v3/index.json; 9 | https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; 10 | https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; 11 | 12 | 13 | $(RestoreSources); 14 | https://api.nuget.org/v3/index.json; 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /korebuild-lock.txt: -------------------------------------------------------------------------------- 1 | version:3.0.0-alpha1-20181004.7 2 | commithash:27fabdaf2b1d4753c3d2749581694ca65d78f7f2 3 | -------------------------------------------------------------------------------- /korebuild.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", 3 | "channel": "master" 4 | } 5 | -------------------------------------------------------------------------------- /run.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' %*; exit $LASTEXITCODE" 3 | -------------------------------------------------------------------------------- /run.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env powershell 2 | #requires -version 4 3 | 4 | <# 5 | .SYNOPSIS 6 | Executes KoreBuild commands. 7 | 8 | .DESCRIPTION 9 | Downloads korebuild if required. Then executes the KoreBuild command. To see available commands, execute with `-Command help`. 10 | 11 | .PARAMETER Command 12 | The KoreBuild command to run. 13 | 14 | .PARAMETER Path 15 | The folder to build. Defaults to the folder containing this script. 16 | 17 | .PARAMETER Channel 18 | The channel of KoreBuild to download. Overrides the value from the config file. 19 | 20 | .PARAMETER DotNetHome 21 | The directory where .NET Core tools will be stored. 22 | 23 | .PARAMETER ToolsSource 24 | The base url where build tools can be downloaded. Overrides the value from the config file. 25 | 26 | .PARAMETER Update 27 | Updates KoreBuild to the latest version even if a lock file is present. 28 | 29 | .PARAMETER Reinstall 30 | Re-installs KoreBuild 31 | 32 | .PARAMETER ConfigFile 33 | The path to the configuration file that stores values. Defaults to korebuild.json. 34 | 35 | .PARAMETER ToolsSourceSuffix 36 | The Suffix to append to the end of the ToolsSource. Useful for query strings in blob stores. 37 | 38 | .PARAMETER CI 39 | Sets up CI specific settings and variables. 40 | 41 | .PARAMETER Arguments 42 | Arguments to be passed to the command 43 | 44 | .NOTES 45 | This function will create a file $PSScriptRoot/korebuild-lock.txt. This lock file can be committed to source, but does not have to be. 46 | When the lockfile is not present, KoreBuild will create one using latest available version from $Channel. 47 | 48 | The $ConfigFile is expected to be an JSON file. It is optional, and the configuration values in it are optional as well. Any options set 49 | in the file are overridden by command line parameters. 50 | 51 | .EXAMPLE 52 | Example config file: 53 | ```json 54 | { 55 | "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", 56 | "channel": "master", 57 | "toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools" 58 | } 59 | ``` 60 | #> 61 | [CmdletBinding(PositionalBinding = $false)] 62 | param( 63 | [Parameter(Mandatory = $true, Position = 0)] 64 | [string]$Command, 65 | [string]$Path = $PSScriptRoot, 66 | [Alias('c')] 67 | [string]$Channel, 68 | [Alias('d')] 69 | [string]$DotNetHome, 70 | [Alias('s')] 71 | [string]$ToolsSource, 72 | [Alias('u')] 73 | [switch]$Update, 74 | [switch]$Reinstall, 75 | [string]$ToolsSourceSuffix, 76 | [string]$ConfigFile = $null, 77 | [switch]$CI, 78 | [Parameter(ValueFromRemainingArguments = $true)] 79 | [string[]]$Arguments 80 | ) 81 | 82 | Set-StrictMode -Version 2 83 | $ErrorActionPreference = 'Stop' 84 | 85 | # 86 | # Functions 87 | # 88 | 89 | function Get-KoreBuild { 90 | 91 | $lockFile = Join-Path $Path 'korebuild-lock.txt' 92 | 93 | if (!(Test-Path $lockFile) -or $Update) { 94 | Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile $ToolsSourceSuffix 95 | } 96 | 97 | $version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1 98 | if (!$version) { 99 | Write-Error "Failed to parse version from $lockFile. Expected a line that begins with 'version:'" 100 | } 101 | $version = $version.TrimStart('version:').Trim() 102 | $korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version) 103 | 104 | if ($Reinstall -and (Test-Path $korebuildPath)) { 105 | Remove-Item -Force -Recurse $korebuildPath 106 | } 107 | 108 | if (!(Test-Path $korebuildPath)) { 109 | Write-Host -ForegroundColor Magenta "Downloading KoreBuild $version" 110 | New-Item -ItemType Directory -Path $korebuildPath | Out-Null 111 | $remotePath = "$ToolsSource/korebuild/artifacts/$version/korebuild.$version.zip" 112 | 113 | try { 114 | $tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip" 115 | Get-RemoteFile $remotePath $tmpfile $ToolsSourceSuffix 116 | if (Get-Command -Name 'Microsoft.PowerShell.Archive\Expand-Archive' -ErrorAction Ignore) { 117 | # Use built-in commands where possible as they are cross-plat compatible 118 | Microsoft.PowerShell.Archive\Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath 119 | } 120 | else { 121 | # Fallback to old approach for old installations of PowerShell 122 | Add-Type -AssemblyName System.IO.Compression.FileSystem 123 | [System.IO.Compression.ZipFile]::ExtractToDirectory($tmpfile, $korebuildPath) 124 | } 125 | } 126 | catch { 127 | Remove-Item -Recurse -Force $korebuildPath -ErrorAction Ignore 128 | throw 129 | } 130 | finally { 131 | Remove-Item $tmpfile -ErrorAction Ignore 132 | } 133 | } 134 | 135 | return $korebuildPath 136 | } 137 | 138 | function Join-Paths([string]$path, [string[]]$childPaths) { 139 | $childPaths | ForEach-Object { $path = Join-Path $path $_ } 140 | return $path 141 | } 142 | 143 | function Get-RemoteFile([string]$RemotePath, [string]$LocalPath, [string]$RemoteSuffix) { 144 | if ($RemotePath -notlike 'http*') { 145 | Copy-Item $RemotePath $LocalPath 146 | return 147 | } 148 | 149 | $retries = 10 150 | while ($retries -gt 0) { 151 | $retries -= 1 152 | try { 153 | Invoke-WebRequest -UseBasicParsing -Uri $($RemotePath + $RemoteSuffix) -OutFile $LocalPath 154 | return 155 | } 156 | catch { 157 | Write-Verbose "Request failed. $retries retries remaining" 158 | } 159 | } 160 | 161 | Write-Error "Download failed: '$RemotePath'." 162 | } 163 | 164 | # 165 | # Main 166 | # 167 | 168 | # Load configuration or set defaults 169 | 170 | $Path = Resolve-Path $Path 171 | if (!$ConfigFile) { $ConfigFile = Join-Path $Path 'korebuild.json' } 172 | 173 | if (Test-Path $ConfigFile) { 174 | try { 175 | $config = Get-Content -Raw -Encoding UTF8 -Path $ConfigFile | ConvertFrom-Json 176 | if ($config) { 177 | if (!($Channel) -and (Get-Member -Name 'channel' -InputObject $config)) { [string] $Channel = $config.channel } 178 | if (!($ToolsSource) -and (Get-Member -Name 'toolsSource' -InputObject $config)) { [string] $ToolsSource = $config.toolsSource} 179 | } 180 | } 181 | catch { 182 | Write-Host -ForegroundColor Red $Error[0] 183 | Write-Error "$ConfigFile contains invalid JSON." 184 | exit 1 185 | } 186 | } 187 | 188 | if (!$DotNetHome) { 189 | $DotNetHome = if ($env:DOTNET_HOME) { $env:DOTNET_HOME } ` 190 | elseif ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dotnet'} ` 191 | elseif ($env:HOME) {Join-Path $env:HOME '.dotnet'}` 192 | else { Join-Path $PSScriptRoot '.dotnet'} 193 | } 194 | 195 | if (!$Channel) { $Channel = 'master' } 196 | if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' } 197 | 198 | # Execute 199 | 200 | $korebuildPath = Get-KoreBuild 201 | Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1') 202 | 203 | try { 204 | Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile -CI:$CI 205 | Invoke-KoreBuildCommand $Command @Arguments 206 | } 207 | finally { 208 | Remove-Module 'KoreBuild' -ErrorAction Ignore 209 | } 210 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # 6 | # variables 7 | # 8 | 9 | RESET="\033[0m" 10 | RED="\033[0;31m" 11 | YELLOW="\033[0;33m" 12 | MAGENTA="\033[0;95m" 13 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 14 | [ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet" 15 | verbose=false 16 | update=false 17 | reinstall=false 18 | repo_path="$DIR" 19 | channel='' 20 | tools_source='' 21 | tools_source_suffix='' 22 | ci=false 23 | 24 | # 25 | # Functions 26 | # 27 | __usage() { 28 | echo "Usage: $(basename "${BASH_SOURCE[0]}") command [options] [[--] ...]" 29 | echo "" 30 | echo "Arguments:" 31 | echo " command The command to be run." 32 | echo " ... Arguments passed to the command. Variable number of arguments allowed." 33 | echo "" 34 | echo "Options:" 35 | echo " --verbose Show verbose output." 36 | echo " -c|--channel The channel of KoreBuild to download. Overrides the value from the config file.." 37 | echo " --config-file The path to the configuration file that stores values. Defaults to korebuild.json." 38 | echo " -d|--dotnet-home The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet." 39 | echo " --path The directory to build. Defaults to the directory containing the script." 40 | echo " -s|--tools-source|-ToolsSource The base url where build tools can be downloaded. Overrides the value from the config file." 41 | echo " --tools-source-suffix|-ToolsSourceSuffix The suffix to append to tools-source. Useful for query strings." 42 | echo " -u|--update Update to the latest KoreBuild even if the lock file is present." 43 | echo " --reinstall Reinstall KoreBuild." 44 | echo " --ci Apply CI specific settings and environment variables." 45 | echo "" 46 | echo "Description:" 47 | echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be." 48 | echo " When the lockfile is not present, KoreBuild will create one using latest available version from \$channel." 49 | 50 | if [[ "${1:-}" != '--no-exit' ]]; then 51 | exit 2 52 | fi 53 | } 54 | 55 | get_korebuild() { 56 | local version 57 | local lock_file="$repo_path/korebuild-lock.txt" 58 | if [ ! -f "$lock_file" ] || [ "$update" = true ]; then 59 | __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" "$tools_source_suffix" 60 | fi 61 | version="$(grep 'version:*' -m 1 "$lock_file")" 62 | if [[ "$version" == '' ]]; then 63 | __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'" 64 | return 1 65 | fi 66 | version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" 67 | local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version" 68 | 69 | if [ "$reinstall" = true ] && [ -d "$korebuild_path" ]; then 70 | rm -rf "$korebuild_path" 71 | fi 72 | 73 | { 74 | if [ ! -d "$korebuild_path" ]; then 75 | mkdir -p "$korebuild_path" 76 | local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip" 77 | tmpfile="$(mktemp)" 78 | echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}" 79 | if __get_remote_file "$remote_path" "$tmpfile" "$tools_source_suffix"; then 80 | unzip -q -d "$korebuild_path" "$tmpfile" 81 | fi 82 | rm "$tmpfile" || true 83 | fi 84 | 85 | source "$korebuild_path/KoreBuild.sh" 86 | } || { 87 | if [ -d "$korebuild_path" ]; then 88 | echo "Cleaning up after failed installation" 89 | rm -rf "$korebuild_path" || true 90 | fi 91 | return 1 92 | } 93 | } 94 | 95 | __error() { 96 | echo -e "${RED}error: $*${RESET}" 1>&2 97 | } 98 | 99 | __warn() { 100 | echo -e "${YELLOW}warning: $*${RESET}" 101 | } 102 | 103 | __machine_has() { 104 | hash "$1" > /dev/null 2>&1 105 | return $? 106 | } 107 | 108 | __get_remote_file() { 109 | local remote_path=$1 110 | local local_path=$2 111 | local remote_path_suffix=$3 112 | 113 | if [[ "$remote_path" != 'http'* ]]; then 114 | cp "$remote_path" "$local_path" 115 | return 0 116 | fi 117 | 118 | local failed=false 119 | if __machine_has wget; then 120 | wget --tries 10 --quiet -O "$local_path" "${remote_path}${remote_path_suffix}" || failed=true 121 | else 122 | failed=true 123 | fi 124 | 125 | if [ "$failed" = true ] && __machine_has curl; then 126 | failed=false 127 | curl --retry 10 -sSL -f --create-dirs -o "$local_path" "${remote_path}${remote_path_suffix}" || failed=true 128 | fi 129 | 130 | if [ "$failed" = true ]; then 131 | __error "Download failed: $remote_path" 1>&2 132 | return 1 133 | fi 134 | } 135 | 136 | # 137 | # main 138 | # 139 | 140 | command="${1:-}" 141 | shift 142 | 143 | while [[ $# -gt 0 ]]; do 144 | case $1 in 145 | -\?|-h|--help) 146 | __usage --no-exit 147 | exit 0 148 | ;; 149 | -c|--channel|-Channel) 150 | shift 151 | channel="${1:-}" 152 | [ -z "$channel" ] && __usage 153 | ;; 154 | --config-file|-ConfigFile) 155 | shift 156 | config_file="${1:-}" 157 | [ -z "$config_file" ] && __usage 158 | if [ ! -f "$config_file" ]; then 159 | __error "Invalid value for --config-file. $config_file does not exist." 160 | exit 1 161 | fi 162 | ;; 163 | -d|--dotnet-home|-DotNetHome) 164 | shift 165 | DOTNET_HOME="${1:-}" 166 | [ -z "$DOTNET_HOME" ] && __usage 167 | ;; 168 | --path|-Path) 169 | shift 170 | repo_path="${1:-}" 171 | [ -z "$repo_path" ] && __usage 172 | ;; 173 | -s|--tools-source|-ToolsSource) 174 | shift 175 | tools_source="${1:-}" 176 | [ -z "$tools_source" ] && __usage 177 | ;; 178 | --tools-source-suffix|-ToolsSourceSuffix) 179 | shift 180 | tools_source_suffix="${1:-}" 181 | [ -z "$tools_source_suffix" ] && __usage 182 | ;; 183 | -u|--update|-Update) 184 | update=true 185 | ;; 186 | --reinstall|-[Rr]einstall) 187 | reinstall=true 188 | ;; 189 | --ci|-[Cc][Ii]) 190 | ci=true 191 | ;; 192 | --verbose|-Verbose) 193 | verbose=true 194 | ;; 195 | --) 196 | shift 197 | break 198 | ;; 199 | *) 200 | break 201 | ;; 202 | esac 203 | shift 204 | done 205 | 206 | if ! __machine_has unzip; then 207 | __error 'Missing required command: unzip' 208 | exit 1 209 | fi 210 | 211 | if ! __machine_has curl && ! __machine_has wget; then 212 | __error 'Missing required command. Either wget or curl is required.' 213 | exit 1 214 | fi 215 | 216 | [ -z "${config_file:-}" ] && config_file="$repo_path/korebuild.json" 217 | if [ -f "$config_file" ]; then 218 | if __machine_has jq ; then 219 | if jq '.' "$config_file" >/dev/null ; then 220 | config_channel="$(jq -r 'select(.channel!=null) | .channel' "$config_file")" 221 | config_tools_source="$(jq -r 'select(.toolsSource!=null) | .toolsSource' "$config_file")" 222 | else 223 | __error "$config_file contains invalid JSON." 224 | exit 1 225 | fi 226 | elif __machine_has python ; then 227 | if python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then 228 | config_channel="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" 229 | config_tools_source="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" 230 | else 231 | __error "$config_file contains invalid JSON." 232 | exit 1 233 | fi 234 | elif __machine_has python3 ; then 235 | if python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then 236 | config_channel="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" 237 | config_tools_source="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" 238 | else 239 | __error "$config_file contains invalid JSON." 240 | exit 1 241 | fi 242 | else 243 | __error 'Missing required command: jq or python. Could not parse the JSON file.' 244 | exit 1 245 | fi 246 | 247 | [ ! -z "${config_channel:-}" ] && channel="$config_channel" 248 | [ ! -z "${config_tools_source:-}" ] && tools_source="$config_tools_source" 249 | fi 250 | 251 | [ -z "$channel" ] && channel='master' 252 | [ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' 253 | 254 | get_korebuild 255 | set_korebuildsettings "$tools_source" "$DOTNET_HOME" "$repo_path" "$config_file" "$ci" 256 | invoke_korebuild_command "$command" "$@" 257 | -------------------------------------------------------------------------------- /samples/AutobahnTestAppAspNet4/AutobahnTestAppAspNet4.csproj.aspnet4: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Debug 8 | AnyCPU 9 | 10 | 11 | 2.0 12 | {72E3AB32-682F-42AF-B7C7-0B777244FF11} 13 | {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} 14 | Library 15 | Properties 16 | AutobahnTestAppAspNet4 17 | AutobahnTestAppAspNet4 18 | v4.6.1 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | true 30 | full 31 | false 32 | bin\ 33 | DEBUG;TRACE 34 | prompt 35 | 4 36 | 37 | 38 | pdbonly 39 | true 40 | bin\ 41 | TRACE 42 | prompt 43 | 4 44 | 45 | 46 | 47 | ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.1.0.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll 48 | True 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | Web.config 74 | 75 | 76 | Web.config 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | EchoSocket.ashx 85 | 86 | 87 | 88 | 89 | 10.0 90 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | True 100 | True 101 | 29392 102 | / 103 | http://localhost:29392 104 | True 105 | http://localhost:29392/EchoSocket.ashx 106 | False 107 | False 108 | 109 | 110 | False 111 | 112 | 113 | 114 | 115 | 116 | 117 | 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}. 118 | 119 | 120 | 121 | 122 | 129 | -------------------------------------------------------------------------------- /samples/AutobahnTestAppAspNet4/EchoSocket.ashx: -------------------------------------------------------------------------------- 1 | <%@ WebHandler Language="C#" CodeBehind="EchoSocket.ashx.cs" Class="AutobahnTestAppAspNet4.EchoSocket" %> 2 | -------------------------------------------------------------------------------- /samples/AutobahnTestAppAspNet4/EchoSocket.ashx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.WebSockets; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Web; 8 | 9 | namespace AutobahnTestAppAspNet4 10 | { 11 | /// 12 | /// Summary description for EchoSocket 13 | /// 14 | public class EchoSocket : IHttpHandler 15 | { 16 | public bool IsReusable => false; 17 | 18 | public void ProcessRequest(HttpContext context) 19 | { 20 | if (context.IsWebSocketRequest) 21 | { 22 | context.AcceptWebSocketRequest(async socketContext => 23 | { 24 | await Echo(socketContext.WebSocket); 25 | }); 26 | } 27 | else 28 | { 29 | context.Response.Write("Ready to accept WebSocket request at: " + context.Request.Url.ToString().Replace("https://", "wss://").Replace("http://", "ws://")); 30 | context.Response.Flush(); 31 | } 32 | } 33 | 34 | private async Task Echo(WebSocket webSocket) 35 | { 36 | var buffer = new byte[1024 * 4]; 37 | var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); 38 | while (!result.CloseStatus.HasValue) 39 | { 40 | await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); 41 | result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); 42 | } 43 | await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /samples/AutobahnTestAppAspNet4/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("AutobahnTestAppAspNet4")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("AutobahnTestAppAspNet4")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("72e3ab32-682f-42af-b7c7-0b777244ff11")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Revision and Build Numbers 33 | // by using the '*' as shown below: 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /samples/AutobahnTestAppAspNet4/Web.Debug.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 29 | 30 | -------------------------------------------------------------------------------- /samples/AutobahnTestAppAspNet4/Web.Release.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | 30 | 31 | -------------------------------------------------------------------------------- /samples/AutobahnTestAppAspNet4/Web.config: -------------------------------------------------------------------------------- 1 |  2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /samples/AutobahnTestAppAspNet4/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/AutobahnTestAppAspNet4/wstest-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": { "failByDrop": false }, 3 | "outdir": "./bin/wstest-report", 4 | "servers": [ 5 | { 6 | "agent": "ASP.NET 4", 7 | "url": "ws://localhost:29392/EchoSocket.ashx", 8 | "options": { "version": 18 } 9 | } 10 | ], 11 | "cases": ["*"], 12 | "exclude-cases": ["12.*", "13.*"], 13 | "exclude-agent-cases": {} 14 | } -------------------------------------------------------------------------------- /samples/AutobahnTestAppHttpListener/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /samples/AutobahnTestAppHttpListener/AutobahnTestAppHttpListener.csproj.net461: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {B7246F23-6A4B-492F-AB61-292AA1A9E9D5} 8 | Exe 9 | Properties 10 | AutobahnTestAppHttpListener 11 | AutobahnTestAppHttpListener 12 | v4.6.1 13 | 512 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 60 | -------------------------------------------------------------------------------- /samples/AutobahnTestAppHttpListener/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.WebSockets; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace AutobahnTestAppHttpListener 10 | { 11 | class Program 12 | { 13 | // This app only works on Windows 8+ 14 | static int Main(string[] args) 15 | { 16 | using (var listener = StartListener()) 17 | { 18 | if (listener == null) 19 | { 20 | return 1; 21 | } 22 | 23 | var httpUrl = listener.Prefixes.Single(); 24 | var wsUrl = httpUrl.Replace("http://", "ws://"); 25 | 26 | var stopTokenSource = new CancellationTokenSource(); 27 | var task = Run(listener, wsUrl, stopTokenSource.Token); 28 | 29 | Console.CancelKeyPress += (sender, a) => 30 | { 31 | a.Cancel = true; 32 | stopTokenSource.Cancel(); 33 | }; 34 | 35 | Console.WriteLine($"HTTP: {httpUrl}"); 36 | Console.WriteLine($"WS : {wsUrl}"); 37 | Console.WriteLine("Press Ctrl-C to stop..."); 38 | 39 | task.Wait(); 40 | } 41 | return 0; 42 | } 43 | 44 | private static async Task Run(HttpListener listener, string wsUrl, CancellationToken stopToken) 45 | { 46 | while (!stopToken.IsCancellationRequested) 47 | { 48 | try 49 | { 50 | var context = await listener.GetContextAsync(); 51 | 52 | if (context.Request.IsWebSocketRequest) 53 | { 54 | var socket = await context.AcceptWebSocketAsync(null); 55 | await Echo(socket.WebSocket); 56 | } 57 | else 58 | { 59 | using (var writer = new StreamWriter(context.Response.OutputStream)) 60 | { 61 | await writer.WriteLineAsync($"Ready to accept WebSocket request at: {wsUrl}"); 62 | } 63 | } 64 | } 65 | catch (Exception ex) 66 | { 67 | Console.WriteLine($"Request failed: {ex}"); 68 | } 69 | } 70 | } 71 | 72 | private static async Task Echo(WebSocket webSocket) 73 | { 74 | var buffer = new byte[1024 * 4]; 75 | var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); 76 | while (!result.CloseStatus.HasValue) 77 | { 78 | await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); 79 | result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); 80 | } 81 | await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); 82 | } 83 | 84 | static HttpListener StartListener() 85 | { 86 | var port = 49152; // IANA recommends starting at port 49152 for dynamic ports 87 | while (port < 65535) 88 | { 89 | HttpListener listener = new HttpListener(); 90 | listener.Prefixes.Add($"http://localhost:{port}/"); 91 | try 92 | { 93 | listener.Start(); 94 | return listener; 95 | } 96 | catch 97 | { 98 | port++; 99 | } 100 | } 101 | 102 | Console.Error.WriteLine("Failed to find a free port!"); 103 | return null; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /samples/AutobahnTestAppHttpListener/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("AutobahnTestAppHttpListener")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("AutobahnTestAppHttpListener")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("b7246f23-6a4b-492f-ab61-292aa1a9e9d5")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /samples/EchoApp/EchoApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2;net461 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /samples/EchoApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Hosting; 7 | 8 | namespace EchoApp 9 | { 10 | public class Program 11 | { 12 | public static void Main(string[] args) 13 | { 14 | var host = new WebHostBuilder() 15 | .UseKestrel() 16 | .UseContentRoot(Directory.GetCurrentDirectory()) 17 | .UseIISIntegration() 18 | .UseStartup() 19 | .Build(); 20 | 21 | host.Run(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/EchoApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:62225/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "EchoApp": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /samples/EchoApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.WebSockets; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Builder; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Logging; 13 | 14 | namespace EchoApp 15 | { 16 | public class Startup 17 | { 18 | // This method gets called by the runtime. Use this method to add services to the container. 19 | // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 20 | public void ConfigureServices(IServiceCollection services) 21 | { 22 | } 23 | 24 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 25 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 26 | { 27 | loggerFactory.AddConsole(LogLevel.Debug); 28 | 29 | if (env.IsDevelopment()) 30 | { 31 | app.UseDeveloperExceptionPage(); 32 | } 33 | 34 | app.UseWebSockets(); 35 | 36 | app.Use(async (context, next) => 37 | { 38 | if (context.WebSockets.IsWebSocketRequest) 39 | { 40 | var webSocket = await context.WebSockets.AcceptWebSocketAsync(); 41 | await Echo(context, webSocket, loggerFactory.CreateLogger("Echo")); 42 | } 43 | else 44 | { 45 | await next(); 46 | } 47 | }); 48 | 49 | app.UseFileServer(); 50 | } 51 | 52 | private async Task Echo(HttpContext context, WebSocket webSocket, ILogger logger) 53 | { 54 | var buffer = new byte[1024 * 4]; 55 | var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); 56 | LogFrame(logger, result, buffer); 57 | while (!result.CloseStatus.HasValue) 58 | { 59 | // If the client send "ServerClose", then they want a server-originated close to occur 60 | string content = "<>"; 61 | if (result.MessageType == WebSocketMessageType.Text) 62 | { 63 | content = Encoding.UTF8.GetString(buffer, 0, result.Count); 64 | if (content.Equals("ServerClose")) 65 | { 66 | await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing from Server", CancellationToken.None); 67 | logger.LogDebug($"Sent Frame Close: {WebSocketCloseStatus.NormalClosure} Closing from Server"); 68 | return; 69 | } 70 | else if (content.Equals("ServerAbort")) 71 | { 72 | context.Abort(); 73 | } 74 | } 75 | 76 | await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); 77 | logger.LogDebug($"Sent Frame {result.MessageType}: Len={result.Count}, Fin={result.EndOfMessage}: {content}"); 78 | 79 | result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); 80 | LogFrame(logger, result, buffer); 81 | } 82 | await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); 83 | } 84 | 85 | private void LogFrame(ILogger logger, WebSocketReceiveResult frame, byte[] buffer) 86 | { 87 | var close = frame.CloseStatus != null; 88 | string message; 89 | if (close) 90 | { 91 | message = $"Close: {frame.CloseStatus.Value} {frame.CloseStatusDescription}"; 92 | } 93 | else 94 | { 95 | string content = "<>"; 96 | if (frame.MessageType == WebSocketMessageType.Text) 97 | { 98 | content = Encoding.UTF8.GetString(buffer, 0, frame.Count); 99 | } 100 | message = $"{frame.MessageType}: Len={frame.Count}, Fin={frame.EndOfMessage}: {content}"; 101 | } 102 | logger.LogDebug("Received Frame " + message); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /samples/EchoApp/wwwroot/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 |

WebSocket Test Page

15 |

Ready to connect...

16 |
17 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 |

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

29 | 30 |

Communication Log

31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
FromToData
42 | 43 | 150 | 151 | -------------------------------------------------------------------------------- /samples/TestServer/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /samples/TestServer/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.WebSockets; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace TestServer 11 | { 12 | class Program 13 | { 14 | static void Main(string[] args) 15 | { 16 | RunEchoServer().Wait(); 17 | } 18 | 19 | private static async Task RunEchoServer() 20 | { 21 | HttpListener listener = new HttpListener(); 22 | listener.Prefixes.Add("http://localhost:12345/"); 23 | listener.Start(); 24 | Console.WriteLine("Started"); 25 | 26 | while (true) 27 | { 28 | HttpListenerContext context = listener.GetContext(); 29 | if (!context.Request.IsWebSocketRequest) 30 | { 31 | context.Response.Close(); 32 | continue; 33 | } 34 | Console.WriteLine("Accepted"); 35 | 36 | var wsContext = await context.AcceptWebSocketAsync(null); 37 | var webSocket = wsContext.WebSocket; 38 | 39 | byte[] buffer = new byte[1024]; 40 | WebSocketReceiveResult received = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); 41 | 42 | while (received.MessageType != WebSocketMessageType.Close) 43 | { 44 | Console.WriteLine($"Echoing {received.Count} bytes received in a {received.MessageType} message; Fin={received.EndOfMessage}"); 45 | // Echo anything we receive 46 | await webSocket.SendAsync(new ArraySegment(buffer, 0, received.Count), received.MessageType, received.EndOfMessage, CancellationToken.None); 47 | 48 | received = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); 49 | } 50 | 51 | await webSocket.CloseAsync(received.CloseStatus.Value, received.CloseStatusDescription, CancellationToken.None); 52 | 53 | webSocket.Dispose(); 54 | Console.WriteLine("Finished"); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /samples/TestServer/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("TestServer")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("TestServer")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("ffe69337-e3b4-4625-8244-36bd609742ba")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /samples/TestServer/TestServer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B} 8 | Exe 9 | Properties 10 | TestServer 11 | TestServer 12 | v4.6.1 13 | 512 14 | 15 | 16 | AnyCPU 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 58 | 59 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.WebSockets/ExtendedWebSocketAcceptContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace Microsoft.AspNetCore.WebSockets 8 | { 9 | public class ExtendedWebSocketAcceptContext : WebSocketAcceptContext 10 | { 11 | public override string SubProtocol { get; set; } 12 | public int? ReceiveBufferSize { get; set; } 13 | public TimeSpan? KeepAliveInterval { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.WebSockets/Internal/Constants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.AspNetCore.WebSockets.Internal 5 | { 6 | public static class Constants 7 | { 8 | public static class Headers 9 | { 10 | public const string Upgrade = "Upgrade"; 11 | public const string UpgradeWebSocket = "websocket"; 12 | public const string Connection = "Connection"; 13 | public const string ConnectionUpgrade = "Upgrade"; 14 | public const string SecWebSocketKey = "Sec-WebSocket-Key"; 15 | public const string SecWebSocketVersion = "Sec-WebSocket-Version"; 16 | public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; 17 | public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; 18 | public const string SupportedVersion = "13"; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.WebSockets/Internal/HandshakeHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using Microsoft.AspNetCore.Http; 9 | 10 | namespace Microsoft.AspNetCore.WebSockets.Internal 11 | { 12 | internal static class HandshakeHelpers 13 | { 14 | /// 15 | /// Gets request headers needed process the handshake on the server. 16 | /// 17 | public static readonly IEnumerable NeededHeaders = new[] 18 | { 19 | Constants.Headers.Upgrade, 20 | Constants.Headers.Connection, 21 | Constants.Headers.SecWebSocketKey, 22 | Constants.Headers.SecWebSocketVersion 23 | }; 24 | 25 | // Verify Method, Upgrade, Connection, version, key, etc.. 26 | public static bool CheckSupportedWebSocketRequest(string method, IEnumerable> headers) 27 | { 28 | bool validUpgrade = false, validConnection = false, validKey = false, validVersion = false; 29 | 30 | if (!string.Equals("GET", method, StringComparison.OrdinalIgnoreCase)) 31 | { 32 | return false; 33 | } 34 | 35 | foreach (var pair in headers) 36 | { 37 | if (string.Equals(Constants.Headers.Connection, pair.Key, StringComparison.OrdinalIgnoreCase)) 38 | { 39 | if (string.Equals(Constants.Headers.ConnectionUpgrade, pair.Value, StringComparison.OrdinalIgnoreCase)) 40 | { 41 | validConnection = true; 42 | } 43 | } 44 | else if (string.Equals(Constants.Headers.Upgrade, pair.Key, StringComparison.OrdinalIgnoreCase)) 45 | { 46 | if (string.Equals(Constants.Headers.UpgradeWebSocket, pair.Value, StringComparison.OrdinalIgnoreCase)) 47 | { 48 | validUpgrade = true; 49 | } 50 | } 51 | else if (string.Equals(Constants.Headers.SecWebSocketVersion, pair.Key, StringComparison.OrdinalIgnoreCase)) 52 | { 53 | if (string.Equals(Constants.Headers.SupportedVersion, pair.Value, StringComparison.OrdinalIgnoreCase)) 54 | { 55 | validVersion = true; 56 | } 57 | } 58 | else if (string.Equals(Constants.Headers.SecWebSocketKey, pair.Key, StringComparison.OrdinalIgnoreCase)) 59 | { 60 | validKey = IsRequestKeyValid(pair.Value); 61 | } 62 | } 63 | 64 | return validConnection && validUpgrade && validVersion && validKey; 65 | } 66 | 67 | public static void GenerateResponseHeaders(string key, string subProtocol, IHeaderDictionary headers) 68 | { 69 | headers[Constants.Headers.Connection] = Constants.Headers.ConnectionUpgrade; 70 | headers[Constants.Headers.Upgrade] = Constants.Headers.UpgradeWebSocket; 71 | headers[Constants.Headers.SecWebSocketAccept] = CreateResponseKey(key); 72 | if (!string.IsNullOrWhiteSpace(subProtocol)) 73 | { 74 | headers[Constants.Headers.SecWebSocketProtocol] = subProtocol; 75 | } 76 | } 77 | 78 | /// 79 | /// Validates the Sec-WebSocket-Key request header 80 | /// 81 | /// 82 | /// 83 | public static bool IsRequestKeyValid(string value) 84 | { 85 | if (string.IsNullOrWhiteSpace(value)) 86 | { 87 | return false; 88 | } 89 | try 90 | { 91 | byte[] data = Convert.FromBase64String(value); 92 | return data.Length == 16; 93 | } 94 | catch (Exception) 95 | { 96 | return false; 97 | } 98 | } 99 | 100 | public static string CreateResponseKey(string requestKey) 101 | { 102 | // "The value of this header field is constructed by concatenating /key/, defined above in step 4 103 | // in Section 4.2.2, with the string "258EAFA5- E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of 104 | // this concatenated value to obtain a 20-byte value and base64-encoding" 105 | // https://tools.ietf.org/html/rfc6455#section-4.2.2 106 | 107 | if (requestKey == null) 108 | { 109 | throw new ArgumentNullException(nameof(requestKey)); 110 | } 111 | 112 | using (var algorithm = SHA1.Create()) 113 | { 114 | string merged = requestKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 115 | byte[] mergedBytes = Encoding.UTF8.GetBytes(merged); 116 | byte[] hashedBytes = algorithm.ComputeHash(mergedBytes); 117 | return Convert.ToBase64String(hashedBytes); 118 | } 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.WebSockets/Microsoft.AspNetCore.WebSockets.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | ASP.NET Core web socket middleware for use on top of opaque servers. 5 | netstandard2.0 6 | $(NoWarn);CS1591 7 | true 8 | true 9 | aspnetcore 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.WebSockets/WebSocketMiddleware.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Net.WebSockets; 9 | using System.Threading.Tasks; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.AspNetCore.Http.Features; 13 | using Microsoft.AspNetCore.WebSockets.Internal; 14 | using Microsoft.Extensions.Logging; 15 | using Microsoft.Extensions.Logging.Abstractions; 16 | using Microsoft.Extensions.Options; 17 | using Microsoft.Extensions.Primitives; 18 | using Microsoft.Net.Http.Headers; 19 | 20 | namespace Microsoft.AspNetCore.WebSockets 21 | { 22 | public class WebSocketMiddleware 23 | { 24 | private readonly RequestDelegate _next; 25 | private readonly WebSocketOptions _options; 26 | private readonly ILogger _logger; 27 | private readonly bool _anyOriginAllowed; 28 | private readonly List _allowedOrigins; 29 | 30 | public WebSocketMiddleware(RequestDelegate next, IOptions options, ILoggerFactory loggerFactory) 31 | { 32 | if (next == null) 33 | { 34 | throw new ArgumentNullException(nameof(next)); 35 | } 36 | if (options == null) 37 | { 38 | throw new ArgumentNullException(nameof(options)); 39 | } 40 | 41 | _next = next; 42 | _options = options.Value; 43 | _allowedOrigins = _options.AllowedOrigins.Select(o => o.ToLowerInvariant()).ToList(); 44 | _anyOriginAllowed = _options.AllowedOrigins.Count == 0 || _options.AllowedOrigins.Contains("*", StringComparer.Ordinal); 45 | 46 | _logger = loggerFactory.CreateLogger(); 47 | 48 | // TODO: validate options. 49 | } 50 | 51 | [Obsolete("This constructor has been replaced with an equivalent constructor which requires an ILoggerFactory.")] 52 | public WebSocketMiddleware(RequestDelegate next, IOptions options) 53 | : this(next, options, NullLoggerFactory.Instance) 54 | { 55 | } 56 | 57 | public Task Invoke(HttpContext context) 58 | { 59 | // Detect if an opaque upgrade is available. If so, add a websocket upgrade. 60 | var upgradeFeature = context.Features.Get(); 61 | if (upgradeFeature != null && context.Features.Get() == null) 62 | { 63 | var webSocketFeature = new UpgradeHandshake(context, upgradeFeature, _options); 64 | context.Features.Set(webSocketFeature); 65 | 66 | if (!_anyOriginAllowed) 67 | { 68 | // Check for Origin header 69 | var originHeader = context.Request.Headers[HeaderNames.Origin]; 70 | 71 | if (!StringValues.IsNullOrEmpty(originHeader) && webSocketFeature.IsWebSocketRequest) 72 | { 73 | // Check allowed origins to see if request is allowed 74 | if (!_allowedOrigins.Contains(originHeader.ToString(), StringComparer.Ordinal)) 75 | { 76 | _logger.LogDebug("Request origin {Origin} is not in the list of allowed origins.", originHeader); 77 | context.Response.StatusCode = StatusCodes.Status403Forbidden; 78 | return Task.CompletedTask; 79 | } 80 | } 81 | } 82 | } 83 | 84 | return _next(context); 85 | } 86 | 87 | private class UpgradeHandshake : IHttpWebSocketFeature 88 | { 89 | private readonly HttpContext _context; 90 | private readonly IHttpUpgradeFeature _upgradeFeature; 91 | private readonly WebSocketOptions _options; 92 | private bool? _isWebSocketRequest; 93 | 94 | public UpgradeHandshake(HttpContext context, IHttpUpgradeFeature upgradeFeature, WebSocketOptions options) 95 | { 96 | _context = context; 97 | _upgradeFeature = upgradeFeature; 98 | _options = options; 99 | } 100 | 101 | public bool IsWebSocketRequest 102 | { 103 | get 104 | { 105 | if (_isWebSocketRequest == null) 106 | { 107 | if (!_upgradeFeature.IsUpgradableRequest) 108 | { 109 | _isWebSocketRequest = false; 110 | } 111 | else 112 | { 113 | var headers = new List>(); 114 | foreach (string headerName in HandshakeHelpers.NeededHeaders) 115 | { 116 | foreach (var value in _context.Request.Headers.GetCommaSeparatedValues(headerName)) 117 | { 118 | headers.Add(new KeyValuePair(headerName, value)); 119 | } 120 | } 121 | _isWebSocketRequest = HandshakeHelpers.CheckSupportedWebSocketRequest(_context.Request.Method, headers); 122 | } 123 | } 124 | return _isWebSocketRequest.Value; 125 | } 126 | } 127 | 128 | public async Task AcceptAsync(WebSocketAcceptContext acceptContext) 129 | { 130 | if (!IsWebSocketRequest) 131 | { 132 | throw new InvalidOperationException("Not a WebSocket request."); // TODO: LOC 133 | } 134 | 135 | string subProtocol = null; 136 | if (acceptContext != null) 137 | { 138 | subProtocol = acceptContext.SubProtocol; 139 | } 140 | 141 | TimeSpan keepAliveInterval = _options.KeepAliveInterval; 142 | int receiveBufferSize = _options.ReceiveBufferSize; 143 | var advancedAcceptContext = acceptContext as ExtendedWebSocketAcceptContext; 144 | if (advancedAcceptContext != null) 145 | { 146 | if (advancedAcceptContext.ReceiveBufferSize.HasValue) 147 | { 148 | receiveBufferSize = advancedAcceptContext.ReceiveBufferSize.Value; 149 | } 150 | if (advancedAcceptContext.KeepAliveInterval.HasValue) 151 | { 152 | keepAliveInterval = advancedAcceptContext.KeepAliveInterval.Value; 153 | } 154 | } 155 | 156 | string key = string.Join(", ", _context.Request.Headers[Constants.Headers.SecWebSocketKey]); 157 | 158 | HandshakeHelpers.GenerateResponseHeaders(key, subProtocol, _context.Response.Headers); 159 | 160 | Stream opaqueTransport = await _upgradeFeature.UpgradeAsync(); // Sets status code to 101 161 | 162 | return WebSocketProtocol.CreateFromStream(opaqueTransport, isServer: true, subProtocol: subProtocol, keepAliveInterval: keepAliveInterval); 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.WebSockets/WebSocketMiddlewareExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.WebSockets; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Microsoft.AspNetCore.Builder 9 | { 10 | public static class WebSocketMiddlewareExtensions 11 | { 12 | public static IApplicationBuilder UseWebSockets(this IApplicationBuilder app) 13 | { 14 | if (app == null) 15 | { 16 | throw new ArgumentNullException(nameof(app)); 17 | } 18 | 19 | return app.UseMiddleware(); 20 | } 21 | 22 | public static IApplicationBuilder UseWebSockets(this IApplicationBuilder app, WebSocketOptions options) 23 | { 24 | if (app == null) 25 | { 26 | throw new ArgumentNullException(nameof(app)); 27 | } 28 | if (options == null) 29 | { 30 | throw new ArgumentNullException(nameof(options)); 31 | } 32 | 33 | return app.UseMiddleware(Options.Create(options)); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.WebSockets/WebSocketOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace Microsoft.AspNetCore.Builder 8 | { 9 | /// 10 | /// Configuration options for the WebSocketMiddleware 11 | /// 12 | public class WebSocketOptions 13 | { 14 | public WebSocketOptions() 15 | { 16 | KeepAliveInterval = TimeSpan.FromMinutes(2); 17 | ReceiveBufferSize = 4 * 1024; 18 | AllowedOrigins = new List(); 19 | } 20 | 21 | /// 22 | /// Gets or sets the frequency at which to send Ping/Pong keep-alive control frames. 23 | /// The default is two minutes. 24 | /// 25 | public TimeSpan KeepAliveInterval { get; set; } 26 | 27 | /// 28 | /// Gets or sets the size of the protocol buffer used to receive and parse frames. 29 | /// The default is 4kb. 30 | /// 31 | public int ReceiveBufferSize { get; set; } 32 | 33 | /// 34 | /// Set the Origin header values allowed for WebSocket requests to prevent Cross-Site WebSocket Hijacking. 35 | /// By default all Origins are allowed. 36 | /// 37 | public IList AllowedOrigins { get; } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.WebSockets/WebSocketsDependencyInjectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace Microsoft.AspNetCore.WebSockets 9 | { 10 | public static class WebSocketsDependencyInjectionExtensions 11 | { 12 | public static IServiceCollection AddWebSockets(this IServiceCollection services, Action configure) 13 | { 14 | return services.Configure(configure); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.WebSockets/baseline.netcore.json: -------------------------------------------------------------------------------- 1 | { 2 | "AssemblyIdentity": "Microsoft.AspNetCore.WebSockets, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", 3 | "Types": [ 4 | { 5 | "Name": "Microsoft.AspNetCore.Builder.WebSocketMiddlewareExtensions", 6 | "Visibility": "Public", 7 | "Kind": "Class", 8 | "Abstract": true, 9 | "Static": true, 10 | "Sealed": true, 11 | "ImplementedInterfaces": [], 12 | "Members": [ 13 | { 14 | "Kind": "Method", 15 | "Name": "UseWebSockets", 16 | "Parameters": [ 17 | { 18 | "Name": "app", 19 | "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" 20 | } 21 | ], 22 | "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", 23 | "Static": true, 24 | "Extension": true, 25 | "Visibility": "Public", 26 | "GenericParameter": [] 27 | }, 28 | { 29 | "Kind": "Method", 30 | "Name": "UseWebSockets", 31 | "Parameters": [ 32 | { 33 | "Name": "app", 34 | "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" 35 | }, 36 | { 37 | "Name": "options", 38 | "Type": "Microsoft.AspNetCore.Builder.WebSocketOptions" 39 | } 40 | ], 41 | "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", 42 | "Static": true, 43 | "Extension": true, 44 | "Visibility": "Public", 45 | "GenericParameter": [] 46 | } 47 | ], 48 | "GenericParameters": [] 49 | }, 50 | { 51 | "Name": "Microsoft.AspNetCore.Builder.WebSocketOptions", 52 | "Visibility": "Public", 53 | "Kind": "Class", 54 | "ImplementedInterfaces": [], 55 | "Members": [ 56 | { 57 | "Kind": "Method", 58 | "Name": "get_KeepAliveInterval", 59 | "Parameters": [], 60 | "ReturnType": "System.TimeSpan", 61 | "Visibility": "Public", 62 | "GenericParameter": [] 63 | }, 64 | { 65 | "Kind": "Method", 66 | "Name": "set_KeepAliveInterval", 67 | "Parameters": [ 68 | { 69 | "Name": "value", 70 | "Type": "System.TimeSpan" 71 | } 72 | ], 73 | "ReturnType": "System.Void", 74 | "Visibility": "Public", 75 | "GenericParameter": [] 76 | }, 77 | { 78 | "Kind": "Method", 79 | "Name": "get_ReceiveBufferSize", 80 | "Parameters": [], 81 | "ReturnType": "System.Int32", 82 | "Visibility": "Public", 83 | "GenericParameter": [] 84 | }, 85 | { 86 | "Kind": "Method", 87 | "Name": "set_ReceiveBufferSize", 88 | "Parameters": [ 89 | { 90 | "Name": "value", 91 | "Type": "System.Int32" 92 | } 93 | ], 94 | "ReturnType": "System.Void", 95 | "Visibility": "Public", 96 | "GenericParameter": [] 97 | }, 98 | { 99 | "Kind": "Constructor", 100 | "Name": ".ctor", 101 | "Parameters": [], 102 | "Visibility": "Public", 103 | "GenericParameter": [] 104 | } 105 | ], 106 | "GenericParameters": [] 107 | }, 108 | { 109 | "Name": "Microsoft.AspNetCore.WebSockets.ExtendedWebSocketAcceptContext", 110 | "Visibility": "Public", 111 | "Kind": "Class", 112 | "BaseType": "Microsoft.AspNetCore.Http.WebSocketAcceptContext", 113 | "ImplementedInterfaces": [], 114 | "Members": [ 115 | { 116 | "Kind": "Method", 117 | "Name": "get_SubProtocol", 118 | "Parameters": [], 119 | "ReturnType": "System.String", 120 | "Virtual": true, 121 | "Override": true, 122 | "Visibility": "Public", 123 | "GenericParameter": [] 124 | }, 125 | { 126 | "Kind": "Method", 127 | "Name": "set_SubProtocol", 128 | "Parameters": [ 129 | { 130 | "Name": "value", 131 | "Type": "System.String" 132 | } 133 | ], 134 | "ReturnType": "System.Void", 135 | "Virtual": true, 136 | "Override": true, 137 | "Visibility": "Public", 138 | "GenericParameter": [] 139 | }, 140 | { 141 | "Kind": "Method", 142 | "Name": "get_ReceiveBufferSize", 143 | "Parameters": [], 144 | "ReturnType": "System.Nullable", 145 | "Visibility": "Public", 146 | "GenericParameter": [] 147 | }, 148 | { 149 | "Kind": "Method", 150 | "Name": "set_ReceiveBufferSize", 151 | "Parameters": [ 152 | { 153 | "Name": "value", 154 | "Type": "System.Nullable" 155 | } 156 | ], 157 | "ReturnType": "System.Void", 158 | "Visibility": "Public", 159 | "GenericParameter": [] 160 | }, 161 | { 162 | "Kind": "Method", 163 | "Name": "get_KeepAliveInterval", 164 | "Parameters": [], 165 | "ReturnType": "System.Nullable", 166 | "Visibility": "Public", 167 | "GenericParameter": [] 168 | }, 169 | { 170 | "Kind": "Method", 171 | "Name": "set_KeepAliveInterval", 172 | "Parameters": [ 173 | { 174 | "Name": "value", 175 | "Type": "System.Nullable" 176 | } 177 | ], 178 | "ReturnType": "System.Void", 179 | "Visibility": "Public", 180 | "GenericParameter": [] 181 | }, 182 | { 183 | "Kind": "Constructor", 184 | "Name": ".ctor", 185 | "Parameters": [], 186 | "Visibility": "Public", 187 | "GenericParameter": [] 188 | } 189 | ], 190 | "GenericParameters": [] 191 | }, 192 | { 193 | "Name": "Microsoft.AspNetCore.WebSockets.WebSocketMiddleware", 194 | "Visibility": "Public", 195 | "Kind": "Class", 196 | "ImplementedInterfaces": [], 197 | "Members": [ 198 | { 199 | "Kind": "Method", 200 | "Name": "Invoke", 201 | "Parameters": [ 202 | { 203 | "Name": "context", 204 | "Type": "Microsoft.AspNetCore.Http.HttpContext" 205 | } 206 | ], 207 | "ReturnType": "System.Threading.Tasks.Task", 208 | "Visibility": "Public", 209 | "GenericParameter": [] 210 | }, 211 | { 212 | "Kind": "Constructor", 213 | "Name": ".ctor", 214 | "Parameters": [ 215 | { 216 | "Name": "next", 217 | "Type": "Microsoft.AspNetCore.Http.RequestDelegate" 218 | }, 219 | { 220 | "Name": "options", 221 | "Type": "Microsoft.Extensions.Options.IOptions" 222 | } 223 | ], 224 | "Visibility": "Public", 225 | "GenericParameter": [] 226 | } 227 | ], 228 | "GenericParameters": [] 229 | } 230 | ] 231 | } -------------------------------------------------------------------------------- /test/AutobahnTestApp/AutobahnTestApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/AutobahnTestApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Runtime.Loader; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace AutobahnTestApp 10 | { 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | var scenarioName = "Unknown"; 16 | var config = new ConfigurationBuilder() 17 | .AddCommandLine(args) 18 | .Build(); 19 | 20 | var builder = new WebHostBuilder() 21 | .ConfigureLogging(loggingBuilder => loggingBuilder.AddConsole()) 22 | .UseConfiguration(config) 23 | .UseContentRoot(Directory.GetCurrentDirectory()) 24 | .UseIISIntegration() 25 | .UseStartup(); 26 | 27 | if (string.Equals(builder.GetSetting("server"), "Microsoft.AspNetCore.Server.HttpSys", System.StringComparison.Ordinal)) 28 | { 29 | scenarioName = "HttpSysServer"; 30 | Console.WriteLine("Using HttpSys server"); 31 | builder.UseHttpSys(); 32 | } 33 | else if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_PORT"))) 34 | { 35 | // ANCM is hosting the process. 36 | // The port will not yet be configured at this point, but will also not require HTTPS. 37 | scenarioName = "AspNetCoreModule"; 38 | Console.WriteLine("Detected ANCM, using Kestrel"); 39 | builder.UseKestrel(); 40 | } 41 | else 42 | { 43 | // Also check "server.urls" for back-compat. 44 | var urls = builder.GetSetting(WebHostDefaults.ServerUrlsKey) ?? builder.GetSetting("server.urls"); 45 | builder.UseSetting(WebHostDefaults.ServerUrlsKey, string.Empty); 46 | 47 | Console.WriteLine($"Using Kestrel, URL: {urls}"); 48 | 49 | if (urls.Contains(";")) 50 | { 51 | throw new NotSupportedException("This test app does not support multiple endpoints."); 52 | } 53 | 54 | var uri = new Uri(urls); 55 | 56 | builder.UseKestrel(options => 57 | { 58 | options.Listen(IPAddress.Loopback, uri.Port, listenOptions => 59 | { 60 | if (uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) 61 | { 62 | scenarioName = "Kestrel(SSL)"; 63 | var certPath = Path.Combine(AppContext.BaseDirectory, "TestResources", "testCert.pfx"); 64 | Console.WriteLine($"Using SSL with certificate: {certPath}"); 65 | listenOptions.UseHttps(certPath, "testPassword"); 66 | } 67 | else 68 | { 69 | scenarioName = "Kestrel(NonSSL)"; 70 | } 71 | }); 72 | }); 73 | } 74 | 75 | var host = builder.Build(); 76 | 77 | AppDomain.CurrentDomain.UnhandledException += (_, a) => 78 | { 79 | Console.WriteLine($"Unhandled exception (Scenario: {scenarioName}): {a.ExceptionObject.ToString()}"); 80 | }; 81 | 82 | Console.WriteLine($"Starting Server for Scenario: {scenarioName}"); 83 | host.Run(); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/AutobahnTestApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:6155/", 7 | "sslPort": 44371 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "ManagedSockets" 16 | } 17 | }, 18 | "AutobahnTestApp": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "ManagedSockets" 24 | } 25 | }, 26 | "AutobahnTestApp (SSL)": { 27 | "commandName": "Project", 28 | "launchBrowser": true, 29 | "launchUrl": "https://localhost:5443", 30 | "environmentVariables": { 31 | "ASPNETCORE_ENVIRONMENT": "ManagedSockets" 32 | } 33 | }, 34 | "WebListener": { 35 | "commandName": "Project", 36 | "commandLineArgs": "--server Microsoft.AspNetCore.Server.HttpSys", 37 | "environmentVariables": { 38 | "ASPNETCORE_ENVIRONMENT": "ManagedSockets" 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /test/AutobahnTestApp/README.md: -------------------------------------------------------------------------------- 1 | # Autobahn Testing 2 | 3 | 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. 4 | 5 | In order to run these tests you must install CPython 2.7, Pip, and the test suite modules. You must also have 6 | the `wstest` executable provided by the Autobahn Suite on the `PATH`. See http://autobahn.ws/testsuite/installation.html#installation for more info 7 | 8 | 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: 9 | 10 | ``` 11 | > .\scripts\RunAutobahnTests.ps1 -ServerUrl ws://server:1234 12 | ``` 13 | 14 | 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. 15 | -------------------------------------------------------------------------------- /test/AutobahnTestApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.WebSockets; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace AutobahnTestApp 11 | { 12 | public class Startup 13 | { 14 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 15 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 16 | { 17 | app.UseWebSockets(); 18 | 19 | var logger = loggerFactory.CreateLogger(); 20 | app.Use(async (context, next) => 21 | { 22 | if (context.WebSockets.IsWebSocketRequest) 23 | { 24 | logger.LogInformation("Received WebSocket request"); 25 | using (var webSocket = await context.WebSockets.AcceptWebSocketAsync()) 26 | { 27 | await Echo(webSocket, context.RequestAborted); 28 | } 29 | } 30 | else 31 | { 32 | var wsScheme = context.Request.IsHttps ? "wss" : "ws"; 33 | var wsUrl = $"{wsScheme}://{context.Request.Host.Host}:{context.Request.Host.Port}{context.Request.Path}"; 34 | await context.Response.WriteAsync($"Ready to accept a WebSocket request at: {wsUrl}"); 35 | } 36 | }); 37 | 38 | } 39 | 40 | private async Task Echo(WebSocket webSocket, CancellationToken cancellationToken) 41 | { 42 | var buffer = new byte[1024 * 4]; 43 | var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); 44 | while (!result.CloseStatus.HasValue) 45 | { 46 | await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancellationToken); 47 | result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); 48 | } 49 | await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, cancellationToken); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/AutobahnTestApp/TestResources/testCert.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspnet/WebSockets/afeb7fe49bf1652108e4e0f9e0e3b1eeec84bfd4/test/AutobahnTestApp/TestResources/testCert.pfx -------------------------------------------------------------------------------- /test/AutobahnTestApp/TestResources/testCert.txt: -------------------------------------------------------------------------------- 1 | The password for this is 'testPassword' 2 | 3 | DO NOT EVER TRUST THIS CERT. The private key for it is publicly released. -------------------------------------------------------------------------------- /test/AutobahnTestApp/scripts/RunAutobahnTests.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # RunAutobahnTests.ps1 3 | # 4 | param([Parameter(Mandatory=$true)][string]$ServerUrl, [string[]]$Cases = @("*"), [string]$OutputDir, [int]$Iterations = 1) 5 | 6 | if(!(Get-Command wstest -ErrorAction SilentlyContinue)) { 7 | throw "Missing required command 'wstest'. See README.md in Microsoft.AspNetCore.WebSockets.Server.Test project for information on installing Autobahn Test Suite." 8 | } 9 | 10 | if(!$OutputDir) { 11 | $OutputDir = Convert-Path "." 12 | $OutputDir = Join-Path $OutputDir "autobahnreports" 13 | } 14 | 15 | Write-Host "Launching Autobahn Test Suite ($Iterations iteration(s))..." 16 | 17 | 0..($Iterations-1) | % { 18 | $iteration = $_ 19 | 20 | $Spec = Convert-Path (Join-Path $PSScriptRoot "autobahn.spec.json") 21 | 22 | $CasesArray = [string]::Join(",", @($Cases | ForEach-Object { "`"$_`"" })) 23 | 24 | $SpecJson = [IO.File]::ReadAllText($Spec).Replace("OUTPUTDIR", $OutputDir.Replace("\", "\\")).Replace("WEBSOCKETURL", $ServerUrl).Replace("`"CASES`"", $CasesArray) 25 | 26 | $TempFile = [IO.Path]::GetTempFileName() 27 | 28 | try { 29 | [IO.File]::WriteAllText($TempFile, $SpecJson) 30 | $wstestOutput = & wstest -m fuzzingclient -s $TempFile 31 | } finally { 32 | if(Test-Path $TempFile) { 33 | rm $TempFile 34 | } 35 | } 36 | 37 | $report = ConvertFrom-Json ([IO.File]::ReadAllText((Convert-Path (Join-Path $OutputDir "index.json")))) 38 | 39 | $report.Server | gm | ? { $_.MemberType -eq "NoteProperty" } | % { 40 | $case = $report.Server."$($_.Name)" 41 | Write-Host "[#$($iteration.ToString().PadRight(2))] [$($case.behavior.PadRight(6))] Case $($_.Name)" 42 | } 43 | } -------------------------------------------------------------------------------- /test/AutobahnTestApp/scripts/autobahn.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": { "failByDrop": false }, 3 | "outdir": "OUTPUTDIR", 4 | "servers": [ 5 | { 6 | "agent": "Server", 7 | "url": "WEBSOCKETURL", 8 | "options": { "version": 18 } 9 | } 10 | ], 11 | "cases": ["CASES"], 12 | "exclude-cases": ["12.*", "13.*"], 13 | "exclude-agent-cases": {} 14 | } 15 | -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | netcoreapp2.2 6 | $(DeveloperBuildTestTfms) 7 | 8 | $(StandardTestTfms);net461 9 | 10 | 11 | 12 | 16 | true 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnCaseResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn 6 | { 7 | public class AutobahnCaseResult 8 | { 9 | public string Name { get; } 10 | public string ActualBehavior { get; } 11 | 12 | public AutobahnCaseResult(string name, string actualBehavior) 13 | { 14 | Name = name; 15 | ActualBehavior = actualBehavior; 16 | } 17 | 18 | public static AutobahnCaseResult FromJson(JProperty prop) 19 | { 20 | var caseObj = (JObject)prop.Value; 21 | var actualBehavior = (string)caseObj["behavior"]; 22 | return new AutobahnCaseResult(prop.Name, actualBehavior); 23 | } 24 | 25 | public bool BehaviorIs(params string[] behaviors) 26 | { 27 | return behaviors.Any(b => string.Equals(b, ActualBehavior, StringComparison.Ordinal)); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnExpectations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.AspNetCore.Server.IntegrationTesting; 5 | 6 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn 7 | { 8 | public class AutobahnExpectations 9 | { 10 | private Dictionary _expectations = new Dictionary(); 11 | public bool Ssl { get; } 12 | public ServerType Server { get; } 13 | public string Environment { get; } 14 | 15 | public AutobahnExpectations(ServerType server, bool ssl, string environment) 16 | { 17 | Server = server; 18 | Ssl = ssl; 19 | Environment = environment; 20 | } 21 | 22 | public AutobahnExpectations Fail(params string[] caseSpecs) => Expect(Expectation.Fail, caseSpecs); 23 | public AutobahnExpectations NonStrict(params string[] caseSpecs) => Expect(Expectation.NonStrict, caseSpecs); 24 | public AutobahnExpectations OkOrFail(params string[] caseSpecs) => Expect(Expectation.OkOrFail, caseSpecs); 25 | 26 | public AutobahnExpectations Expect(Expectation expectation, params string[] caseSpecs) 27 | { 28 | foreach (var caseSpec in caseSpecs) 29 | { 30 | _expectations[caseSpec] = expectation; 31 | } 32 | return this; 33 | } 34 | 35 | internal void Verify(AutobahnServerResult serverResult, StringBuilder failures) 36 | { 37 | foreach (var caseResult in serverResult.Cases) 38 | { 39 | // If this is an informational test result, we can't compare it to anything 40 | if (!string.Equals(caseResult.ActualBehavior, "INFORMATIONAL", StringComparison.Ordinal)) 41 | { 42 | Expectation expectation; 43 | if (!_expectations.TryGetValue(caseResult.Name, out expectation)) 44 | { 45 | expectation = Expectation.Ok; 46 | } 47 | 48 | switch (expectation) 49 | { 50 | case Expectation.Fail: 51 | if (!caseResult.BehaviorIs("FAILED")) 52 | { 53 | failures.AppendLine($"Case {serverResult.Name}:{caseResult.Name}. Expected 'FAILED', but got '{caseResult.ActualBehavior}'"); 54 | } 55 | break; 56 | case Expectation.NonStrict: 57 | if (!caseResult.BehaviorIs("NON-STRICT")) 58 | { 59 | failures.AppendLine($"Case {serverResult.Name}:{caseResult.Name}. Expected 'NON-STRICT', but got '{caseResult.ActualBehavior}'"); 60 | } 61 | break; 62 | case Expectation.Ok: 63 | if (!caseResult.BehaviorIs("NON-STRICT") && !caseResult.BehaviorIs("OK")) 64 | { 65 | failures.AppendLine($"Case {serverResult.Name}:{caseResult.Name}. Expected 'NON-STRICT' or 'OK', but got '{caseResult.ActualBehavior}'"); 66 | } 67 | break; 68 | case Expectation.OkOrFail: 69 | if (!caseResult.BehaviorIs("NON-STRICT") && !caseResult.BehaviorIs("FAILED") && !caseResult.BehaviorIs("OK")) 70 | { 71 | failures.AppendLine($"Case {serverResult.Name}:{caseResult.Name}. Expected 'FAILED', 'NON-STRICT' or 'OK', but got '{caseResult.ActualBehavior}'"); 72 | } 73 | break; 74 | default: 75 | break; 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn 6 | { 7 | public class AutobahnResult 8 | { 9 | public IEnumerable Servers { get; } 10 | 11 | public AutobahnResult(IEnumerable servers) 12 | { 13 | Servers = servers; 14 | } 15 | 16 | public static AutobahnResult FromReportJson(JObject indexJson) 17 | { 18 | // Load the report 19 | return new AutobahnResult(indexJson.Properties().Select(AutobahnServerResult.FromJson)); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnServerResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Server.IntegrationTesting; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn 8 | { 9 | public class AutobahnServerResult 10 | { 11 | public ServerType Server { get; } 12 | public bool Ssl { get; } 13 | public string Environment { get; } 14 | public string Name { get; } 15 | public IEnumerable Cases { get; } 16 | 17 | public AutobahnServerResult(string name, IEnumerable cases) 18 | { 19 | Name = name; 20 | 21 | var splat = name.Split('|'); 22 | if (splat.Length < 3) 23 | { 24 | throw new FormatException("Results incorrectly formatted"); 25 | } 26 | 27 | Server = (ServerType)Enum.Parse(typeof(ServerType), splat[0]); 28 | Ssl = string.Equals(splat[1], "SSL", StringComparison.Ordinal); 29 | Environment = splat[2]; 30 | Cases = cases; 31 | } 32 | 33 | public static AutobahnServerResult FromJson(JProperty prop) 34 | { 35 | var valueObj = ((JObject)prop.Value); 36 | return new AutobahnServerResult(prop.Name, valueObj.Properties().Select(AutobahnCaseResult.FromJson)); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnSpec.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn 8 | { 9 | public class AutobahnSpec 10 | { 11 | public string OutputDirectory { get; } 12 | public IList Servers { get; } = new List(); 13 | public IList Cases { get; } = new List(); 14 | public IList ExcludedCases { get; } = new List(); 15 | 16 | public AutobahnSpec(string outputDirectory) 17 | { 18 | OutputDirectory = outputDirectory; 19 | } 20 | 21 | public AutobahnSpec WithServer(string name, string url) 22 | { 23 | Servers.Add(new ServerSpec(name, url)); 24 | return this; 25 | } 26 | 27 | public AutobahnSpec IncludeCase(params string[] caseSpecs) 28 | { 29 | foreach (var caseSpec in caseSpecs) 30 | { 31 | Cases.Add(caseSpec); 32 | } 33 | return this; 34 | } 35 | 36 | public AutobahnSpec ExcludeCase(params string[] caseSpecs) 37 | { 38 | foreach (var caseSpec in caseSpecs) 39 | { 40 | ExcludedCases.Add(caseSpec); 41 | } 42 | return this; 43 | } 44 | 45 | public void WriteJson(string file) 46 | { 47 | File.WriteAllText(file, GetJson().ToString(Formatting.Indented)); 48 | } 49 | 50 | public JObject GetJson() => new JObject( 51 | new JProperty("options", new JObject( 52 | new JProperty("failByDrop", false))), 53 | new JProperty("outdir", OutputDirectory), 54 | new JProperty("servers", new JArray(Servers.Select(s => s.GetJson()).ToArray())), 55 | new JProperty("cases", new JArray(Cases.ToArray())), 56 | new JProperty("exclude-cases", new JArray(ExcludedCases.ToArray())), 57 | new JProperty("exclude-agent-cases", new JObject())); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/AutobahnTester.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Security.Authentication; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.AspNetCore.Server.IntegrationTesting; 11 | using Microsoft.Extensions.Logging; 12 | using Newtonsoft.Json.Linq; 13 | using Xunit; 14 | 15 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn 16 | { 17 | public class AutobahnTester : IDisposable 18 | { 19 | private readonly List _deployers = new List(); 20 | private readonly List _deployments = new List(); 21 | private readonly List _expectations = new List(); 22 | private readonly ILoggerFactory _loggerFactory; 23 | private readonly ILogger _logger; 24 | 25 | public AutobahnSpec Spec { get; } 26 | 27 | public AutobahnTester(ILoggerFactory loggerFactory, AutobahnSpec baseSpec) 28 | { 29 | _loggerFactory = loggerFactory; 30 | _logger = _loggerFactory.CreateLogger("AutobahnTester"); 31 | 32 | Spec = baseSpec; 33 | } 34 | 35 | public async Task Run(CancellationToken cancellationToken) 36 | { 37 | var specFile = Path.GetTempFileName(); 38 | try 39 | { 40 | // Start pinging the servers to see that they're still running 41 | var pingCts = new CancellationTokenSource(); 42 | var pinger = new Timer(state => Pinger((CancellationToken)state), pingCts.Token, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)); 43 | 44 | Spec.WriteJson(specFile); 45 | 46 | // Run the test (write something to the console so people know this will take a while...) 47 | _logger.LogInformation("Using 'wstest' from: {WsTestPath}", Wstest.Default.Location); 48 | _logger.LogInformation("Now launching Autobahn Test Suite. This will take a while."); 49 | var exitCode = await Wstest.Default.ExecAsync("-m fuzzingclient -s " + specFile, cancellationToken, _loggerFactory.CreateLogger("wstest")); 50 | if (exitCode != 0) 51 | { 52 | throw new Exception("wstest failed"); 53 | } 54 | 55 | pingCts.Cancel(); 56 | } 57 | finally 58 | { 59 | if (File.Exists(specFile)) 60 | { 61 | File.Delete(specFile); 62 | } 63 | } 64 | 65 | cancellationToken.ThrowIfCancellationRequested(); 66 | 67 | // Parse the output. 68 | var outputFile = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", Spec.OutputDirectory, "index.json"); 69 | using (var reader = new StreamReader(File.OpenRead(outputFile))) 70 | { 71 | return AutobahnResult.FromReportJson(JObject.Parse(await reader.ReadToEndAsync())); 72 | } 73 | } 74 | 75 | // Async void! It's OK here because we are running in a timer. We're just using async void to chain continuations. 76 | // There's nobody to await this, hence async void. 77 | private async void Pinger(CancellationToken token) 78 | { 79 | try 80 | { 81 | while (!token.IsCancellationRequested) 82 | { 83 | try 84 | { 85 | foreach (var deployment in _deployments) 86 | { 87 | if (token.IsCancellationRequested) 88 | { 89 | return; 90 | } 91 | 92 | var resp = await deployment.HttpClient.GetAsync("/ping", token); 93 | if (!resp.IsSuccessStatusCode) 94 | { 95 | _logger.LogWarning("Non-successful response when pinging {url}: {statusCode} {reasonPhrase}", deployment.ApplicationBaseUri, resp.StatusCode, resp.ReasonPhrase); 96 | } 97 | } 98 | } 99 | catch (OperationCanceledException) 100 | { 101 | // We don't want to throw when the token fires, just stop. 102 | } 103 | } 104 | } 105 | catch (Exception ex) 106 | { 107 | _logger.LogError(ex, "Error while pinging servers"); 108 | } 109 | } 110 | 111 | public void Verify(AutobahnResult result) 112 | { 113 | var failures = new StringBuilder(); 114 | foreach (var serverResult in result.Servers) 115 | { 116 | var serverExpectation = _expectations.FirstOrDefault(e => e.Server == serverResult.Server && e.Ssl == serverResult.Ssl); 117 | if (serverExpectation == null) 118 | { 119 | failures.AppendLine($"Expected no results for server: {serverResult.Name} but found results!"); 120 | } 121 | else 122 | { 123 | serverExpectation.Verify(serverResult, failures); 124 | } 125 | } 126 | 127 | Assert.True(failures.Length == 0, "Autobahn results did not meet expectations:" + Environment.NewLine + failures.ToString()); 128 | } 129 | 130 | public async Task DeployTestAndAddToSpec(ServerType server, bool ssl, string environment, CancellationToken cancellationToken, Action expectationConfig = null) 131 | { 132 | var sslNamePart = ssl ? "SSL" : "NoSSL"; 133 | var name = $"{server}|{sslNamePart}|{environment}"; 134 | var logger = _loggerFactory.CreateLogger($"AutobahnTestApp:{server}:{sslNamePart}:{environment}"); 135 | 136 | var appPath = Helpers.GetApplicationPath("AutobahnTestApp"); 137 | var configPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "Http.config"); 138 | var targetFramework = 139 | #if NETCOREAPP2_2 140 | "netcoreapp2.2"; 141 | #else 142 | #error Target frameworks need to be updated 143 | #endif 144 | var parameters = new DeploymentParameters(appPath, server, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64) 145 | { 146 | Scheme = (ssl ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), 147 | ApplicationType = ApplicationType.Portable, 148 | TargetFramework = targetFramework, 149 | EnvironmentName = environment, 150 | SiteName = "HttpTestSite", // This is configured in the Http.config 151 | ServerConfigTemplateContent = (server == ServerType.IISExpress) ? File.ReadAllText(configPath) : null, 152 | }; 153 | 154 | var deployer = ApplicationDeployerFactory.Create(parameters, _loggerFactory); 155 | var result = await deployer.DeployAsync(); 156 | _deployers.Add(deployer); 157 | _deployments.Add(result); 158 | cancellationToken.ThrowIfCancellationRequested(); 159 | 160 | var handler = new HttpClientHandler(); 161 | // Win7 HttpClient on NetCoreApp2.2 defaults to TLS 1.0 and won't connect to Kestrel. https://github.com/dotnet/corefx/issues/28733 162 | // Mac HttpClient on NetCoreApp2.0 doesn't alow you to set some combinations. 163 | // https://github.com/dotnet/corefx/blob/586cffcdfdf23ad6c193a4bf37fce88a1bf69508/src/System.Net.Http/src/System/Net/Http/CurlHandler/CurlHandler.SslProvider.OSX.cs#L104-L106 164 | handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls; 165 | handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; 166 | var client = result.CreateHttpClient(handler); 167 | 168 | // Make sure the server works 169 | var resp = await RetryHelper.RetryRequest(() => 170 | { 171 | cancellationToken.ThrowIfCancellationRequested(); 172 | return client.GetAsync(result.ApplicationBaseUri); 173 | }, logger, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, result.HostShutdownToken).Token); 174 | resp.EnsureSuccessStatusCode(); 175 | 176 | cancellationToken.ThrowIfCancellationRequested(); 177 | 178 | // Add to the current spec 179 | var wsUrl = result.ApplicationBaseUri.Replace("https://", "wss://").Replace("http://", "ws://"); 180 | Spec.WithServer(name, wsUrl); 181 | 182 | var expectations = new AutobahnExpectations(server, ssl, environment); 183 | expectationConfig?.Invoke(expectations); 184 | _expectations.Add(expectations); 185 | 186 | cancellationToken.ThrowIfCancellationRequested(); 187 | } 188 | 189 | public void Dispose() 190 | { 191 | foreach (var deployer in _deployers) 192 | { 193 | deployer.Dispose(); 194 | } 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Executable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Runtime.InteropServices; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn 10 | { 11 | public class Executable 12 | { 13 | private static readonly string _exeSuffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty; 14 | 15 | public string Location { get; } 16 | 17 | protected Executable(string path) 18 | { 19 | Location = path; 20 | } 21 | 22 | public static string Locate(string name) 23 | { 24 | foreach (var dir in Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator)) 25 | { 26 | var candidate = Path.Combine(dir, name + _exeSuffix); 27 | if (File.Exists(candidate)) 28 | { 29 | return candidate; 30 | } 31 | } 32 | return null; 33 | } 34 | 35 | public async Task ExecAsync(string args, CancellationToken cancellationToken, ILogger logger) 36 | { 37 | var process = new Process() 38 | { 39 | StartInfo = new ProcessStartInfo() 40 | { 41 | FileName = Location, 42 | Arguments = args, 43 | UseShellExecute = false, 44 | RedirectStandardError = true, 45 | RedirectStandardOutput = true 46 | }, 47 | EnableRaisingEvents = true 48 | }; 49 | var tcs = new TaskCompletionSource(); 50 | 51 | using (cancellationToken.Register(() => Cancel(process, tcs))) 52 | { 53 | process.Exited += (_, __) => tcs.TrySetResult(process.ExitCode); 54 | process.OutputDataReceived += (_, a) => LogIfNotNull(logger.LogInformation, "stdout: {0}", a.Data); 55 | process.ErrorDataReceived += (_, a) => LogIfNotNull(logger.LogError, "stderr: {0}", a.Data); 56 | 57 | process.Start(); 58 | 59 | process.BeginErrorReadLine(); 60 | process.BeginOutputReadLine(); 61 | 62 | return await tcs.Task; 63 | } 64 | } 65 | 66 | private void LogIfNotNull(Action logger, string message, string data) 67 | { 68 | if (!string.IsNullOrEmpty(data)) 69 | { 70 | logger(message, new[] { data }); 71 | } 72 | } 73 | 74 | private static void Cancel(Process process, TaskCompletionSource tcs) 75 | { 76 | if (process != null && !process.HasExited) 77 | { 78 | process.Kill(); 79 | } 80 | tcs.TrySetCanceled(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Expectation.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn 2 | { 3 | public enum Expectation 4 | { 5 | Fail, 6 | NonStrict, 7 | OkOrFail, 8 | Ok 9 | } 10 | } -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/ServerSpec.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | 3 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn 4 | { 5 | public class ServerSpec 6 | { 7 | public string Name { get; } 8 | public string Url { get; } 9 | 10 | public ServerSpec(string name, string url) 11 | { 12 | Name = name; 13 | Url = url; 14 | } 15 | 16 | public JObject GetJson() => new JObject( 17 | new JProperty("agent", Name), 18 | new JProperty("url", Url), 19 | new JProperty("options", new JObject( 20 | new JProperty("version", 18)))); 21 | } 22 | } -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Autobahn/Wstest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn 5 | { 6 | /// 7 | /// Wrapper around the Autobahn Test Suite's "wstest" app. 8 | /// 9 | public class Wstest : Executable 10 | { 11 | private static Lazy _instance = new Lazy(Create); 12 | 13 | public static readonly string DefaultLocation = LocateWstest(); 14 | 15 | public static Wstest Default => _instance.Value; 16 | 17 | public Wstest(string path) : base(path) { } 18 | 19 | private static Wstest Create() 20 | { 21 | var location = LocateWstest(); 22 | 23 | return (location == null || !File.Exists(location)) ? null : new Wstest(location); 24 | } 25 | 26 | private static string LocateWstest() 27 | { 28 | var location = Environment.GetEnvironmentVariable("ASPNETCORE_WSTEST_PATH"); 29 | if (string.IsNullOrEmpty(location)) 30 | { 31 | location = Locate("wstest"); 32 | } 33 | 34 | return location; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.ConformanceTest/AutobahnTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Runtime.InteropServices; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Server.IntegrationTesting; 8 | using Microsoft.AspNetCore.Testing.xunit; 9 | using Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn; 10 | using Microsoft.Extensions.Logging; 11 | using Microsoft.Extensions.Logging.Testing; 12 | using Xunit; 13 | using Xunit.Abstractions; 14 | 15 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest 16 | { 17 | public class AutobahnTests : LoggedTest 18 | { 19 | private static readonly TimeSpan TestTimeout = TimeSpan.FromMinutes(3); 20 | 21 | public AutobahnTests(ITestOutputHelper output) : base(output) 22 | { 23 | } 24 | 25 | // Skip if wstest is not installed for now, see https://github.com/aspnet/WebSockets/issues/95 26 | // We will enable Wstest on every build once we've gotten the necessary infrastructure sorted out :). 27 | [ConditionalFact] 28 | [SkipIfWsTestNotPresent] 29 | public async Task AutobahnTestSuite() 30 | { 31 | // If we're on CI, we want to actually fail if WsTest isn't installed, rather than just skipping the test 32 | // The SkipIfWsTestNotPresent attribute ensures that this test isn't skipped on CI, so we just need to check that Wstest is present 33 | // And we use Assert.True to provide an error message 34 | 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."); 35 | 36 | using (StartLog(out var loggerFactory)) 37 | { 38 | var logger = loggerFactory.CreateLogger(); 39 | var reportDir = Environment.GetEnvironmentVariable("AUTOBAHN_SUITES_REPORT_DIR"); 40 | var outDir = !string.IsNullOrEmpty(reportDir) ? 41 | reportDir : 42 | Path.Combine(AppContext.BaseDirectory, "autobahnreports"); 43 | 44 | if (Directory.Exists(outDir)) 45 | { 46 | Directory.Delete(outDir, recursive: true); 47 | } 48 | 49 | outDir = outDir.Replace("\\", "\\\\"); 50 | 51 | // 9.* is Limits/Performance which is VERY SLOW; 12.*/13.* are compression which we don't implement 52 | var spec = new AutobahnSpec(outDir) 53 | .IncludeCase("*") 54 | .ExcludeCase("9.*", "12.*", "13.*"); 55 | 56 | var cts = new CancellationTokenSource(); 57 | cts.CancelAfter(TestTimeout); // These tests generally complete in just over 1 minute. 58 | 59 | using (cts.Token.Register(() => logger.LogError("Test run is taking longer than maximum duration of {timeoutMinutes:0.00} minutes. Aborting...", TestTimeout.TotalMinutes))) 60 | { 61 | AutobahnResult result; 62 | using (var tester = new AutobahnTester(loggerFactory, spec)) 63 | { 64 | await tester.DeployTestAndAddToSpec(ServerType.Kestrel, ssl: false, environment: "ManagedSockets", cancellationToken: cts.Token); 65 | await tester.DeployTestAndAddToSpec(ServerType.Kestrel, ssl: true, environment: "ManagedSockets", cancellationToken: cts.Token); 66 | 67 | // Windows-only WebListener tests 68 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 69 | { 70 | if (IsWindows8OrHigher()) 71 | { 72 | // 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? 73 | // For now, just allow the non-strict response, it's not a failure. 74 | await tester.DeployTestAndAddToSpec(ServerType.HttpSys, ssl: false, environment: "ManagedSockets", cancellationToken: cts.Token); 75 | } 76 | } 77 | 78 | result = await tester.Run(cts.Token); 79 | tester.Verify(result); 80 | } 81 | } 82 | 83 | // If it hasn't been cancelled yet, cancel the token just to be sure 84 | cts.Cancel(); 85 | } 86 | } 87 | 88 | private bool IsWindows8OrHigher() 89 | { 90 | const string WindowsName = "Microsoft Windows "; 91 | const int VersionOffset = 18; 92 | 93 | if (RuntimeInformation.OSDescription.StartsWith(WindowsName)) 94 | { 95 | var versionStr = RuntimeInformation.OSDescription.Substring(VersionOffset); 96 | Version version; 97 | if (Version.TryParse(versionStr, out version)) 98 | { 99 | return version.Major > 6 || (version.Major == 6 && version.Minor >= 2); 100 | } 101 | } 102 | 103 | return false; 104 | } 105 | 106 | private bool IsIISExpress10Installed() 107 | { 108 | var pf = Environment.GetEnvironmentVariable("PROGRAMFILES"); 109 | var iisExpressExe = Path.Combine(pf, "IIS Express", "iisexpress.exe"); 110 | return File.Exists(iisExpressExe) && FileVersionInfo.GetVersionInfo(iisExpressExe).FileMajorPart >= 10; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest 5 | { 6 | public class Helpers 7 | { 8 | public static string GetApplicationPath(string projectName) 9 | { 10 | var applicationBasePath = AppContext.BaseDirectory; 11 | 12 | var directoryInfo = new DirectoryInfo(applicationBasePath); 13 | do 14 | { 15 | var solutionFileInfo = new FileInfo(Path.Combine(directoryInfo.FullName, "WebSockets.sln")); 16 | if (solutionFileInfo.Exists) 17 | { 18 | return Path.GetFullPath(Path.Combine(directoryInfo.FullName, "test", projectName)); 19 | } 20 | 21 | directoryInfo = directoryInfo.Parent; 22 | } 23 | while (directoryInfo.Parent != null); 24 | 25 | throw new Exception($"Solution root could not be found using {applicationBasePath}"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.ConformanceTest/Microsoft.AspNetCore.WebSockets.ConformanceTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.ConformanceTest/SkipIfWsTestNotPresentAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Testing.xunit; 3 | using Microsoft.AspNetCore.WebSockets.ConformanceTest.Autobahn; 4 | 5 | namespace Microsoft.AspNetCore.WebSockets.ConformanceTest 6 | { 7 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] 8 | public class SkipIfWsTestNotPresentAttribute : Attribute, ITestCondition 9 | { 10 | public bool IsMet => IsOnCi || Wstest.Default != null; 11 | public string SkipReason => "Autobahn Test Suite is not installed on the host machine."; 12 | 13 | private static bool IsOnCi => 14 | !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TEAMCITY_VERSION")) || 15 | string.Equals(Environment.GetEnvironmentVariable("TRAVIS"), "true", StringComparison.OrdinalIgnoreCase) || 16 | string.Equals(Environment.GetEnvironmentVariable("APPVEYOR"), "true", StringComparison.OrdinalIgnoreCase); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.Test/AddWebSocketsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Options; 8 | using Xunit; 9 | 10 | namespace Microsoft.AspNetCore.WebSockets.Test 11 | { 12 | public class AddWebSocketsTests 13 | { 14 | [Fact] 15 | public void AddWebSocketsConfiguresOptions() 16 | { 17 | var serviceCollection = new ServiceCollection(); 18 | 19 | serviceCollection.AddWebSockets(o => 20 | { 21 | o.KeepAliveInterval = TimeSpan.FromSeconds(1000); 22 | o.AllowedOrigins.Add("someString"); 23 | }); 24 | 25 | var services = serviceCollection.BuildServiceProvider(); 26 | var socketOptions = services.GetRequiredService>().Value; 27 | 28 | Assert.Equal(TimeSpan.FromSeconds(1000), socketOptions.KeepAliveInterval); 29 | Assert.Single(socketOptions.AllowedOrigins); 30 | Assert.Equal("someString", socketOptions.AllowedOrigins[0]); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.Test/BufferStream.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. 2 | 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.Diagnostics.Contracts; 7 | using System.IO; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace Microsoft.AspNetCore.WebSockets.Test 12 | { 13 | // This steam accepts writes from one side, buffers them internally, and returns the data via Reads 14 | // when requested on the other side. 15 | public class BufferStream : Stream 16 | { 17 | private bool _disposed; 18 | private bool _aborted; 19 | private bool _terminated; 20 | private Exception _abortException; 21 | private ConcurrentQueue _bufferedData; 22 | private ArraySegment _topBuffer; 23 | private SemaphoreSlim _readLock; 24 | private SemaphoreSlim _writeLock; 25 | private TaskCompletionSource _readWaitingForData; 26 | 27 | internal BufferStream() 28 | { 29 | _readLock = new SemaphoreSlim(1, 1); 30 | _writeLock = new SemaphoreSlim(1, 1); 31 | _bufferedData = new ConcurrentQueue(); 32 | _readWaitingForData = new TaskCompletionSource(); 33 | } 34 | 35 | public override bool CanRead 36 | { 37 | get { return true; } 38 | } 39 | 40 | public override bool CanSeek 41 | { 42 | get { return false; } 43 | } 44 | 45 | public override bool CanWrite 46 | { 47 | get { return true; } 48 | } 49 | 50 | #region NotSupported 51 | 52 | public override long Length 53 | { 54 | get { throw new NotSupportedException(); } 55 | } 56 | 57 | public override long Position 58 | { 59 | get { throw new NotSupportedException(); } 60 | set { throw new NotSupportedException(); } 61 | } 62 | 63 | public override long Seek(long offset, SeekOrigin origin) 64 | { 65 | throw new NotSupportedException(); 66 | } 67 | 68 | public override void SetLength(long value) 69 | { 70 | throw new NotSupportedException(); 71 | } 72 | 73 | #endregion NotSupported 74 | 75 | /// 76 | /// Ends the stream, meaning all future reads will return '0'. 77 | /// 78 | public void End() 79 | { 80 | _terminated = true; 81 | } 82 | 83 | public override void Flush() 84 | { 85 | CheckDisposed(); 86 | // TODO: Wait for data to drain? 87 | } 88 | 89 | public override Task FlushAsync(CancellationToken cancellationToken) 90 | { 91 | if (cancellationToken.IsCancellationRequested) 92 | { 93 | TaskCompletionSource tcs = new TaskCompletionSource(); 94 | tcs.TrySetCanceled(); 95 | return tcs.Task; 96 | } 97 | 98 | Flush(); 99 | 100 | // TODO: Wait for data to drain? 101 | 102 | return Task.FromResult(0); 103 | } 104 | 105 | public override int Read(byte[] buffer, int offset, int count) 106 | { 107 | if(_terminated) 108 | { 109 | return 0; 110 | } 111 | 112 | VerifyBuffer(buffer, offset, count, allowEmpty: false); 113 | _readLock.Wait(); 114 | try 115 | { 116 | int totalRead = 0; 117 | do 118 | { 119 | // Don't drain buffered data when signaling an abort. 120 | CheckAborted(); 121 | if (_topBuffer.Count <= 0) 122 | { 123 | byte[] topBuffer = null; 124 | while (!_bufferedData.TryDequeue(out topBuffer)) 125 | { 126 | if (_disposed) 127 | { 128 | CheckAborted(); 129 | // Graceful close 130 | return totalRead; 131 | } 132 | WaitForDataAsync().Wait(); 133 | } 134 | _topBuffer = new ArraySegment(topBuffer); 135 | } 136 | int actualCount = Math.Min(count, _topBuffer.Count); 137 | Buffer.BlockCopy(_topBuffer.Array, _topBuffer.Offset, buffer, offset, actualCount); 138 | _topBuffer = new ArraySegment(_topBuffer.Array, 139 | _topBuffer.Offset + actualCount, 140 | _topBuffer.Count - actualCount); 141 | totalRead += actualCount; 142 | offset += actualCount; 143 | count -= actualCount; 144 | } 145 | while (count > 0 && (_topBuffer.Count > 0 || _bufferedData.Count > 0)); 146 | // Keep reading while there is more data available and we have more space to put it in. 147 | return totalRead; 148 | } 149 | finally 150 | { 151 | _readLock.Release(); 152 | } 153 | } 154 | 155 | public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) 156 | { 157 | // TODO: This option doesn't preserve the state object. 158 | // return ReadAsync(buffer, offset, count); 159 | return base.BeginRead(buffer, offset, count, callback, state); 160 | } 161 | 162 | public override int EndRead(IAsyncResult asyncResult) 163 | { 164 | // return ((Task)asyncResult).Result; 165 | return base.EndRead(asyncResult); 166 | } 167 | 168 | public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 169 | { 170 | if (_terminated) 171 | { 172 | return 0; 173 | } 174 | 175 | VerifyBuffer(buffer, offset, count, allowEmpty: false); 176 | var registration = cancellationToken.Register(Abort); 177 | await _readLock.WaitAsync(cancellationToken); 178 | try 179 | { 180 | int totalRead = 0; 181 | do 182 | { 183 | // Don't drained buffered data on abort. 184 | CheckAborted(); 185 | if (_topBuffer.Count <= 0) 186 | { 187 | byte[] topBuffer = null; 188 | while (!_bufferedData.TryDequeue(out topBuffer)) 189 | { 190 | if (_disposed) 191 | { 192 | CheckAborted(); 193 | // Graceful close 194 | return totalRead; 195 | } 196 | await WaitForDataAsync(); 197 | } 198 | _topBuffer = new ArraySegment(topBuffer); 199 | } 200 | var actualCount = Math.Min(count, _topBuffer.Count); 201 | Buffer.BlockCopy(_topBuffer.Array, _topBuffer.Offset, buffer, offset, actualCount); 202 | _topBuffer = new ArraySegment(_topBuffer.Array, 203 | _topBuffer.Offset + actualCount, 204 | _topBuffer.Count - actualCount); 205 | totalRead += actualCount; 206 | offset += actualCount; 207 | count -= actualCount; 208 | } 209 | while (count > 0 && (_topBuffer.Count > 0 || _bufferedData.Count > 0)); 210 | // Keep reading while there is more data available and we have more space to put it in. 211 | return totalRead; 212 | } 213 | finally 214 | { 215 | registration.Dispose(); 216 | _readLock.Release(); 217 | } 218 | } 219 | 220 | // Write with count 0 will still trigger OnFirstWrite 221 | public override void Write(byte[] buffer, int offset, int count) 222 | { 223 | VerifyBuffer(buffer, offset, count, allowEmpty: true); 224 | CheckDisposed(); 225 | 226 | _writeLock.Wait(); 227 | try 228 | { 229 | if (count == 0) 230 | { 231 | return; 232 | } 233 | // Copies are necessary because we don't know what the caller is going to do with the buffer afterwards. 234 | var internalBuffer = new byte[count]; 235 | Buffer.BlockCopy(buffer, offset, internalBuffer, 0, count); 236 | _bufferedData.Enqueue(internalBuffer); 237 | 238 | SignalDataAvailable(); 239 | } 240 | finally 241 | { 242 | _writeLock.Release(); 243 | } 244 | } 245 | 246 | public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) 247 | { 248 | Write(buffer, offset, count); 249 | var tcs = new TaskCompletionSource(state); 250 | tcs.TrySetResult(null); 251 | var result = tcs.Task; 252 | if (callback != null) 253 | { 254 | callback(result); 255 | } 256 | return result; 257 | } 258 | 259 | public override void EndWrite(IAsyncResult asyncResult) { } 260 | 261 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 262 | { 263 | VerifyBuffer(buffer, offset, count, allowEmpty: true); 264 | if (cancellationToken.IsCancellationRequested) 265 | { 266 | var tcs = new TaskCompletionSource(); 267 | tcs.TrySetCanceled(); 268 | return tcs.Task; 269 | } 270 | 271 | Write(buffer, offset, count); 272 | return Task.FromResult(null); 273 | } 274 | 275 | private static void VerifyBuffer(byte[] buffer, int offset, int count, bool allowEmpty) 276 | { 277 | if (offset < 0 || offset > buffer.Length) 278 | { 279 | throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); 280 | } 281 | if (count < 0 || count > buffer.Length - offset 282 | || (!allowEmpty && count == 0)) 283 | { 284 | throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty); 285 | } 286 | } 287 | 288 | private void SignalDataAvailable() 289 | { 290 | // Dispatch, as TrySetResult will synchronously execute the waiters callback and block our Write. 291 | Task.Factory.StartNew(() => _readWaitingForData.TrySetResult(null)); 292 | } 293 | 294 | private Task WaitForDataAsync() 295 | { 296 | _readWaitingForData = new TaskCompletionSource(); 297 | 298 | if (!_bufferedData.IsEmpty || _disposed) 299 | { 300 | // Race, data could have arrived before we created the TCS. 301 | _readWaitingForData.TrySetResult(null); 302 | } 303 | 304 | return _readWaitingForData.Task; 305 | } 306 | 307 | internal void Abort() 308 | { 309 | Abort(new OperationCanceledException()); 310 | } 311 | 312 | internal void Abort(Exception innerException) 313 | { 314 | Contract.Requires(innerException != null); 315 | _aborted = true; 316 | _abortException = innerException; 317 | Dispose(); 318 | } 319 | 320 | private void CheckAborted() 321 | { 322 | if (_aborted) 323 | { 324 | throw new IOException(string.Empty, _abortException); 325 | } 326 | } 327 | 328 | [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_writeLock", Justification = "ODEs from the locks would mask IOEs from abort.")] 329 | [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_readLock", Justification = "Data can still be read unless we get aborted.")] 330 | protected override void Dispose(bool disposing) 331 | { 332 | if (disposing) 333 | { 334 | // Throw for further writes, but not reads. Allow reads to drain the buffered data and then return 0 for further reads. 335 | _disposed = true; 336 | _readWaitingForData.TrySetResult(null); 337 | } 338 | 339 | base.Dispose(disposing); 340 | } 341 | 342 | private void CheckDisposed() 343 | { 344 | if (_disposed) 345 | { 346 | throw new ObjectDisposedException(GetType().FullName); 347 | } 348 | } 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.Test/DuplexStream.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. 2 | 3 | using System; 4 | using System.IO; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Microsoft.AspNetCore.WebSockets.Test 9 | { 10 | // A duplex wrapper around a read and write stream. 11 | public class DuplexStream : Stream 12 | { 13 | public BufferStream ReadStream { get; } 14 | public BufferStream WriteStream { get; } 15 | 16 | public DuplexStream() 17 | : this (new BufferStream(), new BufferStream()) 18 | { 19 | } 20 | 21 | public DuplexStream(BufferStream readStream, BufferStream writeStream) 22 | { 23 | ReadStream = readStream; 24 | WriteStream = writeStream; 25 | } 26 | 27 | public DuplexStream CreateReverseDuplexStream() 28 | { 29 | return new DuplexStream(WriteStream, ReadStream); 30 | } 31 | 32 | 33 | #region Properties 34 | 35 | public override bool CanRead 36 | { 37 | get { return ReadStream.CanRead; } 38 | } 39 | 40 | public override bool CanSeek 41 | { 42 | get { return false; } 43 | } 44 | 45 | public override bool CanTimeout 46 | { 47 | get { return ReadStream.CanTimeout || WriteStream.CanTimeout; } 48 | } 49 | 50 | public override bool CanWrite 51 | { 52 | get { return WriteStream.CanWrite; } 53 | } 54 | 55 | public override long Length 56 | { 57 | get { throw new NotSupportedException(); } 58 | } 59 | 60 | public override long Position 61 | { 62 | get { throw new NotSupportedException(); } 63 | set { throw new NotSupportedException(); } 64 | } 65 | 66 | public override int ReadTimeout 67 | { 68 | get { return ReadStream.ReadTimeout; } 69 | set { ReadStream.ReadTimeout = value; } 70 | } 71 | 72 | public override int WriteTimeout 73 | { 74 | get { return WriteStream.WriteTimeout; } 75 | set { WriteStream.WriteTimeout = value; } 76 | } 77 | 78 | #endregion Properties 79 | 80 | public override long Seek(long offset, SeekOrigin origin) 81 | { 82 | throw new NotSupportedException(); 83 | } 84 | 85 | public override void SetLength(long value) 86 | { 87 | throw new NotSupportedException(); 88 | } 89 | 90 | public override int ReadByte() 91 | { 92 | return ReadStream.ReadByte(); 93 | } 94 | 95 | public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) 96 | { 97 | return ReadStream.BeginRead(buffer, offset, count, callback, state); 98 | } 99 | 100 | public override int EndRead(IAsyncResult asyncResult) 101 | { 102 | return ReadStream.EndRead(asyncResult); 103 | } 104 | 105 | public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 106 | { 107 | return ReadStream.ReadAsync(buffer, offset, count, cancellationToken); 108 | } 109 | 110 | public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) 111 | { 112 | return ReadStream.CopyToAsync(destination, bufferSize, cancellationToken); 113 | } 114 | 115 | #region Read 116 | 117 | public override int Read(byte[] buffer, int offset, int count) 118 | { 119 | return ReadStream.Read(buffer, offset, count); 120 | } 121 | 122 | #endregion Read 123 | 124 | #region Write 125 | 126 | public override void Write(byte[] buffer, int offset, int count) 127 | { 128 | WriteStream.Write(buffer, offset, count); 129 | } 130 | public override void WriteByte(byte value) 131 | { 132 | WriteStream.WriteByte(value); 133 | } 134 | 135 | public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) 136 | { 137 | return WriteStream.BeginWrite(buffer, offset, count, callback, state); 138 | } 139 | 140 | public override void EndWrite(IAsyncResult asyncResult) 141 | { 142 | WriteStream.EndWrite(asyncResult); 143 | } 144 | 145 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 146 | { 147 | return WriteStream.WriteAsync(buffer, offset, count, cancellationToken); 148 | } 149 | 150 | public override Task FlushAsync(CancellationToken cancellationToken) 151 | { 152 | return WriteStream.FlushAsync(cancellationToken); 153 | } 154 | 155 | public override void Flush() 156 | { 157 | WriteStream.Flush(); 158 | } 159 | 160 | #endregion Write 161 | 162 | protected override void Dispose(bool disposing) 163 | { 164 | if (disposing) 165 | { 166 | ReadStream.Dispose(); 167 | WriteStream.Dispose(); 168 | } 169 | base.Dispose(disposing); 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.Test/IWebHostPortExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using Microsoft.AspNetCore.Hosting.Server.Features; 8 | 9 | namespace Microsoft.AspNetCore.Hosting 10 | { 11 | public static class IWebHostPortExtensions 12 | { 13 | public static int GetPort(this IWebHost host) 14 | { 15 | return host.GetPorts().First(); 16 | } 17 | 18 | public static IEnumerable GetPorts(this IWebHost host) 19 | { 20 | return host.GetUris() 21 | .Select(u => u.Port); 22 | } 23 | 24 | public static IEnumerable GetUris(this IWebHost host) 25 | { 26 | return host.ServerFeatures.Get().Addresses 27 | .Select(a => new Uri(a)); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.Test/KestrelWebSocketHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.Logging; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | namespace Microsoft.AspNetCore.WebSockets.Test 14 | { 15 | public class KestrelWebSocketHelpers 16 | { 17 | public static IDisposable CreateServer(ILoggerFactory loggerFactory, out int port, Func app, Action configure = null) 18 | { 19 | configure = configure ?? (o => { }); 20 | Action startup = builder => 21 | { 22 | builder.Use(async (ct, next) => 23 | { 24 | try 25 | { 26 | // Kestrel does not return proper error responses: 27 | // https://github.com/aspnet/KestrelHttpServer/issues/43 28 | await next(); 29 | } 30 | catch (Exception ex) 31 | { 32 | if (ct.Response.HasStarted) 33 | { 34 | throw; 35 | } 36 | 37 | ct.Response.StatusCode = 500; 38 | ct.Response.Headers.Clear(); 39 | await ct.Response.WriteAsync(ex.ToString()); 40 | } 41 | }); 42 | builder.UseWebSockets(); 43 | builder.Run(c => app(c)); 44 | }; 45 | 46 | var configBuilder = new ConfigurationBuilder(); 47 | configBuilder.AddInMemoryCollection(); 48 | var config = configBuilder.Build(); 49 | config["server.urls"] = $"http://127.0.0.1:0"; 50 | 51 | var host = new WebHostBuilder() 52 | .ConfigureServices(s => 53 | { 54 | s.AddWebSockets(configure); 55 | s.AddSingleton(loggerFactory); 56 | }) 57 | .UseConfiguration(config) 58 | .UseKestrel() 59 | .Configure(startup) 60 | .Build(); 61 | 62 | host.Start(); 63 | port = host.GetPort(); 64 | 65 | return host; 66 | } 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.Test/Microsoft.AspNetCore.WebSockets.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(StandardTestTfms) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.Test/SendReceiveTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.WebSockets; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Microsoft.AspNetCore.WebSockets.Test 10 | { 11 | public class SendReceiveTests 12 | { 13 | [Fact] 14 | public async Task ClientToServerTextMessage() 15 | { 16 | const string message = "Hello, World!"; 17 | 18 | var pair = WebSocketPair.Create(); 19 | var sendBuffer = Encoding.UTF8.GetBytes(message); 20 | 21 | await pair.ClientSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Text, endOfMessage: true, cancellationToken: CancellationToken.None); 22 | 23 | var receiveBuffer = new byte[32]; 24 | var result = await pair.ServerSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); 25 | 26 | Assert.Equal(WebSocketMessageType.Text, result.MessageType); 27 | Assert.Equal(message, Encoding.UTF8.GetString(receiveBuffer, 0, result.Count)); 28 | } 29 | 30 | [Fact] 31 | public async Task ServerToClientTextMessage() 32 | { 33 | const string message = "Hello, World!"; 34 | 35 | var pair = WebSocketPair.Create(); 36 | var sendBuffer = Encoding.UTF8.GetBytes(message); 37 | 38 | await pair.ServerSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Text, endOfMessage: true, cancellationToken: CancellationToken.None); 39 | 40 | var receiveBuffer = new byte[32]; 41 | var result = await pair.ClientSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); 42 | 43 | Assert.Equal(WebSocketMessageType.Text, result.MessageType); 44 | Assert.Equal(message, Encoding.UTF8.GetString(receiveBuffer, 0, result.Count)); 45 | } 46 | 47 | [Fact] 48 | public async Task ClientToServerBinaryMessage() 49 | { 50 | var pair = WebSocketPair.Create(); 51 | var sendBuffer = new byte[] { 0xde, 0xad, 0xbe, 0xef }; 52 | 53 | await pair.ClientSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Binary, endOfMessage: true, cancellationToken: CancellationToken.None); 54 | 55 | var receiveBuffer = new byte[32]; 56 | var result = await pair.ServerSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); 57 | 58 | Assert.Equal(WebSocketMessageType.Binary, result.MessageType); 59 | Assert.Equal(sendBuffer, receiveBuffer.Take(result.Count).ToArray()); 60 | } 61 | 62 | [Fact] 63 | public async Task ServerToClientBinaryMessage() 64 | { 65 | var pair = WebSocketPair.Create(); 66 | var sendBuffer = new byte[] { 0xde, 0xad, 0xbe, 0xef }; 67 | 68 | await pair.ServerSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Binary, endOfMessage: true, cancellationToken: CancellationToken.None); 69 | 70 | var receiveBuffer = new byte[32]; 71 | var result = await pair.ClientSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); 72 | 73 | Assert.Equal(WebSocketMessageType.Binary, result.MessageType); 74 | Assert.Equal(sendBuffer, receiveBuffer.Take(result.Count).ToArray()); 75 | } 76 | 77 | [Fact] 78 | public async Task ThrowsWhenUnderlyingStreamClosed() 79 | { 80 | var pair = WebSocketPair.Create(); 81 | var sendBuffer = new byte[] { 0xde, 0xad, 0xbe, 0xef }; 82 | 83 | await pair.ServerSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Binary, endOfMessage: true, cancellationToken: CancellationToken.None); 84 | 85 | var receiveBuffer = new byte[32]; 86 | var result = await pair.ClientSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); 87 | 88 | Assert.Equal(WebSocketMessageType.Binary, result.MessageType); 89 | 90 | // Close the client socket's read end 91 | pair.ClientStream.ReadStream.End(); 92 | 93 | // Assert.Throws doesn't support async :( 94 | try 95 | { 96 | await pair.ClientSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); 97 | 98 | // The exception should prevent this line from running 99 | Assert.False(true, "Expected an exception to be thrown!"); 100 | } 101 | catch (WebSocketException ex) 102 | { 103 | Assert.Equal(WebSocketError.ConnectionClosedPrematurely, ex.WebSocketErrorCode); 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.WebSockets.Test/WebSocketPair.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.WebSockets; 3 | using Microsoft.AspNetCore.WebSockets.Internal; 4 | 5 | namespace Microsoft.AspNetCore.WebSockets.Test 6 | { 7 | internal class WebSocketPair 8 | { 9 | public WebSocket ClientSocket { get; } 10 | public WebSocket ServerSocket { get; } 11 | public DuplexStream ServerStream { get; } 12 | public DuplexStream ClientStream { get; } 13 | 14 | public WebSocketPair(DuplexStream serverStream, DuplexStream clientStream, WebSocket clientSocket, WebSocket serverSocket) 15 | { 16 | ClientStream = clientStream; 17 | ServerStream = serverStream; 18 | ClientSocket = clientSocket; 19 | ServerSocket = serverSocket; 20 | } 21 | 22 | public static WebSocketPair Create() 23 | { 24 | // Create streams 25 | var serverStream = new DuplexStream(); 26 | var clientStream = serverStream.CreateReverseDuplexStream(); 27 | 28 | return new WebSocketPair( 29 | serverStream, 30 | clientStream, 31 | clientSocket: WebSocketProtocol.CreateFromStream(clientStream, isServer: false, subProtocol: null, keepAliveInterval: TimeSpan.FromMinutes(2)), 32 | serverSocket: WebSocketProtocol.CreateFromStream(serverStream, isServer: true, subProtocol: null, keepAliveInterval: TimeSpan.FromMinutes(2))); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /version.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 3.0.0 4 | alpha1 5 | $(VersionPrefix) 6 | $(VersionPrefix)-$(VersionSuffix)-final 7 | t000 8 | a- 9 | $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) 10 | $(VersionSuffix)-$(BuildNumber) 11 | 12 | 13 | --------------------------------------------------------------------------------