diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json
index 0ee8c1fd71..32039a6734 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json
@@ -86,12 +86,12 @@
"datatype": "choice",
"choices": [
{
- "choice": "netcoreapp3.1",
- "description": "Target netcoreapp3.1"
+ "choice": "netcoreapp5.0",
+ "description": "Target netcoreapp5.0"
}
],
- "replaces": "netcoreapp3.1",
- "defaultValue": "netcoreapp3.1"
+ "replaces": "netcoreapp5.0",
+ "defaultValue": "netcoreapp5.0"
},
"copyrightYear": {
"type": "generated",
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/template.json
index 13fc5299e5..0ebccbad52 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-FSharp/.template.config/template.json
@@ -82,12 +82,12 @@
"datatype": "choice",
"choices": [
{
- "choice": "netcoreapp3.1",
- "description": "Target netcoreapp3.1"
+ "choice": "netcoreapp5.0",
+ "description": "Target netcoreapp5.0"
}
],
- "replaces": "netcoreapp3.1",
- "defaultValue": "netcoreapp3.1"
+ "replaces": "netcoreapp5.0",
+ "defaultValue": "netcoreapp5.0"
},
"copyrightYear": {
"type": "generated",
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/template.json
index 9499795d70..462630dbbd 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/GrpcService-CSharp/.template.config/template.json
@@ -41,11 +41,11 @@
"datatype": "choice",
"choices": [
{
- "choice": "netcoreapp3.1",
- "description": "Target netcoreapp3.1"
+ "choice": "netcoreapp5.0",
+ "description": "Target netcoreapp5.0"
}
],
- "defaultValue": "netcoreapp3.1"
+ "defaultValue": "netcoreapp5.0"
},
"ExcludeLaunchSettings": {
"type": "parameter",
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/template.json
index e03bc65429..d99bc528b9 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/template.json
@@ -4,8 +4,7 @@
"classifications": [
"Web",
"Razor",
- "Library",
- "Razor Class Library"
+ "Library"
],
"name": "Razor Class Library",
"generatorVersions": "[1.0.0.0-*)",
@@ -48,11 +47,11 @@
"datatype": "choice",
"choices": [
{
- "choice": "netcoreapp3.1",
- "description": "Target netcoreapp3.1"
+ "choice": "netcoreapp5.0",
+ "description": "Target netcoreapp5.0"
}
],
- "defaultValue": "netcoreapp3.1"
+ "defaultValue": "netcoreapp5.0"
},
"HostIdentifier": {
"type": "bind",
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json
index d027fb97e4..82977b366b 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/.template.config/template.json
@@ -310,12 +310,12 @@
"datatype": "choice",
"choices": [
{
- "choice": "netcoreapp3.1",
- "description": "Target netcoreapp3.1"
+ "choice": "netcoreapp5.0",
+ "description": "Target netcoreapp5.0"
}
],
- "replaces": "netcoreapp3.1",
- "defaultValue": "netcoreapp3.1"
+ "replaces": "netcoreapp5.0",
+ "defaultValue": "netcoreapp5.0"
},
"copyrightYear": {
"type": "generated",
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.js b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.js
index 3c76e6dc45..ac49c18641 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.js
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/wwwroot/js/site.js
@@ -1,4 +1,4 @@
// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets.
-// Write your Javascript code.
+// Write your JavaScript code.
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json
index fd4e26bef3..06954164c1 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/.template.config/template.json
@@ -300,12 +300,12 @@
"datatype": "choice",
"choices": [
{
- "choice": "netcoreapp3.1",
- "description": "Target netcoreapp3.1"
+ "choice": "netcoreapp5.0",
+ "description": "Target netcoreapp5.0"
}
],
- "replaces": "netcoreapp3.1",
- "defaultValue": "netcoreapp3.1"
+ "replaces": "netcoreapp5.0",
+ "defaultValue": "netcoreapp5.0"
},
"copyrightYear": {
"type": "generated",
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/template.json
index 5e95e2b1af..d72e90aaff 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/.template.config/template.json
@@ -87,12 +87,12 @@
"datatype": "choice",
"choices": [
{
- "choice": "netcoreapp3.1",
- "description": "Target netcoreapp3.1"
+ "choice": "netcoreapp5.0",
+ "description": "Target netcoreapp5.0"
}
],
- "replaces": "netcoreapp3.1",
- "defaultValue": "netcoreapp3.1"
+ "replaces": "netcoreapp5.0",
+ "defaultValue": "netcoreapp5.0"
},
"copyrightYear": {
"type": "generated",
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.js b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.js
index 3c76e6dc45..ac49c18641 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.js
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-FSharp/wwwroot/js/site.js
@@ -1,4 +1,4 @@
// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets.
-// Write your Javascript code.
+// Write your JavaScript code.
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json
index f48680bf91..a6e465575e 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json
@@ -209,12 +209,12 @@
"datatype": "choice",
"choices": [
{
- "choice": "netcoreapp3.1",
- "description": "Target netcoreapp3.1"
+ "choice": "netcoreapp5.0",
+ "description": "Target netcoreapp5.0"
}
],
- "replaces": "netcoreapp3.1",
- "defaultValue": "netcoreapp3.1"
+ "replaces": "netcoreapp5.0",
+ "defaultValue": "netcoreapp5.0"
},
"copyrightYear": {
"type": "generated",
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-FSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-FSharp/.template.config/template.json
index 75dcdd58f3..20f31e8bcd 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-FSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-FSharp/.template.config/template.json
@@ -82,12 +82,12 @@
"datatype": "choice",
"choices": [
{
- "choice": "netcoreapp3.1",
- "description": "Target netcoreapp3.1"
+ "choice": "netcoreapp5.0",
+ "description": "Target netcoreapp5.0"
}
],
- "replaces": "netcoreapp3.1",
- "defaultValue": "netcoreapp3.1"
+ "replaces": "netcoreapp5.0",
+ "defaultValue": "netcoreapp5.0"
},
"copyrightYear": {
"type": "generated",
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/template.json
index 408eae476b..f0428fa74f 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Worker-CSharp/.template.config/template.json
@@ -47,12 +47,12 @@
"datatype": "choice",
"choices": [
{
- "choice": "netcoreapp3.1",
- "description": "Target netcoreapp3.1"
+ "choice": "netcoreapp5.0",
+ "description": "Target netcoreapp5.0"
}
],
- "replaces": "netcoreapp3.1",
- "defaultValue": "netcoreapp3.1"
+ "replaces": "netcoreapp5.0",
+ "defaultValue": "netcoreapp5.0"
},
"copyrightYear": {
"type": "generated",
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/template.json
index 8855782667..6bf8ed94b4 100644
--- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/.template.config/template.json
@@ -177,12 +177,12 @@
"datatype": "choice",
"choices": [
{
- "choice": "netcoreapp3.1",
- "description": "Target netcoreapp3.1"
+ "choice": "netcoreapp5.0",
+ "description": "Target netcoreapp5.0"
}
],
- "replaces": "netcoreapp3.1",
- "defaultValue": "netcoreapp3.1"
+ "replaces": "netcoreapp5.0",
+ "defaultValue": "netcoreapp5.0"
},
"HostIdentifier": {
"type": "bind",
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/angular.json b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/angular.json
index 56f93675c6..ce5929d3f1 100644
--- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/angular.json
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/angular.json
@@ -71,7 +71,7 @@
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
- "styles": ["styles.css"],
+ "styles": ["src/styles.css"],
"scripts": [],
"assets": ["src/assets"]
}
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/Controllers/OidcConfigurationController.cs b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/Controllers/OidcConfigurationController.cs
index 75e26da4e9..cdcc89182a 100644
--- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/Controllers/OidcConfigurationController.cs
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/Controllers/OidcConfigurationController.cs
@@ -6,12 +6,12 @@ namespace Company.WebApplication1.Controllers
{
public class OidcConfigurationController : Controller
{
- private readonly ILogger
logger;
+ private readonly ILogger _logger;
- public OidcConfigurationController(IClientRequestParametersProvider clientRequestParametersProvider, ILogger _logger)
+ public OidcConfigurationController(IClientRequestParametersProvider clientRequestParametersProvider, ILogger logger)
{
ClientRequestParametersProvider = clientRequestParametersProvider;
- logger = _logger;
+ _logger = logger;
}
public IClientRequestParametersProvider ClientRequestParametersProvider { get; }
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/template.json
index a1079ad570..d1add605c5 100644
--- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/.template.config/template.json
@@ -178,12 +178,12 @@
"datatype": "choice",
"choices": [
{
- "choice": "netcoreapp3.1",
- "description": "Target netcoreapp3.1"
+ "choice": "netcoreapp5.0",
+ "description": "Target netcoreapp5.0"
}
],
- "replaces": "netcoreapp3.1",
- "defaultValue": "netcoreapp3.1"
+ "replaces": "netcoreapp5.0",
+ "defaultValue": "netcoreapp5.0"
},
"HostIdentifier": {
"type": "bind",
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/.env b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/.env
new file mode 100644
index 0000000000..6ce384e5ce
--- /dev/null
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/.env
@@ -0,0 +1 @@
+BROWSER=none
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/template.json
index 301aa0d2df..0f85d08a4d 100644
--- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/.template.config/template.json
@@ -87,12 +87,12 @@
"datatype": "choice",
"choices": [
{
- "choice": "netcoreapp3.1",
- "description": "Target netcoreapp3.1"
+ "choice": "netcoreapp5.0",
+ "description": "Target netcoreapp5.0"
}
],
- "replaces": "netcoreapp3.1",
- "defaultValue": "netcoreapp3.1"
+ "replaces": "netcoreapp5.0",
+ "defaultValue": "netcoreapp5.0"
},
"HostIdentifier": {
"type": "bind",
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/.env b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/.env
new file mode 100644
index 0000000000..6ce384e5ce
--- /dev/null
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/.env
@@ -0,0 +1 @@
+BROWSER=none
diff --git a/src/ProjectTemplates/scripts/Run-Angular-Locally.ps1 b/src/ProjectTemplates/scripts/Run-Angular-Locally.ps1
index ba4180de95..ec8993da7e 100644
--- a/src/ProjectTemplates/scripts/Run-Angular-Locally.ps1
+++ b/src/ProjectTemplates/scripts/Run-Angular-Locally.ps1
@@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop'
. $PSScriptRoot\Test-Template.ps1
-Test-Template "angular" "angular" "Microsoft.DotNet.Web.Spa.ProjectTemplates.3.1.3.1.0-dev.nupkg" $true
+Test-Template "angular" "angular" "Microsoft.DotNet.Web.Spa.ProjectTemplates.5.0.5.0.0-dev.nupkg" $true
diff --git a/src/ProjectTemplates/scripts/Run-Blazor-Locally.ps1 b/src/ProjectTemplates/scripts/Run-Blazor-Locally.ps1
index 7d989b65a3..575036e9c7 100644
--- a/src/ProjectTemplates/scripts/Run-Blazor-Locally.ps1
+++ b/src/ProjectTemplates/scripts/Run-Blazor-Locally.ps1
@@ -10,4 +10,4 @@ $ErrorActionPreference = 'Stop'
. $PSScriptRoot\Test-Template.ps1
-Test-Template "blazorserver" "blazorserver" "Microsoft.DotNet.Web.ProjectTemplates.3.1.3.1.0-dev.nupkg" $false
+Test-Template "blazorserver" "blazorserver" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false
diff --git a/src/ProjectTemplates/scripts/Run-EmptyWeb-Locally.ps1 b/src/ProjectTemplates/scripts/Run-EmptyWeb-Locally.ps1
index 6aeffd7687..d6859c6f72 100644
--- a/src/ProjectTemplates/scripts/Run-EmptyWeb-Locally.ps1
+++ b/src/ProjectTemplates/scripts/Run-EmptyWeb-Locally.ps1
@@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop'
. $PSScriptRoot\Test-Template.ps1
-Test-Template "web" "web" "Microsoft.DotNet.Web.ProjectTemplates.3.1.3.1.0-dev.nupkg" $false
+Test-Template "web" "web" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false
diff --git a/src/ProjectTemplates/scripts/Run-Razor-Locally.ps1 b/src/ProjectTemplates/scripts/Run-Razor-Locally.ps1
index f77838b435..ecba4fbd8f 100644
--- a/src/ProjectTemplates/scripts/Run-Razor-Locally.ps1
+++ b/src/ProjectTemplates/scripts/Run-Razor-Locally.ps1
@@ -6,4 +6,4 @@ param()
. $PSScriptRoot\Test-Template.ps1
-Test-Template "webapp" "webapp -au Individual" "Microsoft.DotNet.Web.ProjectTemplates.3.1.3.1.0-dev.nupkg" $false
+Test-Template "webapp" "webapp -au Individual" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false
diff --git a/src/ProjectTemplates/scripts/Run-React-Locally.ps1 b/src/ProjectTemplates/scripts/Run-React-Locally.ps1
index 972bfc4ead..8308860edc 100644
--- a/src/ProjectTemplates/scripts/Run-React-Locally.ps1
+++ b/src/ProjectTemplates/scripts/Run-React-Locally.ps1
@@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop'
. $PSScriptRoot\Test-Template.ps1
-Test-Template "react" "react" "Microsoft.DotNet.Web.Spa.ProjectTemplates.3.1.3.1.0-dev.nupkg" $true
+Test-Template "react" "react" "Microsoft.DotNet.Web.Spa.ProjectTemplates.5.0.5.0.0-dev.nupkg" $true
diff --git a/src/ProjectTemplates/scripts/Run-ReactRedux-Locally.ps1 b/src/ProjectTemplates/scripts/Run-ReactRedux-Locally.ps1
index 4c01f11400..6100d7cacd 100644
--- a/src/ProjectTemplates/scripts/Run-ReactRedux-Locally.ps1
+++ b/src/ProjectTemplates/scripts/Run-ReactRedux-Locally.ps1
@@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop'
. $PSScriptRoot\Test-Template.ps1
-Test-Template "reactredux" "reactredux" "Microsoft.DotNet.Web.Spa.ProjectTemplates.3.1.3.1.0-dev.nupkg" $true
+Test-Template "reactredux" "reactredux" "Microsoft.DotNet.Web.Spa.ProjectTemplates.5.0.5.0.0-dev.nupkg" $true
diff --git a/src/ProjectTemplates/scripts/Run-Starterweb-Locally.ps1 b/src/ProjectTemplates/scripts/Run-Starterweb-Locally.ps1
index 7971ae01ac..61ad47fbdc 100644
--- a/src/ProjectTemplates/scripts/Run-Starterweb-Locally.ps1
+++ b/src/ProjectTemplates/scripts/Run-Starterweb-Locally.ps1
@@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop'
. $PSScriptRoot\Test-Template.ps1
-Test-Template "mvc" "mvc -au Individual" "Microsoft.DotNet.Web.ProjectTemplates.3.1.3.1.0-dev.nupkg" $false
+Test-Template "mvc" "mvc -au Individual" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false
diff --git a/src/ProjectTemplates/scripts/Run-Worker-Locally.ps1 b/src/ProjectTemplates/scripts/Run-Worker-Locally.ps1
index 94fd8d012b..e6ff856e7e 100644
--- a/src/ProjectTemplates/scripts/Run-Worker-Locally.ps1
+++ b/src/ProjectTemplates/scripts/Run-Worker-Locally.ps1
@@ -9,4 +9,4 @@ $ErrorActionPreference = 'Stop'
. $PSScriptRoot\Test-Template.ps1
-Test-Template "worker" "worker" "Microsoft.DotNet.Web.ProjectTemplates.3.1.3.1.0-dev.nupkg" $false
+Test-Template "worker" "worker" "Microsoft.DotNet.Web.ProjectTemplates.5.0.5.0.0-dev.nupkg" $false
diff --git a/src/ProjectTemplates/scripts/Test-Template.ps1 b/src/ProjectTemplates/scripts/Test-Template.ps1
index d13b7e452c..5ad25a16d6 100644
--- a/src/ProjectTemplates/scripts/Test-Template.ps1
+++ b/src/ProjectTemplates/scripts/Test-Template.ps1
@@ -32,7 +32,7 @@ function Test-Template($templateName, $templateArgs, $templateNupkg, $isSPA) {
$proj = "$tmpDir/$templateName.$extension"
$projContent = Get-Content -Path $proj -Raw
$projContent = $projContent -replace ('', "
-
+
@@ -42,7 +42,7 @@ function Test-Template($templateName, $templateArgs, $templateNupkg, $isSPA) {
$projContent | Set-Content $proj
dotnet.exe ef migrations add mvc
dotnet.exe publish --configuration Release
- dotnet.exe bin\Release\netcoreapp3.1\publish\$templateName.dll
+ dotnet.exe bin\Release\netcoreapp5.0\publish\$templateName.dll
}
finally {
Pop-Location
diff --git a/src/ProjectTemplates/test/BlazorServerTemplateTest.cs b/src/ProjectTemplates/test/BlazorServerTemplateTest.cs
index bf9fe12f2c..a0b8f6cc60 100644
--- a/src/ProjectTemplates/test/BlazorServerTemplateTest.cs
+++ b/src/ProjectTemplates/test/BlazorServerTemplateTest.cs
@@ -36,7 +36,7 @@ namespace Templates.Test
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
- // The output from publish will go into bin/Release/netcoreapp3.1/publish and won't be affected by calling build
+ // The output from publish will go into bin/Release/netcoreapp5.0/publish and won't be affected by calling build
// later, while the opposite is not true.
var buildResult = await Project.RunDotNetBuildAsync();
@@ -93,7 +93,7 @@ namespace Templates.Test
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
- // The output from publish will go into bin/Release/netcoreapp3.1/publish and won't be affected by calling build
+ // The output from publish will go into bin/Release/netcoreapp5.0/publish and won't be affected by calling build
// later, while the opposite is not true.
var buildResult = await Project.RunDotNetBuildAsync();
diff --git a/src/ProjectTemplates/test/EmptyWebTemplateTest.cs b/src/ProjectTemplates/test/EmptyWebTemplateTest.cs
index 64f5d8cf3e..14fae1e2d1 100644
--- a/src/ProjectTemplates/test/EmptyWebTemplateTest.cs
+++ b/src/ProjectTemplates/test/EmptyWebTemplateTest.cs
@@ -32,11 +32,17 @@ namespace Templates.Test
var createResult = await Project.RunDotNetNewAsync("web", language: languageOverride);
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult));
+ // Avoid the F# compiler. See https://github.com/aspnet/AspNetCore/issues/14022
+ if (languageOverride != null)
+ {
+ return;
+ }
+
var publishResult = await Project.RunDotNetPublishAsync();
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
- // The output from publish will go into bin/Release/netcoreapp3.1/publish and won't be affected by calling build
+ // The output from publish will go into bin/Release/netcoreapp5.0/publish and won't be affected by calling build
// later, while the opposite is not true.
var buildResult = await Project.RunDotNetBuildAsync();
diff --git a/src/ProjectTemplates/test/Helpers/Project.cs b/src/ProjectTemplates/test/Helpers/Project.cs
index e768bc3b23..5ac3cde3ec 100644
--- a/src/ProjectTemplates/test/Helpers/Project.cs
+++ b/src/ProjectTemplates/test/Helpers/Project.cs
@@ -21,7 +21,7 @@ namespace Templates.Test.Helpers
{
private const string _urls = "http://127.0.0.1:0;https://127.0.0.1:0";
- public const string DefaultFramework = "netcoreapp3.1";
+ public const string DefaultFramework = "netcoreapp5.0";
public static bool IsCIEnvironment => typeof(Project).Assembly.GetCustomAttributes()
.Any(a => a.Key == "ContinuousIntegrationBuild");
diff --git a/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs b/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs
index 2c34259592..f9de81d768 100644
--- a/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs
+++ b/src/ProjectTemplates/test/Helpers/TemplatePackageInstaller.cs
@@ -32,10 +32,12 @@ namespace Templates.Test.Helpers
"Microsoft.DotNet.Web.ProjectTemplates.2.2",
"Microsoft.DotNet.Web.ProjectTemplates.3.0",
"Microsoft.DotNet.Web.ProjectTemplates.3.1",
+ "Microsoft.DotNet.Web.ProjectTemplates.5.0",
"Microsoft.DotNet.Web.Spa.ProjectTemplates.2.1",
"Microsoft.DotNet.Web.Spa.ProjectTemplates.2.2",
"Microsoft.DotNet.Web.Spa.ProjectTemplates.3.0",
"Microsoft.DotNet.Web.Spa.ProjectTemplates.3.1",
+ "Microsoft.DotNet.Web.Spa.ProjectTemplates.5.0",
"Microsoft.DotNet.Web.Spa.ProjectTemplates"
};
@@ -90,7 +92,7 @@ namespace Templates.Test.Helpers
/*
* The templates are indexed by path, for example:
- &USERPROFILE%\.templateengine\dotnetcli\v3.0.100-preview7-012821\packages\nunit3.dotnetnew.template.1.6.1.nupkg
+ &USERPROFILE%\.templateengine\dotnetcli\v5.0.100-alpha1-013788\packages\nunit3.dotnetnew.template.1.6.1.nupkg
Templates:
NUnit 3 Test Project (nunit) C#
NUnit 3 Test Item (nunit-test) C#
@@ -99,7 +101,7 @@ namespace Templates.Test.Helpers
NUnit 3 Test Project (nunit) VB
NUnit 3 Test Item (nunit-test) VB
Uninstall Command:
- dotnet new -u &USERPROFILE%\.templateengine\dotnetcli\v3.0.100-preview7-012821\packages\nunit3.dotnetnew.template.1.6.1.nupkg
+ dotnet new -u &USERPROFILE%\.templateengine\dotnetcli\v5.0.100-alpha1-013788\packages\nunit3.dotnetnew.template.1.6.1.nupkg
* We don't want to construct this path so we'll rely on dotnet new --uninstall --help to construct the uninstall command.
*/
diff --git a/src/ProjectTemplates/test/IdentityUIPackageTest.cs b/src/ProjectTemplates/test/IdentityUIPackageTest.cs
index 204c560859..80f5768c79 100644
--- a/src/ProjectTemplates/test/IdentityUIPackageTest.cs
+++ b/src/ProjectTemplates/test/IdentityUIPackageTest.cs
@@ -134,7 +134,7 @@ namespace Templates.Test
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
- // The output from publish will go into bin/Release/netcoreapp3.1/publish and won't be affected by calling build
+ // The output from publish will go into bin/Release/netcoreapp5.0/publish and won't be affected by calling build
// later, while the opposite is not true.
var buildResult = await Project.RunDotNetBuildAsync(packageOptions: packageOptions);
diff --git a/src/ProjectTemplates/test/Infrastructure/Directory.Build.props.in b/src/ProjectTemplates/test/Infrastructure/Directory.Build.props.in
index 6186fc2f15..9990532b1d 100644
--- a/src/ProjectTemplates/test/Infrastructure/Directory.Build.props.in
+++ b/src/ProjectTemplates/test/Infrastructure/Directory.Build.props.in
@@ -1,3 +1,6 @@
+
+ netcoreapp5.0
+
diff --git a/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in b/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in
index 11b43cc0cb..e012ba42b7 100644
--- a/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in
+++ b/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in
@@ -5,8 +5,6 @@
$(MSBuildThisFileDirectory)runtimeconfig.norollforward.json
-
- ${MicrosoftNETCorePlatformsPackageVersion}
diff --git a/src/ProjectTemplates/test/MvcTemplateTest.cs b/src/ProjectTemplates/test/MvcTemplateTest.cs
index 95f6ce784a..9fef0e00ff 100644
--- a/src/ProjectTemplates/test/MvcTemplateTest.cs
+++ b/src/ProjectTemplates/test/MvcTemplateTest.cs
@@ -44,11 +44,17 @@ namespace Templates.Test
Assert.DoesNotContain("Microsoft.EntityFrameworkCore.Tools.DotNet", projectFileContents);
Assert.DoesNotContain("Microsoft.Extensions.SecretManager.Tools", projectFileContents);
+ // Avoid the F# compiler. See https://github.com/aspnet/AspNetCore/issues/14022
+ if (languageOverride != null)
+ {
+ return;
+ }
+
var publishResult = await Project.RunDotNetPublishAsync();
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
- // The output from publish will go into bin/Release/netcoreapp3.1/publish and won't be affected by calling build
+ // The output from publish will go into bin/Release/netcoreapp5.0/publish and won't be affected by calling build
// later, while the opposite is not true.
var buildResult = await Project.RunDotNetBuildAsync();
@@ -116,7 +122,7 @@ namespace Templates.Test
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
- // The output from publish will go into bin/Release/netcoreapp3.1/publish and won't be affected by calling build
+ // The output from publish will go into bin/Release/netcoreapp5.0/publish and won't be affected by calling build
// later, while the opposite is not true.
var buildResult = await Project.RunDotNetBuildAsync();
diff --git a/src/ProjectTemplates/test/ProjectTemplates.Tests.csproj b/src/ProjectTemplates/test/ProjectTemplates.Tests.csproj
index b8797c1345..f755b3b8f2 100644
--- a/src/ProjectTemplates/test/ProjectTemplates.Tests.csproj
+++ b/src/ProjectTemplates/test/ProjectTemplates.Tests.csproj
@@ -62,6 +62,7 @@
<_Parameter2>true
+
$([MSBuild]::NormalizePath('$(OutputPath)$(TestTemplateCreationFolder)'))
@@ -79,7 +80,7 @@
<_Parameter1>ArtifactsLogDir
<_Parameter2>$([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'log'))
-
+
<_Parameter1>ArtifactsNonShippingPackagesDir
<_Parameter2>$(ArtifactsNonShippingPackagesDir)
diff --git a/src/ProjectTemplates/test/RazorClassLibraryTemplateTest.cs b/src/ProjectTemplates/test/RazorClassLibraryTemplateTest.cs
index 8186ab4156..8a044fed5d 100644
--- a/src/ProjectTemplates/test/RazorClassLibraryTemplateTest.cs
+++ b/src/ProjectTemplates/test/RazorClassLibraryTemplateTest.cs
@@ -33,7 +33,7 @@ namespace Templates.Test
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
- // The output from publish will go into bin/Release/netcoreapp3.1/publish and won't be affected by calling build
+ // The output from publish will go into bin/Release/netcoreapp5.0/publish and won't be affected by calling build
// later, while the opposite is not true.
var buildResult = await Project.RunDotNetBuildAsync();
@@ -52,7 +52,7 @@ namespace Templates.Test
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
- // The output from publish will go into bin/Release/netcoreapp3.1/publish and won't be affected by calling build
+ // The output from publish will go into bin/Release/netcoreapp5.0/publish and won't be affected by calling build
// later, while the opposite is not true.
var buildResult = await Project.RunDotNetBuildAsync();
diff --git a/src/ProjectTemplates/test/RazorPagesTemplateTest.cs b/src/ProjectTemplates/test/RazorPagesTemplateTest.cs
index 637d7179b5..d2e36aa31a 100644
--- a/src/ProjectTemplates/test/RazorPagesTemplateTest.cs
+++ b/src/ProjectTemplates/test/RazorPagesTemplateTest.cs
@@ -45,7 +45,7 @@ namespace Templates.Test
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, createResult));
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
- // The output from publish will go into bin/Release/netcoreapp3.1/publish and won't be affected by calling build
+ // The output from publish will go into bin/Release/netcoreapp5.0/publish and won't be affected by calling build
// later, while the opposite is not true.
var buildResult = await Project.RunDotNetBuildAsync();
@@ -115,7 +115,7 @@ namespace Templates.Test
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
- // The output from publish will go into bin/Release/netcoreapp3.1/publish and won't be affected by calling build
+ // The output from publish will go into bin/Release/netcoreapp5.0/publish and won't be affected by calling build
// later, while the opposite is not true.
var buildResult = await Project.RunDotNetBuildAsync();
diff --git a/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs b/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs
index 700349b773..df363915b8 100644
--- a/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs
+++ b/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs
@@ -66,17 +66,15 @@ namespace Templates.Test.SpaTemplateTest
using var lintResult = await ProcessEx.RunViaShellAsync(Output, clientAppSubdirPath, "npm run lint");
Assert.True(0 == lintResult.ExitCode, ErrorMessages.GetFailedProcessMessage("npm run lint", Project, lintResult));
- if (template == "react" || template == "reactredux")
- {
- using var testResult = await ProcessEx.RunViaShellAsync(Output, clientAppSubdirPath, "npm run test");
- Assert.True(0 == testResult.ExitCode, ErrorMessages.GetFailedProcessMessage("npm run test", Project, testResult));
- }
+ var testcommand = "npm run test" + template == "angular" ? "-- --watch=false" : "";
+ var testResult = await ProcessEx.RunViaShellAsync(Output, clientAppSubdirPath, testcommand);
+ Assert.True(0 == testResult.ExitCode, ErrorMessages.GetFailedProcessMessage("npm run test", Project, testResult));
using var publishResult = await Project.RunDotNetPublishAsync();
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
- // The output from publish will go into bin/Release/netcoreapp3.1/publish and won't be affected by calling build
+ // The output from publish will go into bin/Release/netcoreapp5.0/publish and won't be affected by calling build
// later, while the opposite is not true.
using var buildResult = await Project.RunDotNetBuildAsync();
diff --git a/src/ProjectTemplates/test/WebApiTemplateTest.cs b/src/ProjectTemplates/test/WebApiTemplateTest.cs
index 8e08db9068..797cbfdc39 100644
--- a/src/ProjectTemplates/test/WebApiTemplateTest.cs
+++ b/src/ProjectTemplates/test/WebApiTemplateTest.cs
@@ -32,11 +32,17 @@ namespace Templates.Test
var createResult = await Project.RunDotNetNewAsync("webapi", language: languageOverride);
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult));
+ // Avoid the F# compiler. See https://github.com/aspnet/AspNetCore/issues/14022
+ if (languageOverride != null)
+ {
+ return;
+ }
+
var publishResult = await Project.RunDotNetPublishAsync();
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
- // The output from publish will go into bin/Release/netcoreapp3.1/publish and won't be affected by calling build
+ // The output from publish will go into bin/Release/netcoreapp5.0/publish and won't be affected by calling build
// later, while the opposite is not true.
var buildResult = await Project.RunDotNetBuildAsync();
diff --git a/src/ProjectTemplates/test/WorkerTemplateTest.cs b/src/ProjectTemplates/test/WorkerTemplateTest.cs
index fbe88be770..f4016a76af 100644
--- a/src/ProjectTemplates/test/WorkerTemplateTest.cs
+++ b/src/ProjectTemplates/test/WorkerTemplateTest.cs
@@ -32,7 +32,7 @@ namespace Templates.Test
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
- // The output from publish will go into bin/Release/netcoreapp3.1/publish and won't be affected by calling build
+ // The output from publish will go into bin/Release/netcoreapp5.0/publish and won't be affected by calling build
// later, while the opposite is not true.
var buildResult = await Project.RunDotNetBuildAsync();
diff --git a/src/ProjectTemplates/test/template-baselines.json b/src/ProjectTemplates/test/template-baselines.json
index d934d8b545..618bb27a1f 100644
--- a/src/ProjectTemplates/test/template-baselines.json
+++ b/src/ProjectTemplates/test/template-baselines.json
@@ -1209,6 +1209,7 @@
"ClientApp/src/App.test.js",
"ClientApp/src/index.js",
"ClientApp/src/registerServiceWorker.js",
+ "ClientApp/.env",
"ClientApp/.gitignore",
"ClientApp/package-lock.json",
"ClientApp/package.json",
@@ -1251,6 +1252,7 @@
"ClientApp/src/index.tsx",
"ClientApp/src/react-app-env.d.ts",
"ClientApp/src/registerServiceWorker.ts",
+ "ClientApp/.env",
"ClientApp/.eslintrc.json",
"ClientApp/.gitignore",
"ClientApp/package-lock.json",
diff --git a/src/Security/Authentication/Certificate/src/README.md b/src/Security/Authentication/Certificate/src/README.md
index 542131fdf1..b5654819e6 100644
--- a/src/Security/Authentication/Certificate/src/README.md
+++ b/src/Security/Authentication/Certificate/src/README.md
@@ -1,31 +1,22 @@
# Microsoft.AspNetCore.Authentication.Certificate
-This project sort of contains an implementation of [Certificate Authentication](https://tools.ietf.org/html/rfc5246#section-7.4.4) for ASP.NET Core.
-Certificate authentication happens at the TLS level, long before it ever gets to ASP.NET Core, so, more accurately this is an authentication handler
-that validates the certificate and then gives you an event where you can resolve that certificate to a ClaimsPrincipal.
+This project sort of contains an implementation of [Certificate Authentication](https://tools.ietf.org/html/rfc5246#section-7.4.4) for ASP.NET Core. Certificate authentication happens at the TLS level, long before it ever gets to ASP.NET Core, so, more accurately this is an authentication handler that validates the certificate and then gives you an event where you can resolve that certificate to a ClaimsPrincipal.
-You **must** [configure your host](#hostConfiguration) for certificate authentication, be it IIS, Kestrel, Azure Web Applications or whatever else you're using.
+You **must** [configure your host](#configuring-your-host-to-require-certificates) for certificate authentication, be it IIS, Kestrel, Azure Web Applications or whatever else you're using.
## Getting started
-First acquire an HTTPS certificate, apply it and then [configure your host](#hostConfiguration) to require certificates.
+First acquire an HTTPS certificate, apply it and then [configure your host](#configuring-your-host-to-require-certificates) to require certificates.
-In your web application add a reference to the package, then in the `ConfigureServices` method in `startup.cs` call
-`app.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).UseCertificateAuthentication(...);` with your options,
-providing a delegate for `OnValidateCertificate` to validate the client certificate sent with requests and turn that information
-into an `ClaimsPrincipal`, set it on the `context.Principal` property and call `context.Success()`.
+In your web application add a reference to the package, then in the `ConfigureServices` method in `startup.cs` call `app.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).UseCertificateAuthentication(...);` with your options, providing a delegate for `OnValidateCertificate` to validate the client certificate sent with requests and turn that information into an `ClaimsPrincipal`, set it on the `context.Principal` property and call `context.Success()`.
-If you change your scheme name in the options for the authentication handler you need to change the scheme name in
-`AddAuthentication()` to ensure it's used on every request which ends in an endpoint that requires authorization.
+If you change your scheme name in the options for the authentication handler you need to change the scheme name in `AddAuthentication()` to ensure it's used on every request which ends in an endpoint that requires authorization.
-If authentication fails this handler will return a `403 (Forbidden)` response rather a `401 (Unauthorized)` as you
-might expect - this is because the authentication should happen during the initial TLS connection - by the time it
-reaches the handler it's too late, and there's no way to actually upgrade the connection from an anonymous connection
-to one with a certificate.
+If authentication fails this handler will return a `403 (Forbidden)` response rather a `401 (Unauthorized)` as you might expect - this is because the authentication should happen during the initial TLS connection - by the time it reaches the handler it's too late, and there's no way to actually upgrade the connection from an anonymous connection to one with a certificate.
You must also add `app.UseAuthentication();` in the `Configure` method, otherwise nothing will ever get called.
-For example;
+For example:
```c#
public void ConfigureServices(IServiceCollection services)
@@ -47,25 +38,19 @@ In the sample above you can see the default way to add certificate authenticatio
## Configuring Certificate Validation
-The `CertificateAuthenticationOptions` handler has some built in validations that are the minimium validations you should perform on
-a certificate. Each of these settings are turned on by default.
+The `CertificateAuthenticationOptions` handler has some built in validations that are the minimum validations you should perform on a certificate. Each of these settings are turned on by default.
### ValidateCertificateChain
-This check validates that the issuer for the certificate is trusted by the application host OS. If
-you are going to accept self-signed certificates you must disable this check.
+This check validates that the issuer for the certificate is trusted by the application host OS. If you are going to accept self-signed certificates you must disable this check.
### ValidateCertificateUse
-This check validates that the certificate presented by the client has the Client Authentication
-extended key use, or no EKUs at all (as the specifications say if no EKU is specified then all EKUs
-are valid).
+This check validates that the certificate presented by the client has the Client Authentication extended key use, or no EKUs at all (as the specifications say if no EKU is specified then all EKUs are valid).
### ValidateValidityPeriod
-This check validates that the certificate is within its validity period. As the handler runs on every
-request this ensures that a certificate that was valid when it was presented has not expired during
-its current session.
+This check validates that the certificate is within its validity period. As the handler runs on every request this ensures that a certificate that was valid when it was presented has not expired during its current session.
### RevocationFlag
@@ -73,24 +58,21 @@ A flag which specifies which certificates in the chain are checked for revocatio
Revocation checks are only performed when the certificate is chained to a root certificate.
-### RevocationMode
+### RevocationMode
A flag which specifies how revocation checks are performed.
+
Specifying an on-line check can result in a long delay while the certificate authority is contacted.
Revocation checks are only performed when the certificate is chained to a root certificate.
### Can I configure my application to require a certificate only on certain paths?
-Not possible, remember the certificate exchange is done that the start of the HTTPS conversation,
-it's done by the host, not the application. Kestrel, IIS, Azure Web Apps don't have any configuration for
-this sort of thing.
+Not possible, remember the certificate exchange is done that the start of the HTTPS conversation, it's done by the host, not the application. Kestrel, IIS, Azure Web Apps don't have any configuration for this sort of thing.
-# Handler events
+## Handler events
-The handler has two events, `OnAuthenticationFailed()`, which is called if an exception happens during authentication and allows you to react, and `OnValidateCertificate()` which is
-called after certificate has been validated, passed validation, abut before the default principal has been created. This allows you to perform your own validation, for example
-checking if the certificate is one your services knows about, and to construct your own principal. For example,
+The handler has two events, `OnAuthenticationFailed()`, which is called if an exception happens during authentication and allows you to react, and `OnValidateCertificate()` which is called after certificate has been validated, passed validation, abut before the default principal has been created. This allows you to perform your own validation, for example checking if the certificate is one your services knows about, and to construct your own principal. For example:
```c#
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
@@ -117,8 +99,7 @@ services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationSchem
If you find the inbound certificate doesn't meet your extra validation call `context.Fail("failure Reason")` with a failure reason.
-For real functionality you will probably want to call a service registered in DI which talks to a database or other type of
-user store. You can grab your service by using the context passed into your delegates, like so
+For real functionality you will probably want to call a service registered in DI which talks to a database or other type of user store. You can grab your service by using the context passed into your delegates, like so
```c#
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
@@ -130,7 +111,7 @@ services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationSchem
{
var validationService =
context.HttpContext.RequestServices.GetService();
-
+
if (validationService.ValidateCertificate(context.ClientCertificate))
{
var claims = new[]
@@ -141,17 +122,18 @@ services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationSchem
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
- }
+ }
return Task.CompletedTask;
}
};
});
```
+
Note that conceptually the validation of the certification is an authorization concern, and putting a check on, for example, an issuer or thumbprint in an authorization policy rather
than inside OnCertificateValidated() is perfectly acceptable.
-## Configuring your host to require certificates
+## Configuring your host to require certificates
### Kestrel
@@ -170,12 +152,12 @@ public static IWebHost BuildWebHost(string[] args)
})
.Build();
```
-You must set the `ClientCertificateValidation` delegate to `CertificateValidator.DisableChannelValidation` in order to stop Kestrel using the default OS certificate validation routine and,
-instead, letting the authentication handler perform the validation.
+
+You must set the `ClientCertificateValidation` delegate to `CertificateValidator.DisableChannelValidation` in order to stop Kestrel using the default OS certificate validation routine and, instead, letting the authentication handler perform the validation.
### IIS
-In the IIS Manager
+In the IIS Manager:
1. Select your Site in the Connections tab.
2. Double click the SSL Settings in the Features View window.
@@ -185,9 +167,7 @@ In the IIS Manager
### Azure
-See the [Azure documentation](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth)
-to configure Azure Web Apps then add the following to your application startup method, `Configure(IApplicationBuilder app)` add the
-following line before the call to `app.UseAuthentication();`
+See the [Azure documentation](https://docs.microsoft.com/azure/app-service/app-service-web-configure-tls-mutual-auth) to configure Azure Web Apps then add the following to your application startup method, `Configure(IApplicationBuilder app)` add the following line before the call to `app.UseAuthentication();`:
```c#
app.UseCertificateHeaderForwarding();
@@ -195,18 +175,13 @@ app.UseCertificateHeaderForwarding();
### Random custom web proxies
-If you're using a proxy which isn't IIS or Azure's Web Apps Application Request Routing you will need to configure your proxy
-to forward the certificate it received in an HTTP header.
-In your application startup method, `Configure(IApplicationBuilder app)`, add the
-following line before the call to `app.UseAuthentication();`
+If you're using a proxy which isn't IIS or Azure's Web Apps Application Request Routing you will need to configure your proxy to forward the certificate it received in an HTTP header. In your application startup method, `Configure(IApplicationBuilder app)`, add the following line before the call to `app.UseAuthentication();`:
```c#
app.UseCertificateForwarding();
```
-You will also need to configure the Certificate Forwarding middleware to specify the header name.
-In your service configuration method, `ConfigureServices(IServiceCollection services)` add
-the following code to configure the header the forwarding middleware will build a certificate from;
+You will also need to configure the Certificate Forwarding middleware to specify the header name. In your service configuration method, `ConfigureServices(IServiceCollection services)` add the following code to configure the header the forwarding middleware will build a certificate from:
```c#
services.AddCertificateForwarding(options =>
@@ -215,9 +190,7 @@ services.AddCertificateForwarding(options =>
});
```
-Finally, if your proxy is doing something weird to pass the header on, rather than base 64 encoding it
-(looking at you nginx (╯°□°)╯︵ ┻━┻) you can override the converter option to be a func that will
-perform the optional conversion, for example
+Finally, if your proxy is doing something weird to pass the header on, rather than base 64 encoding it (looking at you nginx (╯°□°)╯︵ ┻━┻) you can override the converter option to be a func that will perform the optional conversion, for example
```c#
services.AddCertificateForwarding(options =>
@@ -231,4 +204,3 @@ services.AddCertificateForwarding(options =>
}
});
```
-
diff --git a/src/Security/Authentication/MicrosoftAccount/src/MicrosoftChallengeProperties.cs b/src/Security/Authentication/MicrosoftAccount/src/MicrosoftChallengeProperties.cs
index 8625e3f093..4e9737b509 100644
--- a/src/Security/Authentication/MicrosoftAccount/src/MicrosoftChallengeProperties.cs
+++ b/src/Security/Authentication/MicrosoftAccount/src/MicrosoftChallengeProperties.cs
@@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Authentication.OAuth;
namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount
{
///
- /// See https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code for reference
+ /// See https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code for reference
///
public class MicrosoftChallengeProperties : OAuthChallengeProperties
{
diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/CrossMachineReadMe.md b/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/CrossMachineReadMe.md
index e263a2c5f7..ec83949331 100644
--- a/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/CrossMachineReadMe.md
+++ b/src/Security/Authentication/Negotiate/test/Negotiate.FunctionalTest/CrossMachineReadMe.md
@@ -1,24 +1,24 @@
Cross Machine Tests
-Kerberos can only be tested in a multi-machine environment. On localhost it always falls back to NTLM which has different requirements. Multi-machine is also neccisary for interop testing across OSs. Kerberos also requires domain controler SPN configuration so we can't test it on arbitrary test boxes.
+Kerberos can only be tested in a multi-machine environment. On localhost it always falls back to NTLM which has different requirements. Multi-machine is also necessary for interop testing across OSs. Kerberos also requires domain controller SPN configuration so we can't test it on arbitrary test boxes.
Test structure:
- A remote test server with various endpoints with different authentication restrictions.
-- A remote test client with endpoints that execute specific scenarios. The input for these endpoints is theory data. The output is either 200Ok, or a failure code and desciption.
+- A remote test client with endpoints that execute specific scenarios. The input for these endpoints is theory data. The output is either 200Ok, or a failure code and description.
- The CrossMachineTest class that drives the tests. It invokes the client app with the theory data and confirms the results.
-We use these three components beceause it allows us to run the tests from a dev machine or CI agent that is not part of the dedicated test domain/environment.
+We use these three components because it allows us to run the tests from a dev machine or CI agent that is not part of the dedicated test domain/environment.
(Static) Environment Setup:
- Warning, this environment can take a day to set up. That's why we want a static test environment that we can re-use.
- Create a Windows server running DNS and Active Directory. Promote it to a domain controller.
- Create an SPN on this machine for Windows -> Windows testing. `setspn -S "http/chrross-dc.crkerberos.com" -U administrator`
- Future: Can we replace the domain controller with an AAD instance? We'd still want a second windows machine for Windows -> Windows testing, but AAD might be easier to configure.
- - https://docs.microsoft.com/en-us/azure/active-directory-domain-services/active-directory-ds-getting-started
- - https://docs.microsoft.com/en-us/azure/active-directory-domain-services/active-directory-ds-join-ubuntu-linux-vm
- - https://docs.microsoft.com/en-us/azure/active-directory-domain-services/active-directory-ds-enable-kcd
+ - https://docs.microsoft.com/azure/active-directory-domain-services/active-directory-ds-getting-started
+ - https://docs.microsoft.com/azure/active-directory-domain-services/active-directory-ds-join-ubuntu-linux-vm
+ - https://docs.microsoft.com/azure/active-directory-domain-services/active-directory-ds-enable-kcd
- Create another Windows machine and join it to the test domain.
-- Create a Linux machine and joing it to the domain. Ubuntu 18.04 has been used in the past.
+- Create a Linux machine and joining it to the domain. Ubuntu 18.04 has been used in the past.
- https://www.safesquid.com/content-filtering/integrating-linux-host-windows-ad-kerberos-sso-authentication
- Include an HTTP SPN
diff --git a/src/Security/README.md b/src/Security/README.md
index 0ba28c1e97..5ed702c4f2 100644
--- a/src/Security/README.md
+++ b/src/Security/README.md
@@ -3,9 +3,9 @@ ASP.NET Core Security
Contains the security and authorization middlewares for ASP.NET Core.
-A list of community projects related to authentication and security for ASP.NET Core are listed in the [documentation](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/community).
+A list of community projects related to authentication and security for ASP.NET Core are listed in the [documentation](https://docs.microsoft.com/aspnet/core/security/authentication/community).
-See the [ASP.NET Core security documentation](https://docs.microsoft.com/en-us/aspnet/core/security/).
+See the [ASP.NET Core security documentation](https://docs.microsoft.com/aspnet/core/security/).
### Notes
diff --git a/src/Security/samples/Identity.ExternalClaims/README.md b/src/Security/samples/Identity.ExternalClaims/README.md
index 7a9141075d..70205c0367 100644
--- a/src/Security/samples/Identity.ExternalClaims/README.md
+++ b/src/Security/samples/Identity.ExternalClaims/README.md
@@ -4,7 +4,7 @@ AuthSamples.Identity.ExternalClaims
Sample demonstrating copying over static and dynamic external claims from Google authentication during login:
Steps:
-1. Configure a google OAuth2 project. See https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?tabs=aspnetcore2x for basic setup using google logins.
+1. Configure a google OAuth2 project. See https://docs.microsoft.com/aspnet/core/security/authentication/social/google-logins for basic setup using google logins.
2. Update Startup.cs AddGoogle()'s options with ClientId and ClientSecret for your google app.
3. Run the app and click on the MyClaims tab, this should trigger a redirect to login.
4. Login via the Google button, this should redirect you to google.
diff --git a/src/Servers/HttpSys/src/MessagePump.cs b/src/Servers/HttpSys/src/MessagePump.cs
index f829d6e92d..40efc20b79 100644
--- a/src/Servers/HttpSys/src/MessagePump.cs
+++ b/src/Servers/HttpSys/src/MessagePump.cs
@@ -2,16 +2,18 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections.Generic;
using System.Diagnostics.Contracts;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.HttpSys.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Microsoft.AspNetCore.HttpSys.Internal;
namespace Microsoft.AspNetCore.Server.HttpSys
{
@@ -74,6 +76,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
}
var hostingUrlsPresent = _serverAddresses.Addresses.Count > 0;
+ var serverAddressCopy = _serverAddresses.Addresses.ToList();
+ _serverAddresses.Addresses.Clear();
if (_serverAddresses.PreferHostingUrls && hostingUrlsPresent)
{
@@ -85,10 +89,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
Listener.Options.UrlPrefixes.Clear();
}
- foreach (var value in _serverAddresses.Addresses)
- {
- Listener.Options.UrlPrefixes.Add(value);
- }
+ UpdateUrlPrefixes(serverAddressCopy);
}
else if (_options.UrlPrefixes.Count > 0)
{
@@ -100,23 +101,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys
_serverAddresses.Addresses.Clear();
}
- foreach (var prefix in _options.UrlPrefixes)
- {
- _serverAddresses.Addresses.Add(prefix.FullPrefix);
- }
}
else if (hostingUrlsPresent)
{
- foreach (var value in _serverAddresses.Addresses)
- {
- Listener.Options.UrlPrefixes.Add(value);
- }
+ UpdateUrlPrefixes(serverAddressCopy);
}
else
{
LogHelper.LogDebug(_logger, $"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default.");
- _serverAddresses.Addresses.Add(Constants.DefaultServerAddress);
Listener.Options.UrlPrefixes.Add(Constants.DefaultServerAddress);
}
@@ -129,6 +122,13 @@ namespace Microsoft.AspNetCore.Server.HttpSys
Listener.Start();
+ // Update server addresses after we start listening as port 0
+ // needs to be selected at the point of binding.
+ foreach (var prefix in _options.UrlPrefixes)
+ {
+ _serverAddresses.Addresses.Add(prefix.FullPrefix);
+ }
+
ActivateRequestProcessingLimits();
return Task.CompletedTask;
@@ -142,6 +142,14 @@ namespace Microsoft.AspNetCore.Server.HttpSys
}
}
+ private void UpdateUrlPrefixes(IList serverAddressCopy)
+ {
+ foreach (var value in serverAddressCopy)
+ {
+ Listener.Options.UrlPrefixes.Add(value);
+ }
+ }
+
// The message pump.
// When we start listening for the next request on one thread, we may need to be sure that the
// completion continues on another thread as to not block the current request processing.
diff --git a/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs b/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs
index 8f33c7b678..87a5012641 100644
--- a/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs
+++ b/src/Servers/HttpSys/src/NativeInterop/UrlGroup.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@@ -73,7 +73,6 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
LogHelper.LogInfo(_logger, "Listening on prefix: " + uriPrefix);
CheckDisposed();
-
var statusCode = HttpApi.HttpAddUrlToUrlGroup(Id, uriPrefix, (ulong)contextId, 0);
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs
index 826cf629da..3d7d9a4088 100644
--- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs
+++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs
@@ -274,7 +274,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
Protocol = handshake.Protocol;
// The OS considers client and server TLS as different enum values. SslProtocols choose to combine those for some reason.
// We need to fill in the client bits so the enum shows the expected protocol.
- // https://docs.microsoft.com/en-us/windows/desktop/api/schannel/ns-schannel-_secpkgcontext_connectioninfo
+ // https://docs.microsoft.com/windows/desktop/api/schannel/ns-schannel-_secpkgcontext_connectioninfo
// Compare to https://referencesource.microsoft.com/#System/net/System/Net/SecureProtocols/_SslState.cs,8905d1bf17729de3
#pragma warning disable CS0618 // Type or member is obsolete
if ((Protocol & SslProtocols.Ssl2) != 0)
diff --git a/src/Servers/HttpSys/src/UrlPrefixCollection.cs b/src/Servers/HttpSys/src/UrlPrefixCollection.cs
index 92c50aea09..954b3d6d6f 100644
--- a/src/Servers/HttpSys/src/UrlPrefixCollection.cs
+++ b/src/Servers/HttpSys/src/UrlPrefixCollection.cs
@@ -1,8 +1,13 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using System.Collections;
using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Microsoft.AspNetCore.HttpSys.Internal;
namespace Microsoft.AspNetCore.Server.HttpSys
{
@@ -15,6 +20,12 @@ namespace Microsoft.AspNetCore.Server.HttpSys
private UrlGroup _urlGroup;
private int _nextId = 1;
+ // Valid port range of 5000 - 48000.
+ private const int BasePort = 5000;
+ private const int MaxPortIndex = 43000;
+ private const int MaxRetries = 1000;
+ private static int NextPortIndex;
+
internal UrlPrefixCollection()
{
}
@@ -138,10 +149,55 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
_urlGroup = urlGroup;
// go through the uri list and register for each one of them
- foreach (var pair in _prefixes)
+ // Call ToList to avoid modification when enumerating.
+ foreach (var pair in _prefixes.ToList())
{
- // We'll get this index back on each request and use it to look up the prefix to calculate PathBase.
- _urlGroup.RegisterPrefix(pair.Value.FullPrefix, pair.Key);
+ var urlPrefix = pair.Value;
+ if (urlPrefix.PortValue == 0)
+ {
+ if (urlPrefix.IsHttps)
+ {
+ throw new InvalidOperationException("Cannot bind to port 0 with https.");
+ }
+
+ FindHttpPortUnsynchronized(pair.Key, urlPrefix);
+ }
+ else
+ {
+ // We'll get this index back on each request and use it to look up the prefix to calculate PathBase.
+ _urlGroup.RegisterPrefix(pair.Value.FullPrefix, pair.Key);
+ }
+ }
+ }
+ }
+
+ private void FindHttpPortUnsynchronized(int key, UrlPrefix urlPrefix)
+ {
+ for (var index = 0; index < MaxRetries; index++)
+ {
+ try
+ {
+ // Bit of complicated math to always try 3000 ports, starting from NextPortIndex + 5000,
+ // circling back around if we go above 8000 back to 5000, and so on.
+ var port = ((index + NextPortIndex) % MaxPortIndex) + BasePort;
+
+ Debug.Assert(port >= 5000 || port < 8000);
+
+ var newPrefix = UrlPrefix.Create(urlPrefix.Scheme, urlPrefix.Host, port, urlPrefix.Path);
+ _urlGroup.RegisterPrefix(newPrefix.FullPrefix, key);
+ _prefixes[key] = newPrefix;
+
+ NextPortIndex += index + 1;
+ return;
+ }
+ catch (HttpSysException ex)
+ {
+ if ((ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_ACCESS_DENIED
+ && ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SHARING_VIOLATION
+ && ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_ALREADY_EXISTS) || index == MaxRetries - 1)
+ {
+ throw;
+ }
}
}
}
@@ -159,4 +215,4 @@ namespace Microsoft.AspNetCore.Server.HttpSys
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Servers/HttpSys/startvs.cmd b/src/Servers/HttpSys/startvs.cmd
new file mode 100644
index 0000000000..94b00042d2
--- /dev/null
+++ b/src/Servers/HttpSys/startvs.cmd
@@ -0,0 +1,3 @@
+@ECHO OFF
+
+%~dp0..\..\..\startvs.cmd %~dp0HttpSysServer.sln
diff --git a/src/Servers/HttpSys/test/FunctionalTests/MessagePumpTests.cs b/src/Servers/HttpSys/test/FunctionalTests/MessagePumpTests.cs
index 250ff9c9be..f9b4d81209 100644
--- a/src/Servers/HttpSys/test/FunctionalTests/MessagePumpTests.cs
+++ b/src/Servers/HttpSys/test/FunctionalTests/MessagePumpTests.cs
@@ -114,7 +114,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
- Assert.Equal(Constants.DefaultServerAddress, server.Features.Get().Addresses.Single());
+ // Trailing slash is added when put in UrlPrefix.
+ Assert.StartsWith(Constants.DefaultServerAddress, server.Features.Get().Addresses.Single());
}
}
diff --git a/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs b/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs
index 8ba9e7b39a..a347ce427f 100644
--- a/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs
+++ b/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
@@ -21,11 +22,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
// When tests projects are run in parallel, overlapping port ranges can cause a race condition when looking for free
// ports during dynamic port allocation.
- private const int BasePort = 5001;
- private const int MaxPort = 8000;
private const int BaseHttpsPort = 44300;
private const int MaxHttpsPort = 44399;
- private static int NextPort = BasePort;
private static int NextHttpsPort = BaseHttpsPort;
private static object PortLock = new object();
internal static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15);
@@ -84,39 +82,26 @@ namespace Microsoft.AspNetCore.Server.HttpSys
internal static IWebHost CreateDynamicHost(string basePath, out string root, out string baseAddress, Action configureOptions, RequestDelegate app)
{
- lock (PortLock)
- {
- while (NextPort < MaxPort)
+ var prefix = UrlPrefix.Create("http", "localhost", "0", basePath);
+
+ var builder = new WebHostBuilder()
+ .UseHttpSys(options =>
{
- var port = NextPort++;
- var prefix = UrlPrefix.Create("http", "localhost", port, basePath);
- root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
- baseAddress = prefix.ToString();
+ options.UrlPrefixes.Add(prefix);
+ configureOptions(options);
+ })
+ .Configure(appBuilder => appBuilder.Run(app));
- var builder = new WebHostBuilder()
- .UseHttpSys(options =>
- {
- options.UrlPrefixes.Add(prefix);
- configureOptions(options);
- })
- .Configure(appBuilder => appBuilder.Run(app));
+ var host = builder.Build();
- var host = builder.Build();
+ host.Start();
+ var options = host.Services.GetRequiredService>();
+ prefix = options.Value.UrlPrefixes.First(); // Has new port
+ root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
+ baseAddress = prefix.ToString();
- try
- {
- host.Start();
- return host;
- }
- catch (HttpSysException)
- {
- }
-
- }
- NextPort = BasePort;
- }
- throw new Exception("Failed to locate a free port.");
+ return host;
}
internal static MessagePump CreatePump()
@@ -131,30 +116,17 @@ namespace Microsoft.AspNetCore.Server.HttpSys
internal static IServer CreateDynamicHttpServer(string basePath, out string root, out string baseAddress, Action configureOptions, RequestDelegate app)
{
- lock (PortLock)
- {
- while (NextPort < MaxPort)
- {
+ var prefix = UrlPrefix.Create("http", "localhost", "0", basePath);
- var port = NextPort++;
- var prefix = UrlPrefix.Create("http", "localhost", port, basePath);
- root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
- baseAddress = prefix.ToString();
+ var server = CreatePump(configureOptions);
+ server.Features.Get().Addresses.Add(prefix.ToString());
+ server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
- var server = CreatePump(configureOptions);
- server.Features.Get().Addresses.Add(baseAddress);
- try
- {
- server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
- return server;
- }
- catch (HttpSysException)
- {
- }
- }
- NextPort = BasePort;
- }
- throw new Exception("Failed to locate a free port.");
+ prefix = server.Listener.Options.UrlPrefixes.First(); // Has new port
+ root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
+ baseAddress = prefix.ToString();
+
+ return server;
}
internal static IServer CreateDynamicHttpsServer(out string baseAddress, RequestDelegate app)
diff --git a/src/Servers/HttpSys/test/Tests/UrlPrefixTests.cs b/src/Servers/HttpSys/test/Tests/UrlPrefixTests.cs
index 8614ac36db..20d0f0713f 100644
--- a/src/Servers/HttpSys/test/Tests/UrlPrefixTests.cs
+++ b/src/Servers/HttpSys/test/Tests/UrlPrefixTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/BasicAuthTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/BasicAuthTests.cs
index 6130cdf882..36520369a6 100644
--- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/BasicAuthTests.cs
+++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/BasicAuthTests.cs
@@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
public static TestMatrix TestVariants
=> TestMatrix.ForServers(DeployerSelector.ServerType)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithApplicationTypes(ApplicationType.Portable)
.WithAllHostingModels();
diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs
index ccc4ea6458..6d39a56921 100644
--- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs
+++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/ClientCertificateTests.cs
@@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
public static TestMatrix TestVariants
=> TestMatrix.ForServers(DeployerSelector.ServerType)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithAllApplicationTypes()
.WithAllHostingModels();
diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/CommonStartupTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/CommonStartupTests.cs
index 82df45fdbf..38f4dae818 100644
--- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/CommonStartupTests.cs
+++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/CommonStartupTests.cs
@@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
public static TestMatrix TestVariants
=> TestMatrix.ForServers(DeployerSelector.ServerType)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithAllApplicationTypes()
.WithAllHostingModels();
diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/HttpsTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/HttpsTests.cs
index 9a931f7d52..8e3251570b 100644
--- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/HttpsTests.cs
+++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/HttpsTests.cs
@@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
public static TestMatrix TestVariants
=> TestMatrix.ForServers(DeployerSelector.ServerType)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithAllApplicationTypes()
.WithAllHostingModels();
diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs
index 5445bd24cc..d3aecfc29c 100644
--- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs
+++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs
@@ -172,7 +172,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.InProcess
public static TestMatrix TestVariants
=> TestMatrix.ForServers(DeployerSelector.ServerType)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithAllApplicationTypes()
.WithAncmV2InProcess();
diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/LogFileTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/LogFileTests.cs
index 699bd8b3ac..ffde811027 100644
--- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/LogFileTests.cs
+++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/LogFileTests.cs
@@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
public static TestMatrix TestVariants
=> TestMatrix.ForServers(DeployerSelector.ServerType)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithAllApplicationTypes()
.WithAllHostingModels();
diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/AspNetCorePortTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/AspNetCorePortTests.cs
index ddc981d7af..df160cb44c 100644
--- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/AspNetCorePortTests.cs
+++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/AspNetCorePortTests.cs
@@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.OutOfProcess
public static TestMatrix TestVariants
=> TestMatrix.ForServers(DeployerSelector.ServerType)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithApplicationTypes(ApplicationType.Portable);
public static IEnumerable InvalidTestVariants
diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs
index deaf45f95a..2054011422 100644
--- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs
+++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/OutOfProcess/HelloWorldTest.cs
@@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.OutOfProcess
public static TestMatrix TestVariants
=> TestMatrix.ForServers(DeployerSelector.ServerType)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithAllApplicationTypes();
[ConditionalTheory]
diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/PublishedSitesFixture.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/PublishedSitesFixture.cs
index a0fd00f869..70a7606e31 100644
--- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/PublishedSitesFixture.cs
+++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/PublishedSitesFixture.cs
@@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
RuntimeFlavor = RuntimeFlavor.CoreClr,
RuntimeArchitecture = RuntimeArchitecture.x64,
HostingModel = hostingModel,
- TargetFramework = Tfm.NetCoreApp31
+ TargetFramework = Tfm.NetCoreApp50
});
}
diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs
index 645e595cbf..29d6f9c80c 100644
--- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs
+++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Utilities/IISTestSiteFixture.cs
@@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
{
RuntimeArchitecture = RuntimeArchitecture.x64,
RuntimeFlavor = RuntimeFlavor.CoreClr,
- TargetFramework = Tfm.NetCoreApp31,
+ TargetFramework = Tfm.NetCoreApp50,
HostingModel = HostingModel.InProcess,
PublishApplicationBeforeDeployment = true,
ApplicationPublisher = new PublishedApplicationPublisher(Helpers.GetInProcessTestSitesName()),
diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/WindowsAuthTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/WindowsAuthTests.cs
index f9716c2a26..e22f96dadc 100644
--- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/WindowsAuthTests.cs
+++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/WindowsAuthTests.cs
@@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
public static TestMatrix TestVariants
=> TestMatrix.ForServers(DeployerSelector.ServerType)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithApplicationTypes(ApplicationType.Portable)
.WithAllHostingModels();
diff --git a/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/OutOfProcess/NtlmAuthentationTest.cs b/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/OutOfProcess/NtlmAuthentationTest.cs
index 17f557a338..c63c777c80 100644
--- a/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/OutOfProcess/NtlmAuthentationTest.cs
+++ b/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/OutOfProcess/NtlmAuthentationTest.cs
@@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
public static TestMatrix TestVariants
=> TestMatrix.ForServers(DeployerSelector.ServerType)
- .WithTfms(Tfm.NetCoreApp31);
+ .WithTfms(Tfm.NetCoreApp50);
[ConditionalTheory]
[RequiresIIS(IISCapability.WindowsAuthentication)]
diff --git a/src/Servers/IIS/IntegrationTesting.IIS/src/IISExpressDeployer.cs b/src/Servers/IIS/IntegrationTesting.IIS/src/IISExpressDeployer.cs
index 4206ab8f22..288c11a278 100644
--- a/src/Servers/IIS/IntegrationTesting.IIS/src/IISExpressDeployer.cs
+++ b/src/Servers/IIS/IntegrationTesting.IIS/src/IISExpressDeployer.cs
@@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS
// Start timer
StartTimer();
- // For an unpublished application the dllroot points pre-built dlls like projectdir/bin/debug/netcoreapp3.1/
+ // For an unpublished application the dllroot points pre-built dlls like projectdir/bin/debug/netcoreapp5.0/
// and contentRoot points to the project directory so you get things like static assets.
// For a published app both point to the publish directory.
var dllRoot = CheckIfPublishIsRequired();
diff --git a/src/Servers/Kestrel/Core/src/BadHttpRequestException.cs b/src/Servers/Kestrel/Core/src/BadHttpRequestException.cs
index 929a408778..16f7ab0fce 100644
--- a/src/Servers/Kestrel/Core/src/BadHttpRequestException.cs
+++ b/src/Servers/Kestrel/Core/src/BadHttpRequestException.cs
@@ -139,6 +139,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
BadHttpRequestException ex;
switch (reason)
{
+ case RequestRejectionReason.TlsOverHttpError:
+ ex = new BadHttpRequestException(CoreStrings.HttpParserTlsOverHttpError, StatusCodes.Status400BadRequest, reason);
+ break;
case RequestRejectionReason.InvalidRequestLine:
ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(detail), StatusCodes.Status400BadRequest, reason);
break;
diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx
index 0f49dedc81..20af4f4a9c 100644
--- a/src/Servers/Kestrel/Core/src/CoreStrings.resx
+++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx
@@ -617,4 +617,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
A new stream was refused because this connection has too many streams that haven't finished processing. This may happen if many streams are aborted but not yet cleaned up.
-
+
+ Detected a TLS handshake to an endpoint that does not have TLS enabled.
+
+
\ No newline at end of file
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs
index 2fe5dfdb36..ce63ec989f 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs
@@ -31,6 +31,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private const byte ByteTab = (byte)'\t';
private const byte ByteQuestionMark = (byte)'?';
private const byte BytePercentage = (byte)'%';
+ private const int MinTlsRequestSize = 1; // We need at least 1 byte to check for a proper TLS request line
public unsafe bool ParseRequestLine(TRequestHandler handler, in ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined)
{
@@ -415,9 +416,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return new Span(data, methodLength);
}
+ private unsafe bool IsTlsHandshake(byte* data, int length)
+ {
+ const byte SslRecordTypeHandshake = (byte)0x16;
+
+ // Make sure we can check at least for the existence of a TLS handshake - we check the first byte
+ // See https://serializethoughts.com/2014/07/27/dissecting-tls-client-hello-message/
+
+ return (length >= MinTlsRequestSize && data[0] == SslRecordTypeHandshake);
+ }
+
[StackTraceHidden]
private unsafe void RejectRequestLine(byte* requestLine, int length)
- => throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestLine, requestLine, length);
+ {
+ // Check for incoming TLS handshake over HTTP
+ if (IsTlsHandshake(requestLine, length))
+ {
+ throw GetInvalidRequestException(RequestRejectionReason.TlsOverHttpError, requestLine, length);
+ }
+ else
+ {
+ throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestLine, requestLine, length);
+ }
+ }
[StackTraceHidden]
private unsafe void RejectRequestHeader(byte* headerLine, int length)
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/RequestRejectionReason.cs b/src/Servers/Kestrel/Core/src/Internal/Http/RequestRejectionReason.cs
index fce21b6210..23dc6c67c6 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/RequestRejectionReason.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/RequestRejectionReason.cs
@@ -1,10 +1,11 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
internal enum RequestRejectionReason
{
+ TlsOverHttpError,
UnrecognizedHTTPVersion,
InvalidRequestLine,
InvalidRequestHeader,
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
index 3ccdccef69..10756c0e80 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
@@ -204,27 +204,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
while (_isClosed == 0)
{
var result = await Input.ReadAsync();
- var readableBuffer = result.Buffer;
- var consumed = readableBuffer.Start;
- var examined = readableBuffer.Start;
+ var buffer = result.Buffer;
// Call UpdateCompletedStreams() prior to frame processing in order to remove any streams that have exceded their drain timeouts.
UpdateCompletedStreams();
try
{
- if (!readableBuffer.IsEmpty)
+ while (Http2FrameReader.TryReadFrame(ref buffer, _incomingFrame, _serverSettings.MaxFrameSize, out var framePayload))
{
- if (Http2FrameReader.ReadFrame(readableBuffer, _incomingFrame, _serverSettings.MaxFrameSize, out var framePayload))
- {
- Log.Http2FrameReceived(ConnectionId, _incomingFrame);
- consumed = examined = framePayload.End;
- await ProcessFrameAsync(application, framePayload);
- }
- else
- {
- examined = readableBuffer.End;
- }
+ Log.Http2FrameReceived(ConnectionId, _incomingFrame);
+ await ProcessFrameAsync(application, framePayload);
}
if (result.IsCompleted)
@@ -242,7 +232,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
finally
{
- Input.AdvanceTo(consumed, examined);
+ Input.AdvanceTo(buffer.Start, buffer.End);
UpdateConnectionState();
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameReader.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameReader.cs
index 8437ad334a..ed4db88f0e 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameReader.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameReader.cs
@@ -31,16 +31,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public const int SettingSize = 6; // 2 bytes for the id, 4 bytes for the value.
- public static bool ReadFrame(in ReadOnlySequence readableBuffer, Http2Frame frame, uint maxFrameSize, out ReadOnlySequence framePayload)
+ public static bool TryReadFrame(ref ReadOnlySequence buffer, Http2Frame frame, uint maxFrameSize, out ReadOnlySequence framePayload)
{
framePayload = ReadOnlySequence.Empty;
- if (readableBuffer.Length < HeaderLength)
+ if (buffer.Length < HeaderLength)
{
return false;
}
- var headerSlice = readableBuffer.Slice(0, HeaderLength);
+ var headerSlice = buffer.Slice(0, HeaderLength);
var header = headerSlice.ToSpan();
var payloadLength = (int)Bitshifter.ReadUInt24BigEndian(header);
@@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// Make sure the whole frame is buffered
var frameLength = HeaderLength + payloadLength;
- if (readableBuffer.Length < frameLength)
+ if (buffer.Length < frameLength)
{
return false;
}
@@ -61,10 +61,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
frame.Flags = header[FlagsOffset];
frame.StreamId = (int)Bitshifter.ReadUInt31BigEndian(header.Slice(StreamIdOffset));
- var extendedHeaderLength = ReadExtendedFields(frame, readableBuffer);
+ var extendedHeaderLength = ReadExtendedFields(frame, buffer);
// The remaining payload minus the extra fields
- framePayload = readableBuffer.Slice(HeaderLength + extendedHeaderLength, payloadLength - extendedHeaderLength);
+ framePayload = buffer.Slice(HeaderLength + extendedHeaderLength, payloadLength - extendedHeaderLength);
+ buffer = buffer.Slice(framePayload.End);
return true;
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs
index 7555b9f223..1177504aa6 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs
@@ -239,7 +239,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
}
- public ValueTask WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, in ReadOnlySequence data, bool endStream)
+ public ValueTask WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, in ReadOnlySequence data, bool endStream, bool forceFlush)
{
// The Length property of a ReadOnlySequence can be expensive, so we cache the value.
var dataLength = data.Length;
@@ -261,7 +261,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// This cast is safe since if dataLength would overflow an int, it's guaranteed to be greater than the available flow control window.
flowControl.Advance((int)dataLength);
WriteDataUnsynchronized(streamId, data, dataLength, endStream);
- return TimeFlushUnsynchronizedAsync();
+
+ if (forceFlush)
+ {
+ return TimeFlushUnsynchronizedAsync();
+ }
+
+ return default;
}
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs
index 18adcc1a82..3bd9794e47 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs
@@ -380,7 +380,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// Write any remaining content then write trailers
if (readResult.Buffer.Length > 0)
{
- flushResult = await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: false);
+ // Only flush if required (i.e. content length exceeds flow control availability)
+ // Writing remaining content without flushing allows content and trailers to be sent in the same packet
+ await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: false, forceFlush: false);
}
_stream.ResponseTrailers.SetReadOnly();
@@ -404,7 +406,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
_stream.DecrementActiveClientStreamCount();
}
- flushResult = await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream);
+ flushResult = await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream, forceFlush: true);
}
_pipeReader.AdvanceTo(readResult.Buffer.End);
diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/BufferSegment.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/BufferSegment.cs
index fdd8ac7367..a9bf8d9424 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/BufferSegment.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/BufferSegment.cs
@@ -59,20 +59,16 @@ namespace System.IO.Pipelines
AvailableMemory = arrayPoolBuffer;
}
- public void SetUnownedMemory(Memory memory)
- {
- AvailableMemory = memory;
- }
-
public void ResetMemory()
{
if (_memoryOwner is IMemoryOwner owner)
{
owner.Dispose();
}
- else if (_memoryOwner is byte[] array)
+ else
{
- ArrayPool.Shared.Return(array);
+ byte[] poolArray = (byte[])_memoryOwner;
+ ArrayPool.Shared.Return(poolArray);
}
// Order of below field clears is significant as it clears in a sequential order
diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/ConcurrentPipeWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/ConcurrentPipeWriter.cs
index 3ab051c8c4..4e09b0a8a4 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/ConcurrentPipeWriter.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/ConcurrentPipeWriter.cs
@@ -341,8 +341,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeW
}
else
{
- // We can't use the pool so allocate an array
- newSegment.SetUnownedMemory(new byte[sizeHint]);
+ // We can't use the recommended pool so use the ArrayPool
+ newSegment.SetOwnedMemory(ArrayPool.Shared.Rent(sizeHint));
}
_tailMemory = newSegment.AvailableMemory;
diff --git a/src/Servers/Kestrel/Core/test/HttpParserTests.cs b/src/Servers/Kestrel/Core/test/HttpParserTests.cs
index 7ce8587743..82d69d8b4d 100644
--- a/src/Servers/Kestrel/Core/test/HttpParserTests.cs
+++ b/src/Servers/Kestrel/Core/test/HttpParserTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@@ -394,6 +394,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal(buffer.End, examined);
}
+ [Fact]
+ public void ParseRequestLineTlsOverHttp()
+ {
+ var parser = CreateParser(_nullTrace);
+ var buffer = ReadOnlySequenceFactory.CreateSegments(new byte[] { 0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0xfc, 0x03, 0x03, 0x03, 0xca, 0xe0, 0xfd, 0x0a });
+
+ var requestHandler = new RequestHandler();
+
+ var badHttpRequestException = Assert.Throws(() =>
+ {
+ parser.ParseRequestLine(requestHandler, buffer, out var consumed, out var examined);
+ });
+
+ Assert.Equal(badHttpRequestException.StatusCode, StatusCodes.Status400BadRequest);
+ Assert.Equal(RequestRejectionReason.TlsOverHttpError, badHttpRequestException.Reason);
+ }
+
[Fact]
public void ParseHeadersWithGratuitouslySplitBuffers()
{
diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs
index ccdb774674..600d674d98 100644
--- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs
+++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs
@@ -62,6 +62,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets
throw new InvalidOperationException(SocketsStrings.TransportAlreadyBound);
}
+ // Check if EndPoint is a FileHandleEndpoint before attempting to access EndPoint.AddressFamily
+ // since that will throw an NotImplementedException.
+ if (EndPoint is FileHandleEndPoint)
+ {
+ throw new NotSupportedException(SocketsStrings.FileHandleEndPointNotSupported);
+ }
+
Socket listenSocket;
// Unix domain sockets are unspecified
diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketsStrings.resx b/src/Servers/Kestrel/Transport.Sockets/src/SocketsStrings.resx
index 52b26c66bc..5f1475a1cf 100644
--- a/src/Servers/Kestrel/Transport.Sockets/src/SocketsStrings.resx
+++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketsStrings.resx
@@ -117,10 +117,13 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ The Socket transport does not support binding to file handles. Consider using the libuv transport instead.
+
Only ListenType.IPEndPoint is supported by the Socket Transport. https://go.microsoft.com/fwlink/?linkid=874850
Transport is already bound.
-
\ No newline at end of file
+
diff --git a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs
index 7ddb4deb26..47e3288be5 100644
--- a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs
+++ b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs
@@ -95,7 +95,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2
await stopTask.DefaultTimeout();
}
- Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains("Request finished in"));
+ Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains("Request finished "));
Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains("is closing."));
Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains("is closed. The last processed stream ID was 1."));
}
diff --git a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs
index d3fa68a1bd..14cea7f48a 100644
--- a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs
+++ b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs
@@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
@@ -755,6 +756,176 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
};
testContext.InitializeHeartbeat();
+ var dateHeaderValueManager = new DateHeaderValueManager();
+ dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue);
+ testContext.DateHeaderValueManager = dateHeaderValueManager;
+
+ var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0));
+
+ async Task App(HttpContext context)
+ {
+ context.RequestAborted.Register(() =>
+ {
+ requestAborted = true;
+ });
+
+ for (var i = 0; i < chunkCount; i++)
+ {
+ await context.Response.BodyWriter.WriteAsync(new Memory(chunkData, 0, chunkData.Length), context.RequestAborted);
+ }
+
+ appFuncCompleted.SetResult(null);
+ }
+
+ using (var server = new TestServer(App, testContext, listenOptions))
+ {
+ using (var connection = server.CreateConnection())
+ {
+ // Close the connection with the last request so AssertStreamCompleted actually completes.
+ await connection.Send(
+ "GET / HTTP/1.1",
+ "Host:",
+ "",
+ "");
+
+ await connection.Receive(
+ "HTTP/1.1 200 OK",
+ $"Date: {dateHeaderValueManager.GetDateHeaderValues().String}");
+
+ // Make sure consuming a single chunk exceeds the 2 second timeout.
+ var targetBytesPerSecond = chunkSize / 4;
+
+ // expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless
+ // the response header writing logic or response body chunking logic itself changes.
+ await AssertBytesReceivedAtTargetRate(connection.Stream, expectedBytes: 33_553_537, targetBytesPerSecond);
+ await appFuncCompleted.Task.DefaultTimeout();
+
+ connection.ShutdownSend();
+ await connection.WaitForConnectionClose();
+ }
+ await server.StopAsync();
+ }
+
+ mockKestrelTrace.Verify(t => t.ResponseMinimumDataRateNotSatisfied(It.IsAny(), It.IsAny()), Times.Never());
+ mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny()), Times.Once());
+ Assert.False(requestAborted);
+ }
+
+ [Fact]
+ [CollectDump]
+ public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeaders()
+ {
+ var headerSize = 1024 * 1024; // 1 MB for each header value
+ var headerCount = 64; // 64 MB of headers per response
+ var requestCount = 4; // Minimum of 256 MB of total response headers
+ var headerValue = new string('a', headerSize);
+ var headerStringValues = new StringValues(Enumerable.Repeat(headerValue, headerCount).ToArray());
+
+ var requestAborted = false;
+ var mockKestrelTrace = new Mock();
+
+ var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object)
+ {
+ ServerOptions =
+ {
+ Limits =
+ {
+ MinResponseDataRate = new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(2))
+ }
+ }
+ };
+
+ testContext.InitializeHeartbeat();
+ var dateHeaderValueManager = new DateHeaderValueManager();
+ dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue);
+ testContext.DateHeaderValueManager = dateHeaderValueManager;
+
+ var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0));
+
+ async Task App(HttpContext context)
+ {
+ context.RequestAborted.Register(() =>
+ {
+ requestAborted = true;
+ });
+
+ context.Response.Headers[$"X-Custom-Header"] = headerStringValues;
+ context.Response.ContentLength = 0;
+
+ await context.Response.BodyWriter.FlushAsync();
+ }
+
+ using (var server = new TestServer(App, testContext, listenOptions))
+ {
+ using (var connection = server.CreateConnection())
+ {
+ for (var i = 0; i < requestCount - 1; i++)
+ {
+ await connection.Send(
+ "GET / HTTP/1.1",
+ "Host:",
+ "",
+ "");
+ }
+
+ await connection.Send(
+ "GET / HTTP/1.1",
+ "Host:",
+ "",
+ "");
+
+ await connection.Receive(
+ "HTTP/1.1 200 OK",
+ $"Date: {dateHeaderValueManager.GetDateHeaderValues().String}");
+
+ var minResponseSize = headerSize * headerCount;
+ var minTotalOutputSize = requestCount * minResponseSize;
+
+ // Make sure consuming a single set of response headers exceeds the 2 second timeout.
+ var targetBytesPerSecond = minResponseSize / 4;
+
+ // expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless
+ // the response header writing logic itself changes.
+ await AssertBytesReceivedAtTargetRate(connection.Stream, expectedBytes: 268_439_596, targetBytesPerSecond);
+ connection.ShutdownSend();
+ await connection.WaitForConnectionClose();
+ }
+
+ await server.StopAsync();
+ }
+
+ mockKestrelTrace.Verify(t => t.ResponseMinimumDataRateNotSatisfied(It.IsAny(), It.IsAny()), Times.Never());
+ mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny()), Times.Once());
+ Assert.False(requestAborted);
+ }
+
+ [Fact]
+ [Flaky("https://github.com/aspnet/AspNetCore/issues/13219", FlakyOn.AzP.Linux, FlakyOn.Helix.All)]
+ public async Task ClientCanReceiveFullConnectionCloseResponseWithoutErrorAtALowDataRate()
+ {
+ var chunkSize = 64 * 128 * 1024;
+ var chunkCount = 4;
+ var chunkData = new byte[chunkSize];
+
+ var requestAborted = false;
+ var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var mockKestrelTrace = new Mock();
+
+ var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object)
+ {
+ ServerOptions =
+ {
+ Limits =
+ {
+ MinResponseDataRate = new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(2))
+ }
+ }
+ };
+
+ testContext.InitializeHeartbeat();
+ var dateHeaderValueManager = new DateHeaderValueManager();
+ dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue);
+ testContext.DateHeaderValueManager = dateHeaderValueManager;
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0));
@@ -785,11 +956,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
"",
"");
- var minTotalOutputSize = chunkCount * chunkSize;
+ await connection.Receive(
+ "HTTP/1.1 200 OK",
+ "Connection: close",
+ $"Date: {dateHeaderValueManager.GetDateHeaderValues().String}");
// Make sure consuming a single chunk exceeds the 2 second timeout.
var targetBytesPerSecond = chunkSize / 4;
- await AssertStreamCompleted(connection.Stream, minTotalOutputSize, targetBytesPerSecond);
+
+ // expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless
+ // the response header writing logic or response body chunking logic itself changes.
+ await AssertStreamCompletedAtTargetRate(connection.Stream, expectedBytes: 33_553_556, targetBytesPerSecond);
await appFuncCompleted.Task.DefaultTimeout();
}
await server.StopAsync();
@@ -800,87 +977,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
Assert.False(requestAborted);
}
- private bool ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeadersRetryPredicate(Exception e)
- => e is IOException && e.Message.Contains("Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request");
-
- [Fact]
- [Flaky("https://github.com/dotnet/corefx/issues/30691", FlakyOn.AzP.Windows)]
- [CollectDump]
- public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeaders()
- {
- var headerSize = 1024 * 1024; // 1 MB for each header value
- var headerCount = 64; // 64 MB of headers per response
- var requestCount = 4; // Minimum of 256 MB of total response headers
- var headerValue = new string('a', headerSize);
- var headerStringValues = new StringValues(Enumerable.Repeat(headerValue, headerCount).ToArray());
-
- var requestAborted = false;
- var mockKestrelTrace = new Mock();
-
- var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object)
- {
- ServerOptions =
- {
- Limits =
- {
- MinResponseDataRate = new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(2))
- }
- }
- };
-
- testContext.InitializeHeartbeat();
-
- var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0));
-
- async Task App(HttpContext context)
- {
- context.RequestAborted.Register(() =>
- {
- requestAborted = true;
- });
-
- context.Response.Headers[$"X-Custom-Header"] = headerStringValues;
- context.Response.ContentLength = 0;
-
- await context.Response.BodyWriter.FlushAsync();
- }
-
- using (var server = new TestServer(App, testContext, listenOptions))
- {
- using (var connection = server.CreateConnection())
- {
- for (var i = 0; i < requestCount - 1; i++)
- {
- await connection.Send(
- "GET / HTTP/1.1",
- "Host:",
- "",
- "");
- }
-
- // Close the connection with the last request so AssertStreamCompleted actually completes.
- await connection.Send(
- "GET / HTTP/1.1",
- "Host:",
- "Connection: close",
- "",
- "");
-
- var responseSize = headerSize * headerCount;
- var minTotalOutputSize = requestCount * responseSize;
-
- // Make sure consuming a single set of response headers exceeds the 2 second timeout.
- var targetBytesPerSecond = responseSize / 4;
- await AssertStreamCompleted(connection.Stream, minTotalOutputSize, targetBytesPerSecond);
- }
- await server.StopAsync();
- }
-
- mockKestrelTrace.Verify(t => t.ResponseMinimumDataRateNotSatisfied(It.IsAny(), It.IsAny()), Times.Never());
- mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny()), Times.Once());
- Assert.False(requestAborted);
- }
-
private async Task AssertStreamAborted(Stream stream, int totalBytes)
{
var receiveBuffer = new byte[64 * 1024];
@@ -908,7 +1004,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
Assert.True(totalReceived < totalBytes, $"{nameof(AssertStreamAborted)} Stream completed successfully.");
}
- private async Task AssertStreamCompleted(Stream stream, long minimumBytes, int targetBytesPerSecond)
+ private async Task AssertBytesReceivedAtTargetRate(Stream stream, int expectedBytes, int targetBytesPerSecond)
+ {
+ var receiveBuffer = new byte[64 * 1024];
+ var totalReceived = 0;
+ var startTime = DateTimeOffset.UtcNow;
+
+ do
+ {
+ var received = await stream.ReadAsync(receiveBuffer, 0, Math.Min(receiveBuffer.Length, expectedBytes - totalReceived));
+
+ Assert.NotEqual(0, received);
+
+ totalReceived += received;
+
+ var expectedTimeElapsed = TimeSpan.FromSeconds(totalReceived / targetBytesPerSecond);
+ var timeElapsed = DateTimeOffset.UtcNow - startTime;
+ if (timeElapsed < expectedTimeElapsed)
+ {
+ await Task.Delay(expectedTimeElapsed - timeElapsed);
+ }
+ } while (totalReceived < expectedBytes);
+ }
+
+ private async Task AssertStreamCompletedAtTargetRate(Stream stream, long expectedBytes, int targetBytesPerSecond)
{
var receiveBuffer = new byte[64 * 1024];
var received = 0;
@@ -928,7 +1047,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
}
} while (received > 0);
- Assert.True(totalReceived >= minimumBytes, $"{nameof(AssertStreamCompleted)} Stream aborted prematurely.");
+ Assert.Equal(expectedBytes, totalReceived);
}
public static TheoryData NullHeaderData
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
index 4937000db9..00f8f3b829 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
@@ -2044,6 +2044,127 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Contains(CoreStrings.HPackErrorNotEnoughBuffer, message.Exception.Message);
}
+ [Fact]
+ public async Task ResponseTrailers_WithLargeUnflushedData_DataExceedsFlowControlAvailableAndNotSentWithTrailers()
+ {
+ const int windowSize = (int)Http2PeerSettings.DefaultMaxFrameSize;
+ _clientSettings.InitialWindowSize = windowSize;
+
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ };
+ await InitializeConnectionAsync(async context =>
+ {
+ await context.Response.StartAsync();
+
+ // Body exceeds flow control available and requires the client to allow more
+ // data via updating the window
+ context.Response.BodyWriter.GetMemory(windowSize + 1);
+ context.Response.BodyWriter.Advance(windowSize + 1);
+
+ context.Response.AppendTrailer("CustomName", "Custom Value");
+ }).DefaultTimeout();
+
+ await StartStreamAsync(1, headers, endStream: true).DefaultTimeout();
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 37,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1).DefaultTimeout();
+
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 16384,
+ withFlags: (byte)Http2DataFrameFlags.NONE,
+ withStreamId: 1).DefaultTimeout();
+
+ var dataTask = ExpectAsync(Http2FrameType.DATA,
+ withLength: 1,
+ withFlags: (byte)Http2DataFrameFlags.NONE,
+ withStreamId: 1).DefaultTimeout();
+
+ // Reading final frame of data requires window update
+ // Verify this data task is waiting on window update
+ Assert.False(dataTask.IsCompletedSuccessfully);
+
+ await SendWindowUpdateAsync(1, 1);
+
+ await dataTask;
+
+ var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 25,
+ withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+ withStreamId: 1).DefaultTimeout();
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false).DefaultTimeout();
+
+ _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this);
+
+ Assert.Equal(2, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+
+ _decodedHeaders.Clear();
+ _hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this);
+
+ Assert.Single(_decodedHeaders);
+ Assert.Equal("Custom Value", _decodedHeaders["CustomName"]);
+ }
+
+ [Fact]
+ public async Task ResponseTrailers_WithUnflushedData_DataSentWithTrailers()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ };
+ await InitializeConnectionAsync(async context =>
+ {
+ await context.Response.StartAsync();
+
+ var s = context.Response.BodyWriter.GetMemory(1);
+ s.Span[0] = byte.MaxValue;
+ context.Response.BodyWriter.Advance(1);
+
+ context.Response.AppendTrailer("CustomName", "Custom Value");
+ });
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 37,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 1,
+ withFlags: (byte)Http2DataFrameFlags.NONE,
+ withStreamId: 1);
+
+ var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 25,
+ withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this);
+
+ Assert.Equal(2, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+
+ _decodedHeaders.Clear();
+ _hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this);
+
+ Assert.Single(_decodedHeaders);
+ Assert.Equal("Custom Value", _decodedHeaders["CustomName"]);
+ }
+
[Fact]
public async Task ApplicationException_BeforeFirstWrite_Sends500()
{
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
index 137ea743b7..24e4e59228 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
@@ -1112,12 +1112,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var buffer = result.Buffer;
var consumed = buffer.Start;
var examined = buffer.Start;
+ var copyBuffer = buffer;
try
{
Assert.True(buffer.Length > 0);
- if (Http2FrameReader.ReadFrame(buffer, frame, maxFrameSize, out var framePayload))
+ if (Http2FrameReader.TryReadFrame(ref buffer, frame, maxFrameSize, out var framePayload))
{
consumed = examined = framePayload.End;
frame.Payload = framePayload.ToArray();
@@ -1135,7 +1136,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
finally
{
- _bytesReceived += buffer.Slice(buffer.Start, consumed).Length;
+ _bytesReceived += copyBuffer.Slice(copyBuffer.Start, consumed).Length;
_pair.Application.Input.AdvanceTo(consumed, examined);
}
}
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs
index f52e83eb62..1c78af53bf 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs
@@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.Http2
try
{
- if (Http2FrameReader.ReadFrame(buffer, frame, 16_384, out var framePayload))
+ if (Http2FrameReader.TryReadFrame(ref buffer, frame, 16_384, out var framePayload))
{
consumed = examined = framePayload.End;
return frame;
diff --git a/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportFactoryTests.cs b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportFactoryTests.cs
new file mode 100644
index 0000000000..2ff92f497f
--- /dev/null
+++ b/src/Servers/Kestrel/test/Sockets.BindTests/SocketTransportFactoryTests.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Sockets.BindTests
+{
+ public class SocketTransportFactoryTests
+ {
+ [Fact]
+ public async Task ThrowsNotSupportedExceptionWhenBindingToFileHandleEndPoint()
+ {
+ var socketTransportFactory = new SocketTransportFactory(Options.Create(new SocketTransportOptions()), Mock.Of());
+ await Assert.ThrowsAsync(async () => await socketTransportFactory.BindAsync(new FileHandleEndPoint(0, FileHandleType.Auto)));
+ }
+ }
+}
+
diff --git a/src/Servers/test/FunctionalTests/HelloWorldTest.cs b/src/Servers/test/FunctionalTests/HelloWorldTest.cs
index e047288908..cf20b9d617 100644
--- a/src/Servers/test/FunctionalTests/HelloWorldTest.cs
+++ b/src/Servers/test/FunctionalTests/HelloWorldTest.cs
@@ -21,7 +21,7 @@ namespace ServerComparison.FunctionalTests
public static TestMatrix TestVariants
=> TestMatrix.ForServers(ServerType.IISExpress, ServerType.Kestrel, ServerType.Nginx, ServerType.HttpSys)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithApplicationTypes(ApplicationType.Portable)
.WithAllHostingModels()
.WithAllArchitectures();
diff --git a/src/Servers/test/FunctionalTests/NtlmAuthenticationTest.cs b/src/Servers/test/FunctionalTests/NtlmAuthenticationTest.cs
index 330586eafa..75ef46207a 100644
--- a/src/Servers/test/FunctionalTests/NtlmAuthenticationTest.cs
+++ b/src/Servers/test/FunctionalTests/NtlmAuthenticationTest.cs
@@ -22,7 +22,7 @@ namespace ServerComparison.FunctionalTests
public static TestMatrix TestVariants
=> TestMatrix.ForServers(ServerType.IISExpress, ServerType.HttpSys, ServerType.Kestrel)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithAllHostingModels();
[ConditionalTheory]
diff --git a/src/Servers/test/FunctionalTests/ResponseCompressionTests.cs b/src/Servers/test/FunctionalTests/ResponseCompressionTests.cs
index 4f4af0b3e0..e441343521 100644
--- a/src/Servers/test/FunctionalTests/ResponseCompressionTests.cs
+++ b/src/Servers/test/FunctionalTests/ResponseCompressionTests.cs
@@ -33,7 +33,7 @@ namespace ServerComparison.FunctionalTests
public static TestMatrix NoCompressionTestVariants
=> TestMatrix.ForServers(ServerType.IISExpress, ServerType.Kestrel, ServerType.Nginx, ServerType.HttpSys)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithAllHostingModels();
[ConditionalTheory]
@@ -45,7 +45,7 @@ namespace ServerComparison.FunctionalTests
public static TestMatrix HostCompressionTestVariants
=> TestMatrix.ForServers(ServerType.IISExpress, ServerType.Nginx)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithAllHostingModels();
[ConditionalTheory]
@@ -57,7 +57,7 @@ namespace ServerComparison.FunctionalTests
public static TestMatrix AppCompressionTestVariants
=> TestMatrix.ForServers(ServerType.IISExpress, ServerType.Kestrel, ServerType.HttpSys) // No pass-through compression for nginx
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithAllHostingModels();
[ConditionalTheory]
@@ -69,7 +69,7 @@ namespace ServerComparison.FunctionalTests
public static TestMatrix HostAndAppCompressionTestVariants
=> TestMatrix.ForServers(ServerType.IISExpress, ServerType.Kestrel, ServerType.Nginx, ServerType.HttpSys)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithAllHostingModels();
[ConditionalTheory]
diff --git a/src/Servers/test/FunctionalTests/ResponseTests.cs b/src/Servers/test/FunctionalTests/ResponseTests.cs
index 169132f30b..904ec52b7a 100644
--- a/src/Servers/test/FunctionalTests/ResponseTests.cs
+++ b/src/Servers/test/FunctionalTests/ResponseTests.cs
@@ -26,7 +26,7 @@ namespace ServerComparison.FunctionalTests
public static TestMatrix TestVariants
=> TestMatrix.ForServers(/* ServerType.IISExpress, https://github.com/aspnet/AspNetCore/issues/6168, */ ServerType.Kestrel, ServerType.Nginx, ServerType.HttpSys)
- .WithTfms(Tfm.NetCoreApp31)
+ .WithTfms(Tfm.NetCoreApp50)
.WithAllHostingModels();
[ConditionalTheory]
@@ -52,7 +52,7 @@ namespace ServerComparison.FunctionalTests
public static TestMatrix SelfhostTestVariants
=> TestMatrix.ForServers(ServerType.Kestrel, ServerType.HttpSys)
- .WithTfms(Tfm.NetCoreApp31);
+ .WithTfms(Tfm.NetCoreApp50);
// Connection Close tests do not work through reverse proxies
[ConditionalTheory]
diff --git a/src/Shared/E2ETesting/BrowserFixture.cs b/src/Shared/E2ETesting/BrowserFixture.cs
index 0fa652994f..a453a2d7be 100644
--- a/src/Shared/E2ETesting/BrowserFixture.cs
+++ b/src/Shared/E2ETesting/BrowserFixture.cs
@@ -118,7 +118,7 @@ namespace Microsoft.AspNetCore.E2ETesting
// To prevent this we let the client attempt several times to connect to the server, increasing
// the max allowed timeout for a command on each attempt linearly.
// This can also be caused if many tests are running concurrently, we might want to manage
- // chrome and chromedriver instances more aggresively if we have to.
+ // chrome and chromedriver instances more aggressively if we have to.
// Additionally, if we think the selenium server has become irresponsive, we could spin up
// replace the current selenium server instance and let a new instance take over for the
// remaining tests.
diff --git a/src/Shared/E2ETesting/SeleniumStandaloneServer.cs b/src/Shared/E2ETesting/SeleniumStandaloneServer.cs
index d62b3ffb00..91f80afb2b 100644
--- a/src/Shared/E2ETesting/SeleniumStandaloneServer.cs
+++ b/src/Shared/E2ETesting/SeleniumStandaloneServer.cs
@@ -191,7 +191,7 @@ Captured output lines:
private static Process StartSentinelProcess(Process process, string sentinelFile, int timeout)
{
- // This sentinel process will start and will kill any roge selenium server that want' torn down
+ // This sentinel process will start and will kill any rouge selenium server that want' torn down
// via normal means.
var psi = new ProcessStartInfo
{
diff --git a/src/Shared/ErrorPage/GeneratePage.ps1 b/src/Shared/ErrorPage/GeneratePage.ps1
index 8bc0f2c07c..94e6746169 100644
--- a/src/Shared/ErrorPage/GeneratePage.ps1
+++ b/src/Shared/ErrorPage/GeneratePage.ps1
@@ -2,7 +2,7 @@ param(
[Parameter(Mandatory = $true)][string]$ToolingRepoPath
)
-$ToolPath = Join-Path $ToolingRepoPath "artifacts\bin\RazorPageGenerator\Debug\netcoreapp3.1\dotnet-razorpagegenerator.exe"
+$ToolPath = Join-Path $ToolingRepoPath "artifacts\bin\RazorPageGenerator\Debug\netcoreapp5.0\dotnet-razorpagegenerator.exe"
if (!(Test-Path $ToolPath)) {
throw "Unable to find razor page generator tool at $ToolPath"
diff --git a/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs b/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs
index 4483cbc306..4316e020e1 100644
--- a/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs
+++ b/src/Shared/HttpSys/NativeInterop/UnsafeNativeMethods.cs
@@ -24,6 +24,8 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
internal static class ErrorCodes
{
internal const uint ERROR_SUCCESS = 0;
+ internal const uint ERROR_ACCESS_DENIED = 5;
+ internal const uint ERROR_SHARING_VIOLATION = 32;
internal const uint ERROR_HANDLE_EOF = 38;
internal const uint ERROR_NOT_SUPPORTED = 50;
internal const uint ERROR_INVALID_PARAMETER = 87;
diff --git a/src/SignalR/README.md b/src/SignalR/README.md
index 084b5fbf71..80a69f98e7 100644
--- a/src/SignalR/README.md
+++ b/src/SignalR/README.md
@@ -7,7 +7,7 @@ You can watch an introductory presentation here - [ASP.NET Core SignalR: Build 2
## Documentation
-Documentation for ASP.NET Core SignalR can be found in the [Real-time Apps](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction?view=aspnetcore-2.1) section of the ASP.NET Core Documentation site.
+Documentation for ASP.NET Core SignalR can be found in the [Real-time Apps](https://docs.microsoft.com/aspnet/core/signalr/introduction) section of the ASP.NET Core Documentation site.
## TypeScript Version
diff --git a/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.0.cs b/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.0.cs
index a361a4ed03..0635cd850a 100644
--- a/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.0.cs
+++ b/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.0.cs
@@ -3,7 +3,7 @@
namespace Microsoft.AspNetCore.SignalR.Client
{
- public partial class HubConnection
+ public partial class HubConnection : System.IAsyncDisposable
{
public static readonly System.TimeSpan DefaultHandshakeTimeout;
public static readonly System.TimeSpan DefaultKeepAliveInterval;
@@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
public event System.Func Reconnected { add { } remove { } }
public event System.Func Reconnecting { add { } remove { } }
[System.Diagnostics.DebuggerStepThroughAttribute]
- public System.Threading.Tasks.Task DisposeAsync() { throw null; }
+ public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
public System.Threading.Tasks.Task InvokeCoreAsync(string methodName, System.Type returnType, object[] args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public System.IDisposable On(string methodName, System.Type[] parameterTypes, System.Func handler, object state) { throw null; }
diff --git a/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.1.cs b/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.1.cs
index a361a4ed03..0635cd850a 100644
--- a/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.1.cs
+++ b/src/SignalR/clients/csharp/Client.Core/ref/Microsoft.AspNetCore.SignalR.Client.Core.netstandard2.1.cs
@@ -3,7 +3,7 @@
namespace Microsoft.AspNetCore.SignalR.Client
{
- public partial class HubConnection
+ public partial class HubConnection : System.IAsyncDisposable
{
public static readonly System.TimeSpan DefaultHandshakeTimeout;
public static readonly System.TimeSpan DefaultKeepAliveInterval;
@@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
public event System.Func Reconnected { add { } remove { } }
public event System.Func Reconnecting { add { } remove { } }
[System.Diagnostics.DebuggerStepThroughAttribute]
- public System.Threading.Tasks.Task DisposeAsync() { throw null; }
+ public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
public System.Threading.Tasks.Task InvokeCoreAsync(string methodName, System.Type returnType, object[] args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public System.IDisposable On(string methodName, System.Type[] parameterTypes, System.Func handler, object state) { throw null; }
diff --git a/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs b/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs
index 5e04bd309a..576e69326f 100644
--- a/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs
+++ b/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs
@@ -22,7 +22,6 @@ using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
-using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.SignalR.Client
{
@@ -34,7 +33,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
/// Before hub methods can be invoked the connection must be started using .
/// Clean up a connection using or .
///
- public partial class HubConnection
+ public partial class HubConnection : IAsyncDisposable
{
public static readonly TimeSpan DefaultServerTimeout = TimeSpan.FromSeconds(30); // Server ping rate is 15 sec, this is 2 times that.
public static readonly TimeSpan DefaultHandshakeTimeout = TimeSpan.FromSeconds(15);
@@ -292,8 +291,8 @@ namespace Microsoft.AspNetCore.SignalR.Client
///
/// Disposes the .
///
- /// A that represents the asynchronous dispose.
- public async Task DisposeAsync()
+ /// A that represents the asynchronous dispose.
+ public async ValueTask DisposeAsync()
{
if (!_disposed)
{
@@ -504,8 +503,16 @@ namespace Microsoft.AspNetCore.SignalR.Client
if (disposing)
{
- (_serviceProvider as IDisposable)?.Dispose();
+ // Must set this before calling DisposeAsync because the service provider has a reference to the HubConnection and will try to dispose it again
_disposed = true;
+ if (_serviceProvider is IAsyncDisposable asyncDispose)
+ {
+ await asyncDispose.DisposeAsync();
+ }
+ else
+ {
+ (_serviceProvider as IDisposable)?.Dispose();
+ }
}
}
finally
@@ -532,7 +539,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
///
public IAsyncEnumerable StreamAsyncCore(string methodName, object[] args, CancellationToken cancellationToken = default)
{
- var cts = cancellationToken.CanBeCanceled ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) : new CancellationTokenSource();
+ var cts = cancellationToken.CanBeCanceled ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, default) : new CancellationTokenSource();
var stream = CastIAsyncEnumerable(methodName, args, cts);
var cancelableStream = AsyncEnumerableAdapters.MakeCancelableTypedAsyncEnumerable(stream, cts);
return cancelableStream;
diff --git a/src/SignalR/clients/csharp/Client.Core/src/Microsoft.AspNetCore.SignalR.Client.Core.csproj b/src/SignalR/clients/csharp/Client.Core/src/Microsoft.AspNetCore.SignalR.Client.Core.csproj
index df54c48fec..e287d4c869 100644
--- a/src/SignalR/clients/csharp/Client.Core/src/Microsoft.AspNetCore.SignalR.Client.Core.csproj
+++ b/src/SignalR/clients/csharp/Client.Core/src/Microsoft.AspNetCore.SignalR.Client.Core.csproj
@@ -32,7 +32,7 @@
-
+
diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs
index 14e9d78e5b..5d40dcaf70 100644
--- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs
+++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs
@@ -114,6 +114,71 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
}
}
+ [Fact]
+ public async Task ServerRejectsClientWithOldProtocol()
+ {
+ bool ExpectedError(WriteContext writeContext)
+ {
+ return writeContext.LoggerName == typeof(HttpConnection).FullName &&
+ writeContext.EventId.Name == "ErrorWithNegotiation";
+ }
+
+ var protocol = HubProtocols["json"];
+ using (StartServer(out var server, ExpectedError))
+ {
+ var connectionBuilder = new HubConnectionBuilder()
+ .WithLoggerFactory(LoggerFactory)
+ .WithUrl(server.Url + "/negotiateProtocolVersion12", HttpTransportType.LongPolling);
+ connectionBuilder.Services.AddSingleton(protocol);
+
+ var connection = connectionBuilder.Build();
+
+ try
+ {
+ var ex = await Assert.ThrowsAnyAsync(() => connection.StartAsync()).OrTimeout();
+ Assert.Equal("The client requested version '1', but the server does not support this version.", ex.Message);
+ }
+ catch (Exception ex)
+ {
+ LoggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
+ throw;
+ }
+ finally
+ {
+ await connection.DisposeAsync().OrTimeout();
+ }
+ }
+ }
+
+ [Fact]
+ public async Task ClientCanConnectToServerWithLowerMinimumProtocol()
+ {
+ var protocol = HubProtocols["json"];
+ using (StartServer(out var server))
+ {
+ var connectionBuilder = new HubConnectionBuilder()
+ .WithLoggerFactory(LoggerFactory)
+ .WithUrl(server.Url + "/negotiateProtocolVersionNegative", HttpTransportType.LongPolling);
+ connectionBuilder.Services.AddSingleton(protocol);
+
+ var connection = connectionBuilder.Build();
+
+ try
+ {
+ await connection.StartAsync().OrTimeout();
+ }
+ catch (Exception ex)
+ {
+ LoggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
+ throw;
+ }
+ finally
+ {
+ await connection.DisposeAsync().OrTimeout();
+ }
+ }
+ }
+
[Theory]
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
public async Task CanSendAndReceiveMessage(string protocolName, HttpTransportType transportType, string path)
@@ -1414,6 +1479,118 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
}
}
+ [Fact]
+ public async Task UserAgentIsSet()
+ {
+ using (StartServer(out var server))
+ {
+ var hubConnection = new HubConnectionBuilder()
+ .WithLoggerFactory(LoggerFactory)
+ .WithUrl(server.Url + "/default", HttpTransportType.LongPolling, options =>
+ {
+ options.Headers["X-test"] = "42";
+ options.Headers["X-42"] = "test";
+ })
+ .Build();
+ try
+ {
+ await hubConnection.StartAsync().OrTimeout();
+ var headerValues = await hubConnection.InvokeAsync(nameof(TestHub.GetHeaderValues), new[] { "User-Agent" }).OrTimeout();
+ Assert.NotNull(headerValues);
+ Assert.Single(headerValues);
+
+ var userAgent = headerValues[0];
+
+ Assert.StartsWith("Microsoft SignalR/", userAgent);
+
+ var majorVersion = typeof(HttpConnection).Assembly.GetName().Version.Major;
+ var minorVersion = typeof(HttpConnection).Assembly.GetName().Version.Minor;
+
+ Assert.Contains($"{majorVersion}.{minorVersion}", userAgent);
+
+ }
+ catch (Exception ex)
+ {
+ LoggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
+ throw;
+ }
+ finally
+ {
+ await hubConnection.DisposeAsync().OrTimeout();
+ }
+ }
+ }
+
+ [Fact]
+ public async Task UserAgentCanBeCleared()
+ {
+ using (StartServer(out var server))
+ {
+ var hubConnection = new HubConnectionBuilder()
+ .WithLoggerFactory(LoggerFactory)
+ .WithUrl(server.Url + "/default", HttpTransportType.LongPolling, options =>
+ {
+ options.Headers["User-Agent"] = "";
+ })
+ .Build();
+ try
+ {
+ await hubConnection.StartAsync().OrTimeout();
+ var headerValues = await hubConnection.InvokeAsync(nameof(TestHub.GetHeaderValues), new[] { "User-Agent" }).OrTimeout();
+ Assert.NotNull(headerValues);
+ Assert.Single(headerValues);
+
+ var userAgent = headerValues[0];
+
+ Assert.Null(userAgent);
+ }
+ catch (Exception ex)
+ {
+ LoggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
+ throw;
+ }
+ finally
+ {
+ await hubConnection.DisposeAsync().OrTimeout();
+ }
+ }
+ }
+
+ [Fact]
+ public async Task UserAgentCanBeSet()
+ {
+ using (StartServer(out var server))
+ {
+ var hubConnection = new HubConnectionBuilder()
+ .WithLoggerFactory(LoggerFactory)
+ .WithUrl(server.Url + "/default", HttpTransportType.LongPolling, options =>
+ {
+ options.Headers["User-Agent"] = "User Value";
+ })
+ .Build();
+ try
+ {
+ await hubConnection.StartAsync().OrTimeout();
+ var headerValues = await hubConnection.InvokeAsync(nameof(TestHub.GetHeaderValues), new[] { "User-Agent" }).OrTimeout();
+ Assert.NotNull(headerValues);
+ Assert.Single(headerValues);
+
+ var userAgent = headerValues[0];
+
+ Assert.Equal("User Value", userAgent);
+ }
+ catch (Exception ex)
+ {
+ LoggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
+ throw;
+ }
+ finally
+ {
+ await hubConnection.DisposeAsync().OrTimeout();
+ }
+ }
+ }
+
[ConditionalFact]
[WebSocketsSupportedCondition]
public async Task WebSocketOptionsAreApplied()
diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs
index 1d7dbd6718..4cbc35c510 100644
--- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs
+++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs
@@ -69,6 +69,16 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
endpoints.MapHub("/default-nowebsockets", options => options.Transports = HttpTransportType.LongPolling | HttpTransportType.ServerSentEvents);
+ endpoints.MapHub("/negotiateProtocolVersion12", options =>
+ {
+ options.MinimumProtocolVersion = 12;
+ });
+
+ endpoints.MapHub("/negotiateProtocolVersionNegative", options =>
+ {
+ options.MinimumProtocolVersion = -1;
+ });
+
endpoints.MapGet("/generateJwtToken", context =>
{
return context.Response.WriteAsync(GenerateJwtToken());
diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs
index fa95fbc83b..e04e82a2b5 100644
--- a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs
+++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs
@@ -359,7 +359,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var httpHandler = new TestHttpMessageHandler();
var connectResponseTcs = new TaskCompletionSource();
- httpHandler.OnGet("/?id=00000000-0000-0000-0000-000000000000", async (_, __) =>
+ httpHandler.OnGet("/?negotiateVersion=1&id=00000000-0000-0000-0000-000000000000", async (_, __) =>
{
await connectResponseTcs.Task;
return ResponseUtils.CreateResponse(HttpStatusCode.Accepted);
diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Negotiate.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Negotiate.cs
index 348e33cebf..5abbde0313 100644
--- a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Negotiate.cs
+++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Negotiate.cs
@@ -36,6 +36,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
return RunInvalidNegotiateResponseTest(ResponseUtils.CreateNegotiationContent(connectionId: string.Empty), "Invalid connection id.");
}
+ [Fact]
+ public Task NegotiateResponseWithNegotiateVersionRequiresConnectionToken()
+ {
+ return RunInvalidNegotiateResponseTest(ResponseUtils.CreateNegotiationContent(negotiateVersion: 1, connectionToken: null), "Invalid negotiation response received.");
+ }
+
[Fact]
public Task ConnectionCannotBeStartedIfNoCommonTransportsBetweenClientAndServer()
{
@@ -50,12 +56,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
}
[Theory]
- [InlineData("http://fakeuri.org/", "http://fakeuri.org/negotiate")]
- [InlineData("http://fakeuri.org/?q=1/0", "http://fakeuri.org/negotiate?q=1/0")]
- [InlineData("http://fakeuri.org?q=1/0", "http://fakeuri.org/negotiate?q=1/0")]
- [InlineData("http://fakeuri.org/endpoint", "http://fakeuri.org/endpoint/negotiate")]
- [InlineData("http://fakeuri.org/endpoint/", "http://fakeuri.org/endpoint/negotiate")]
- [InlineData("http://fakeuri.org/endpoint?q=1/0", "http://fakeuri.org/endpoint/negotiate?q=1/0")]
+ [InlineData("http://fakeuri.org/", "http://fakeuri.org/negotiate?negotiateVersion=1")]
+ [InlineData("http://fakeuri.org/?q=1/0", "http://fakeuri.org/negotiate?q=1/0&negotiateVersion=1")]
+ [InlineData("http://fakeuri.org?q=1/0", "http://fakeuri.org/negotiate?q=1/0&negotiateVersion=1")]
+ [InlineData("http://fakeuri.org/endpoint", "http://fakeuri.org/endpoint/negotiate?negotiateVersion=1")]
+ [InlineData("http://fakeuri.org/endpoint/", "http://fakeuri.org/endpoint/negotiate?negotiateVersion=1")]
+ [InlineData("http://fakeuri.org/endpoint?q=1/0", "http://fakeuri.org/endpoint/negotiate?q=1/0&negotiateVersion=1")]
public async Task CorrectlyHandlesQueryStringWhenAppendingNegotiateToUrl(string requestedUrl, string expectedNegotiate)
{
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
@@ -119,6 +125,124 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
}
+ [Fact]
+ public async Task NegotiateCanHaveNewFields()
+ {
+ string connectionId = null;
+
+ var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
+ testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
+ JsonConvert.SerializeObject(new
+ {
+ connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
+ availableTransports = new object[]
+ {
+ new
+ {
+ transport = "LongPolling",
+ transferFormats = new[] { "Text" }
+ },
+ },
+ newField = "ignore this",
+ })));
+ testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
+ testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
+
+ using (var noErrorScope = new VerifyNoErrorsScope())
+ {
+ await WithConnectionAsync(
+ CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
+ async (connection) =>
+ {
+ await connection.StartAsync().OrTimeout();
+ connectionId = connection.ConnectionId;
+ });
+ }
+
+ Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
+ }
+
+ [Fact]
+ public async Task ConnectionIdGetsSetWithNegotiateProtocolGreaterThanZero()
+ {
+ string connectionId = null;
+
+ var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
+ testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
+ JsonConvert.SerializeObject(new
+ {
+ connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
+ negotiateVersion = 1,
+ connectionToken = "different-id",
+ availableTransports = new object[]
+ {
+ new
+ {
+ transport = "LongPolling",
+ transferFormats = new[] { "Text" }
+ },
+ },
+ newField = "ignore this",
+ })));
+ testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
+ testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
+
+ using (var noErrorScope = new VerifyNoErrorsScope())
+ {
+ await WithConnectionAsync(
+ CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
+ async (connection) =>
+ {
+ await connection.StartAsync().OrTimeout();
+ connectionId = connection.ConnectionId;
+ });
+ }
+
+ Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
+ Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
+ Assert.Equal("http://fakeuri.org/?negotiateVersion=1&id=different-id", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
+ }
+
+ [Fact]
+ public async Task ConnectionTokenFieldIsIgnoredForNegotiateIdLessThanOne()
+ {
+ string connectionId = null;
+
+ var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
+ testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
+ JsonConvert.SerializeObject(new
+ {
+ connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
+ connectionToken = "different-id",
+ availableTransports = new object[]
+ {
+ new
+ {
+ transport = "LongPolling",
+ transferFormats = new[] { "Text" }
+ },
+ },
+ newField = "ignore this",
+ })));
+ testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
+ testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
+
+ using (var noErrorScope = new VerifyNoErrorsScope())
+ {
+ await WithConnectionAsync(
+ CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
+ async (connection) =>
+ {
+ await connection.StartAsync().OrTimeout();
+ connectionId = connection.ConnectionId;
+ });
+ }
+
+ Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
+ Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
+ Assert.Equal("http://fakeuri.org/?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
+ }
+
[Fact]
public async Task NegotiateThatReturnsUrlGetFollowed()
{
@@ -172,10 +296,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
});
}
- Assert.Equal("http://fakeuri.org/negotiate", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
- Assert.Equal("https://another.domain.url/chat/negotiate", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
- Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
- Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
+ Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
+ Assert.Equal("https://another.domain.url/chat/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
+ Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
+ Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
}
@@ -278,10 +402,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
});
}
- Assert.Equal("http://fakeuri.org/negotiate", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
- Assert.Equal("https://another.domain.url/chat/negotiate", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
- Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
- Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
+ Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
+ Assert.Equal("https://another.domain.url/chat/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
+ Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
+ Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
// Delete request
Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
}
diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Transport.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Transport.cs
index 142e40546c..0244af0afd 100644
--- a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Transport.cs
+++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Transport.cs
@@ -3,6 +3,7 @@
using System;
using System.IO.Pipelines;
+using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
@@ -113,16 +114,17 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
testHttpHandler.OnRequest(async (request, next, token) =>
{
- var userAgentHeaderCollection = request.Headers.UserAgent;
- var userAgentHeader = Assert.Single(userAgentHeaderCollection);
- Assert.Equal("Microsoft.AspNetCore.Http.Connections.Client", userAgentHeader.Product.Name);
+ var userAgentHeader = request.Headers.UserAgent.ToString();
+
+ Assert.NotNull(userAgentHeader);
+ Assert.StartsWith("Microsoft SignalR/", userAgentHeader);
// user agent version should come from version embedded in assembly metadata
var assemblyVersion = typeof(Constants)
.Assembly
.GetCustomAttribute();
- Assert.Equal(assemblyVersion.InformationalVersion, userAgentHeader.Product.Version);
+ Assert.Contains(assemblyVersion.InformationalVersion, userAgentHeader);
requestsExecuted = true;
diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs
index 2c9df93cb8..b303e71ddd 100644
--- a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs
+++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs
@@ -414,6 +414,17 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
}
}
+ [Fact]
+ public async Task CanAwaitUsingHubConnection()
+ {
+ using (StartVerifiableLog())
+ {
+ var connection = new TestConnection();
+ await using var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory);
+ await hubConnection.StartAsync().OrTimeout();
+ }
+ }
+
private class SampleObject
{
public SampleObject(string foo, int bar)
diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/ResponseUtils.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/ResponseUtils.cs
index 58d5fbb53c..33dddf6aab 100644
--- a/src/SignalR/clients/csharp/Client/test/UnitTests/ResponseUtils.cs
+++ b/src/SignalR/clients/csharp/Client/test/UnitTests/ResponseUtils.cs
@@ -62,7 +62,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
}
public static string CreateNegotiationContent(string connectionId = "00000000-0000-0000-0000-000000000000",
- HttpTransportType? transportTypes = null)
+ HttpTransportType? transportTypes = null, string connectionToken = "connection-token", int negotiateVersion = 0)
{
var availableTransports = new List();
@@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
});
}
- return JsonConvert.SerializeObject(new { connectionId, availableTransports });
+ return JsonConvert.SerializeObject(new { connectionId, availableTransports, connectionToken, negotiateVersion });
}
}
}
diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/TestHttpMessageHandler.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/TestHttpMessageHandler.cs
index 06d05da7f5..36596d3236 100644
--- a/src/SignalR/clients/csharp/Client/test/UnitTests/TestHttpMessageHandler.cs
+++ b/src/SignalR/clients/csharp/Client/test/UnitTests/TestHttpMessageHandler.cs
@@ -1,3 +1,6 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
using System;
using System.Collections.Generic;
using System.Net;
@@ -117,7 +120,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
});
testHttpMessageHandler.OnRequest((request, next, cancellationToken) =>
{
- if (request.Method.Equals(HttpMethod.Delete) && request.RequestUri.PathAndQuery.StartsWith("/?id="))
+ if (request.Method.Equals(HttpMethod.Delete) && request.RequestUri.PathAndQuery.Contains("&id="))
{
deleteCts.Cancel();
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netcoreapp.cs b/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netcoreapp.cs
new file mode 100644
index 0000000000..35b6c7cc35
--- /dev/null
+++ b/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netcoreapp.cs
@@ -0,0 +1,49 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Http.Connections.Client
+{
+ public partial class HttpConnection : Microsoft.AspNetCore.Connections.ConnectionContext, Microsoft.AspNetCore.Connections.Features.IConnectionInherentKeepAliveFeature
+ {
+ public HttpConnection(Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions httpConnectionOptions, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
+ public HttpConnection(System.Uri url) { }
+ public HttpConnection(System.Uri url, Microsoft.AspNetCore.Http.Connections.HttpTransportType transports) { }
+ public HttpConnection(System.Uri url, Microsoft.AspNetCore.Http.Connections.HttpTransportType transports, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
+ public override string ConnectionId { get { throw null; } set { } }
+ public override Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
+ public override System.Collections.Generic.IDictionary Items { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ bool Microsoft.AspNetCore.Connections.Features.IConnectionInherentKeepAliveFeature.HasInherentKeepAlive { get { throw null; } }
+ public override System.IO.Pipelines.IDuplexPipe Transport { get { throw null; } set { } }
+ [System.Diagnostics.DebuggerStepThroughAttribute]
+ public override System.Threading.Tasks.ValueTask DisposeAsync() { throw null; }
+ [System.Diagnostics.DebuggerStepThroughAttribute]
+ public System.Threading.Tasks.Task StartAsync(Microsoft.AspNetCore.Connections.TransferFormat transferFormat, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
+ public System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
+ }
+ public partial class HttpConnectionOptions
+ {
+ public HttpConnectionOptions() { }
+ public System.Func> AccessTokenProvider { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public System.Security.Cryptography.X509Certificates.X509CertificateCollection ClientCertificates { get { throw null; } set { } }
+ public System.TimeSpan CloseTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public System.Net.CookieContainer Cookies { get { throw null; } set { } }
+ public System.Net.ICredentials Credentials { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public System.Collections.Generic.IDictionary Headers { get { throw null; } set { } }
+ public System.Func HttpMessageHandlerFactory { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public System.Net.IWebProxy Proxy { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public bool SkipNegotiation { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public Microsoft.AspNetCore.Http.Connections.HttpTransportType Transports { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public System.Uri Url { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public bool? UseDefaultCredentials { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public System.Action WebSocketConfiguration { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ }
+ public partial class NoTransportSupportedException : System.Exception
+ {
+ public NoTransportSupportedException(string message) { }
+ }
+ public partial class TransportFailedException : System.Exception
+ {
+ public TransportFailedException(string transportType, string message, System.Exception innerException = null) { }
+ public string TransportType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
+ }
+}
diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs
index 852681d963..74715c638c 100644
--- a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs
+++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs
@@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
// Not configurable on purpose, high enough that if we reach here, it's likely
// a buggy server
private static readonly int _maxRedirects = 100;
+ private static readonly int _protocolVersionNumber = 1;
private static readonly Task _noAccessToken = Task.FromResult(null);
private static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(120);
@@ -41,6 +42,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
private readonly HttpConnectionOptions _httpConnectionOptions;
private ITransport _transport;
private readonly ITransportFactory _transportFactory;
+ private string _connectionToken;
private string _connectionId;
private readonly ConnectionLogScope _logScope;
private readonly ILoggerFactory _loggerFactory;
@@ -341,7 +343,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
}
// This should only need to happen once
- var connectUrl = CreateConnectUrl(uri, negotiationResponse.ConnectionId);
+ var connectUrl = CreateConnectUrl(uri, _connectionToken);
// We're going to search for the transfer format as a string because we don't want to parse
// all the transfer formats in the negotiation response, and we want to allow transfer formats
@@ -382,7 +384,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
if (negotiationResponse == null)
{
negotiationResponse = await GetNegotiationResponseAsync(uri, cancellationToken);
- connectUrl = CreateConnectUrl(uri, negotiationResponse.ConnectionId);
+ connectUrl = CreateConnectUrl(uri, _connectionToken);
}
Log.StartingTransport(_logger, transportType, connectUrl);
@@ -428,8 +430,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
urlBuilder.Path += "/";
}
urlBuilder.Path += "negotiate";
+ var uri = Utils.AppendQueryString(urlBuilder.Uri, $"negotiateVersion={_protocolVersionNumber}");
- using (var request = new HttpRequestMessage(HttpMethod.Post, urlBuilder.Uri))
+ using (var request = new HttpRequestMessage(HttpMethod.Post, uri))
{
// Corefx changed the default version and High Sierra curlhandler tries to upgrade request
request.Version = new Version(1, 1);
@@ -466,7 +469,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
throw new FormatException("Invalid connection id.");
}
- return Utils.AppendQueryString(url, "id=" + connectionId);
+ return Utils.AppendQueryString(url, $"negotiateVersion={_protocolVersionNumber}&id=" + connectionId);
}
private async Task StartTransport(Uri connectUrl, HttpTransportType transportType, TransferFormat transferFormat, CancellationToken cancellationToken)
@@ -551,14 +554,34 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
httpClient.Timeout = HttpClientTimeout;
// Start with the user agent header
- httpClient.DefaultRequestHeaders.UserAgent.Add(Constants.UserAgentHeader);
+ httpClient.DefaultRequestHeaders.Add(Constants.UserAgent, Constants.UserAgentHeader);
// Apply any headers configured on the HttpConnectionOptions
if (_httpConnectionOptions?.Headers != null)
{
foreach (var header in _httpConnectionOptions.Headers)
{
- httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
+ // Check if the key is User-Agent and remove if empty string then replace if it exists.
+ if (string.Equals(header.Key, Constants.UserAgent, StringComparison.OrdinalIgnoreCase))
+ {
+ if (string.IsNullOrEmpty(header.Value))
+ {
+ httpClient.DefaultRequestHeaders.Remove(header.Key);
+ }
+ else if (httpClient.DefaultRequestHeaders.Contains(header.Key))
+ {
+ httpClient.DefaultRequestHeaders.Remove(header.Key);
+ httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
+ }
+ else
+ {
+ httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
+ }
+ }
+ else
+ {
+ httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
+ }
}
}
@@ -607,7 +630,19 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
private async Task GetNegotiationResponseAsync(Uri uri, CancellationToken cancellationToken)
{
var negotiationResponse = await NegotiateAsync(uri, _httpClient, _logger, cancellationToken);
- _connectionId = negotiationResponse.ConnectionId;
+ // If the negotiationVersion is greater than zero then we know that the negotiation response contains a
+ // connectionToken that will be required to conenct. Otherwise we just set the connectionId and the
+ // connectionToken on the client to the same value.
+ if (negotiationResponse.Version > 0)
+ {
+ _connectionId = negotiationResponse.ConnectionId;
+ _connectionToken = negotiationResponse.ConnectionToken;
+ }
+ else
+ {
+ _connectionToken = _connectionId = negotiationResponse.ConnectionId;
+ }
+
_logScope.ConnectionId = _connectionId;
return negotiationResponse;
}
diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/Constants.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/Constants.cs
index 22b41d56f3..c99c7db1a0 100644
--- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/Constants.cs
+++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/Constants.cs
@@ -3,19 +3,18 @@
using System.Diagnostics;
using System.Linq;
-using System.Net.Http.Headers;
using System.Reflection;
+using System.Runtime.InteropServices;
namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
{
internal static class Constants
{
- public static readonly ProductInfoHeaderValue UserAgentHeader;
+ public const string UserAgent = "User-Agent";
+ public static readonly string UserAgentHeader;
static Constants()
{
- var userAgent = "Microsoft.AspNetCore.Http.Connections.Client";
-
var assemblyVersion = typeof(Constants)
.Assembly
.GetCustomAttributes()
@@ -23,14 +22,22 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
Debug.Assert(assemblyVersion != null);
+ var majorVersion = typeof(Constants).Assembly.GetName().Version.Major;
+ var minorVersion = typeof(Constants).Assembly.GetName().Version.Minor;
+ var os = RuntimeInformation.OSDescription;
+ var runtime = ".NET";
+ var runtimeVersion = RuntimeInformation.FrameworkDescription;
+
// assembly version attribute should always be present
// but in case it isn't then don't include version in user-agent
if (assemblyVersion != null)
{
- userAgent += "/" + assemblyVersion.InformationalVersion;
+ UserAgentHeader = $"Microsoft SignalR/{majorVersion}.{minorVersion} ({assemblyVersion.InformationalVersion}; {os}; {runtime}; {runtimeVersion})";
+ }
+ else
+ {
+ UserAgentHeader = $"Microsoft SignalR/{majorVersion}.{minorVersion} ({os}; {runtime}; {runtimeVersion})";
}
-
- UserAgentHeader = ProductInfoHeaderValue.Parse(userAgent);
}
}
}
diff --git a/src/SignalR/clients/java/signalr/build.gradle b/src/SignalR/clients/java/signalr/build.gradle
index 61b170a40d..8feab7b9b7 100644
--- a/src/SignalR/clients/java/signalr/build.gradle
+++ b/src/SignalR/clients/java/signalr/build.gradle
@@ -108,3 +108,27 @@ task generatePOM {
}
task createPackage(dependsOn: [jar,sourceJar,javadocJar,generatePOM])
+
+task generateVersionClass {
+ inputs.property "version", project.version
+ outputs.dir "$buildDir/generated"
+ doFirst {
+ def versionFile = file("$buildDir/../src/main/java/com/microsoft/signalr/Version.java")
+ versionFile.parentFile.mkdirs()
+ versionFile.text =
+ """
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+package com.microsoft.signalr;
+
+class Version {
+ public static String getDetailedVersion() {
+ return "$project.version";
+ }
+}
+"""
+ }
+}
+
+compileJava.dependsOn generateVersionClass
diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java
index aa333fb508..57604be1a8 100644
--- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java
+++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java
@@ -57,6 +57,8 @@ public class HubConnection {
private Map streamMap = new ConcurrentHashMap<>();
private TransportEnum transportEnum = TransportEnum.ALL;
private String connectionId;
+ private String connectionToken;
+ private final int negotiateVersion = 1;
private final Logger logger = LoggerFactory.getLogger(HubConnection.class);
/**
@@ -327,6 +329,7 @@ public class HubConnection {
handshakeResponseSubject = CompletableSubject.create();
handshakeReceived = false;
CompletableSubject tokenCompletable = CompletableSubject.create();
+ localHeaders.put(UserAgentHelper.getUserAgentName(), UserAgentHelper.createUserAgentString());
if (headers != null) {
this.localHeaders.putAll(headers);
}
@@ -339,11 +342,12 @@ public class HubConnection {
});
stopError = null;
+ String urlWithQS = Utils.appendQueryString(baseUrl, "negotiateVersion=" + negotiateVersion);
Single negotiate = null;
if (!skipNegotiate) {
- negotiate = tokenCompletable.andThen(Single.defer(() -> startNegotiate(baseUrl, 0)));
+ negotiate = tokenCompletable.andThen(Single.defer(() -> startNegotiate(urlWithQS, 0)));
} else {
- negotiate = tokenCompletable.andThen(Single.defer(() -> Single.just(new NegotiateResponse(baseUrl))));
+ negotiate = tokenCompletable.andThen(Single.defer(() -> Single.just(new NegotiateResponse(urlWithQS))));
}
CompletableSubject start = CompletableSubject.create();
@@ -376,7 +380,6 @@ public class HubConnection {
hubConnectionStateLock.lock();
try {
hubConnectionState = HubConnectionState.CONNECTED;
- this.connectionId = negotiateResponse.getConnectionId();
logger.info("HubConnection started.");
resetServerTimeout();
//Don't send pings if we're using long polling.
@@ -446,19 +449,21 @@ public class HubConnection {
throw new RuntimeException("There were no compatible transports on the server.");
}
- String finalUrl = url;
- if (response.getConnectionId() != null) {
- if (url.contains("?")) {
- finalUrl = url + "&id=" + response.getConnectionId();
- } else {
- finalUrl = url + "?id=" + response.getConnectionId();
- }
+ if (response.getVersion() > 0) {
+ this.connectionId = response.getConnectionId();
+ this.connectionToken = response.getConnectionToken();
+ } else {
+ this.connectionToken = this.connectionId = response.getConnectionId();
}
+
+ String finalUrl = Utils.appendQueryString(url, "id=" + this.connectionToken);
+
response.setFinalUrl(finalUrl);
return Single.just(response);
}
- return startNegotiate(response.getRedirectUrl(), negotiateAttempts + 1);
+ String redirectUrl = Utils.appendQueryString(response.getRedirectUrl(), "negotiateVersion=" + negotiateVersion);
+ return startNegotiate(redirectUrl, negotiateAttempts + 1);
});
}
@@ -520,6 +525,7 @@ public class HubConnection {
handshakeResponseSubject.onComplete();
redirectAccessTokenProvider = null;
connectionId = null;
+ connectionToken = null;
transportEnum = TransportEnum.ALL;
this.localHeaders.clear();
this.streamMap.clear();
diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Negotiate.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Negotiate.java
index d63359b90c..d177d32fb9 100644
--- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Negotiate.java
+++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Negotiate.java
@@ -10,7 +10,7 @@ class Negotiate {
// Check if we have a query string. If we do then we ignore it for now.
int queryStringIndex = url.indexOf('?');
if (queryStringIndex > 0) {
- negotiateUrl = url.substring(0, url.indexOf('?'));
+ negotiateUrl = url.substring(0, queryStringIndex);
} else {
negotiateUrl = url;
}
@@ -24,7 +24,7 @@ class Negotiate {
// Add the query string back if it existed.
if (queryStringIndex > 0) {
- negotiateUrl += url.substring(url.indexOf('?'));
+ negotiateUrl += url.substring(queryStringIndex);
}
return negotiateUrl;
diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/NegotiateResponse.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/NegotiateResponse.java
index f115e9601b..bf09b37578 100644
--- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/NegotiateResponse.java
+++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/NegotiateResponse.java
@@ -11,11 +11,13 @@ import com.google.gson.stream.JsonReader;
class NegotiateResponse {
private String connectionId;
+ private String connectionToken;
private Set availableTransports = new HashSet<>();
private String redirectUrl;
private String accessToken;
private String error;
private String finalUrl;
+ private int version;
public NegotiateResponse(JsonReader reader) {
try {
@@ -30,6 +32,12 @@ class NegotiateResponse {
case "ProtocolVersion":
this.error = "Detected an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details.";
return;
+ case "negotiateVersion":
+ this.version = reader.nextInt();
+ break;
+ case "connectionToken":
+ this.connectionToken = reader.nextString();
+ break;
case "url":
this.redirectUrl = reader.nextString();
break;
@@ -106,6 +114,14 @@ class NegotiateResponse {
return finalUrl;
}
+ public int getVersion() {
+ return version;
+ }
+
+ public String getConnectionToken() {
+ return connectionToken;
+ }
+
public void setFinalUrl(String url) {
this.finalUrl = url;
}
diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/UserAgentHelper.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/UserAgentHelper.java
new file mode 100644
index 0000000000..e54809c700
--- /dev/null
+++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/UserAgentHelper.java
@@ -0,0 +1,54 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+package com.microsoft.signalr;
+
+public class UserAgentHelper {
+
+ private final static String USER_AGENT = "User-Agent";
+
+ public static String getUserAgentName() {
+ return USER_AGENT;
+ }
+
+ public static String createUserAgentString() {
+ StringBuilder agentBuilder = new StringBuilder("Microsoft SignalR/");
+
+ // Parsing version numbers
+ String detailedVersion = Version.getDetailedVersion();
+ agentBuilder.append(getVersion(detailedVersion));
+ agentBuilder.append(" (");
+ agentBuilder.append(detailedVersion);
+ agentBuilder.append("; ");
+
+ // Getting the OS name
+ agentBuilder.append(getOS());
+ agentBuilder.append("; Java; ");
+
+ // Vendor and Version
+ agentBuilder.append(getJavaVersion());
+ agentBuilder.append("; ");
+ agentBuilder.append(getJavaVendor());
+ agentBuilder.append(")");
+
+ return agentBuilder.toString();
+ }
+
+ static String getVersion(String detailedVersion) {
+ // Getting the index of the second . so we can return just the major and minor version.
+ int shortVersionIndex = detailedVersion.indexOf(".", detailedVersion.indexOf(".") + 1);
+ return detailedVersion.substring(0, shortVersionIndex);
+ }
+
+ static String getJavaVendor() {
+ return System.getProperty("java.vendor");
+ }
+
+ static String getJavaVersion() {
+ return System.getProperty("java.version");
+ }
+
+ static String getOS() {
+ return System.getProperty("os.name");
+ }
+}
diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Utils.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Utils.java
new file mode 100644
index 0000000000..d08c6fb914
--- /dev/null
+++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Utils.java
@@ -0,0 +1,14 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+package com.microsoft.signalr;
+
+class Utils {
+ public static String appendQueryString(String original, String queryStringValue) {
+ if (original.contains("?")) {
+ return original + "&" + queryStringValue;
+ } else {
+ return original + "?" + queryStringValue;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Version.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Version.java
new file mode 100644
index 0000000000..abafa2e53b
--- /dev/null
+++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/Version.java
@@ -0,0 +1,10 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+package com.microsoft.signalr;
+
+class Version {
+ public static String getDetailedVersion() {
+ return "99.99.99-dev";
+ }
+}
diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java
index f28adf13e7..67aceec18b 100644
--- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java
+++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java
@@ -1116,7 +1116,7 @@ class HubConnectionTest {
hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
AtomicBoolean done = new AtomicBoolean();
- Single result = hubConnection.invoke(String.class, "fixedMessage", null);
+ Single result = hubConnection.invoke(String.class, "fixedMessage", (Object)null);
result.doOnSuccess(value -> done.set(true)).subscribe();
assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"fixedMessage\",\"arguments\":[null]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]);
assertFalse(done.get());
@@ -1714,12 +1714,12 @@ class HubConnectionTest {
List sentRequests = client.getSentRequests();
assertEquals(1, sentRequests.size());
- assertEquals("http://example.com/negotiate", sentRequests.get(0).getUrl());
+ assertEquals("http://example.com/negotiate?negotiateVersion=1", sentRequests.get(0).getUrl());
}
@Test
public void negotiateThatRedirectsForeverFailsAfter100Tries() {
- TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate",
+ TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1",
(req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://example.com\"}")));
HubConnection hubConnection = HubConnectionBuilder
@@ -1752,7 +1752,7 @@ class HubConnectionTest {
@Test
public void connectionIdIsAvailableAfterStart() {
- TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate",
+ TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1",
(req) -> Single.just(new HttpResponse(200, "",
"{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")));
@@ -1775,9 +1775,62 @@ class HubConnectionTest {
assertNull(hubConnection.getConnectionId());
}
+ @Test
+ public void connectionTokenAppearsInQSConnectionIdIsOnConnectionInstance() {
+ TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1",
+ (req) -> Single.just(new HttpResponse(200, "",
+ "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," +
+ "\"negotiateVersion\": 1," +
+ "\"connectionToken\":\"connection-token-value\"," +
+ "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")));
+
+ MockTransport transport = new MockTransport(true);
+ HubConnection hubConnection = HubConnectionBuilder
+ .create("http://example.com")
+ .withTransportImplementation(transport)
+ .withHttpClient(client)
+ .build();
+
+ assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState());
+ assertNull(hubConnection.getConnectionId());
+ hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
+ assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
+ assertEquals("bVOiRPG8-6YiJ6d7ZcTOVQ", hubConnection.getConnectionId());
+ assertEquals("http://example.com?negotiateVersion=1&id=connection-token-value", transport.getUrl());
+ hubConnection.stop().timeout(1, TimeUnit.SECONDS).blockingAwait();
+ assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState());
+ assertNull(hubConnection.getConnectionId());
+ }
+
+ @Test
+ public void connectionTokenIsIgnoredIfNegotiateVersionIsNotPresentInNegotiateResponse() {
+ TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1",
+ (req) -> Single.just(new HttpResponse(200, "",
+ "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," +
+ "\"connectionToken\":\"connection-token-value\"," +
+ "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")));
+
+ MockTransport transport = new MockTransport(true);
+ HubConnection hubConnection = HubConnectionBuilder
+ .create("http://example.com")
+ .withTransportImplementation(transport)
+ .withHttpClient(client)
+ .build();
+
+ assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState());
+ assertNull(hubConnection.getConnectionId());
+ hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
+ assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
+ assertEquals("bVOiRPG8-6YiJ6d7ZcTOVQ", hubConnection.getConnectionId());
+ assertEquals("http://example.com?negotiateVersion=1&id=bVOiRPG8-6YiJ6d7ZcTOVQ", transport.getUrl());
+ hubConnection.stop().timeout(1, TimeUnit.SECONDS).blockingAwait();
+ assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState());
+ assertNull(hubConnection.getConnectionId());
+ }
+
@Test
public void afterSuccessfulNegotiateConnectsWithWebsocketsTransport() {
- TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate",
+ TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1",
(req) -> Single.just(new HttpResponse(200, "",
"{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")));
@@ -1798,7 +1851,7 @@ class HubConnectionTest {
@Test
public void afterSuccessfulNegotiateConnectsWithLongPollingTransport() {
- TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate",
+ TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1",
(req) -> Single.just(new HttpResponse(200, "",
"{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"LongPolling\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")));
@@ -1891,7 +1944,7 @@ class HubConnectionTest {
@Test
public void receivingServerSentEventsTransportFromNegotiateFails() {
- TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate",
+ TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1",
(req) -> Single.just(new HttpResponse(200, "",
"{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"ServerSentEvents\",\"transferFormats\":[\"Text\"]}]}")));
@@ -1911,7 +1964,7 @@ class HubConnectionTest {
@Test
public void negotiateThatReturnsErrorThrowsFromStart() {
- TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate",
+ TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1",
(req) -> Single.just(new HttpResponse(200, "", "{\"error\":\"Test error.\"}")));
MockTransport transport = new MockTransport(true);
@@ -1928,7 +1981,7 @@ class HubConnectionTest {
@Test
public void DetectWhenTryingToConnectToClassicSignalRServer() {
- TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate",
+ TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1",
(req) -> Single.just(new HttpResponse(200, "", "{\"Url\":\"/signalr\"," +
"\"ConnectionToken\":\"X97dw3uxW4NPPggQsYVcNcyQcuz4w2\"," +
"\"ConnectionId\":\"05265228-1e2c-46c5-82a1-6a5bcc3f0143\"," +
@@ -1954,9 +2007,9 @@ class HubConnectionTest {
@Test
public void negotiateRedirectIsFollowed() {
- TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate",
+ TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate?negotiateVersion=1",
(req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\"}")))
- .on("POST", "http://testexample.com/negotiate",
+ .on("POST", "http://testexample.com/negotiate?negotiateVersion=1",
(req) -> Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")));
@@ -1978,11 +2031,11 @@ class HubConnectionTest {
AtomicReference beforeRedirectToken = new AtomicReference<>();
TestHttpClient client = new TestHttpClient()
- .on("POST", "http://example.com/negotiate", (req) -> {
+ .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> {
beforeRedirectToken.set(req.getHeaders().get("Authorization"));
return Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\",\"accessToken\":\"newToken\"}"));
})
- .on("POST", "http://testexample.com/negotiate", (req) -> {
+ .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", (req) -> {
token.set(req.getHeaders().get("Authorization"));
return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"));
@@ -2018,7 +2071,7 @@ class HubConnectionTest {
public void accessTokenProviderIsUsedForNegotiate() {
AtomicReference token = new AtomicReference<>();
TestHttpClient client = new TestHttpClient()
- .on("POST", "http://example.com/negotiate",
+ .on("POST", "http://example.com/negotiate?negotiateVersion=1",
(req) -> {
token.set(req.getHeaders().get("Authorization"));
return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
@@ -2043,11 +2096,13 @@ class HubConnectionTest {
public void accessTokenProviderIsOverriddenFromRedirectNegotiate() {
AtomicReference token = new AtomicReference<>();
TestHttpClient client = new TestHttpClient()
- .on("POST", "http://example.com/negotiate", (req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\",\"accessToken\":\"newToken\"}")))
- .on("POST", "http://testexample.com/negotiate", (req) -> {
+ .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\",\"accessToken\":\"newToken\"}")))
+ .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", (req) -> {
token.set(req.getHeaders().get("Authorization"));
- return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
- + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"));
+ return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\","
+ + "\"connectionToken\":\"connection-token-value\","
+ + "\"negotiateVersion\":1,"
+ + "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"));
});
MockTransport transport = new MockTransport(true);
@@ -2060,7 +2115,7 @@ class HubConnectionTest {
hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
- assertEquals("http://testexample.com/?id=bVOiRPG8-6YiJ6d7ZcTOVQ", transport.getUrl());
+ assertEquals("http://testexample.com/?negotiateVersion=1&id=connection-token-value", transport.getUrl());
hubConnection.stop();
assertEquals("Bearer newToken", token.get());
}
@@ -2071,14 +2126,14 @@ class HubConnectionTest {
AtomicReference beforeRedirectToken = new AtomicReference<>();
TestHttpClient client = new TestHttpClient()
- .on("POST", "http://example.com/negotiate", (req) -> {
+ .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> {
beforeRedirectToken.set(req.getHeaders().get("Authorization"));
return Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\",\"accessToken\":\"newToken\"}"));
})
- .on("POST", "http://testexample.com/negotiate", (req) -> {
+ .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", (req) -> {
token.set(req.getHeaders().get("Authorization"));
- return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
- + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"));
+ return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\","
+ + "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"));
});
MockTransport transport = new MockTransport(true);
@@ -2112,7 +2167,7 @@ class HubConnectionTest {
AtomicInteger redirectCount = new AtomicInteger();
TestHttpClient client = new TestHttpClient()
- .on("POST", "http://example.com/negotiate", (req) -> {
+ .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> {
if (redirectCount.get() == 0) {
redirectCount.incrementAndGet();
redirectToken.set(req.getHeaders().get("Authorization"));
@@ -2122,7 +2177,7 @@ class HubConnectionTest {
return Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\",\"accessToken\":\"secondRedirectToken\"}"));
}
})
- .on("POST", "http://testexample.com/negotiate", (req) -> {
+ .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", (req) -> {
token.set(req.getHeaders().get("Authorization"));
return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"));
@@ -2185,11 +2240,82 @@ class HubConnectionTest {
}
}
+ @Test
+ public void userAgentHeaderIsSet() {
+ AtomicReference header = new AtomicReference<>();
+ TestHttpClient client = new TestHttpClient()
+ .on("POST", "http://example.com/negotiate?negotiateVersion=1",
+ (req) -> {
+ header.set(req.getHeaders().get("User-Agent"));
+ return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"));
+ });
+
+ MockTransport transport = new MockTransport();
+ HubConnection hubConnection = HubConnectionBuilder.create("http://example.com")
+ .withTransportImplementation(transport)
+ .withHttpClient(client)
+ .build();
+
+ hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
+ assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
+ hubConnection.stop();
+
+ assertTrue(header.get().startsWith("Microsoft SignalR/"));
+ }
+
+ @Test
+ public void userAgentHeaderCanBeOverwritten() {
+ AtomicReference header = new AtomicReference<>();
+ TestHttpClient client = new TestHttpClient()
+ .on("POST", "http://example.com/negotiate?negotiateVersion=1",
+ (req) -> {
+ header.set(req.getHeaders().get("User-Agent"));
+ return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"));
+ });
+
+ MockTransport transport = new MockTransport();
+ HubConnection hubConnection = HubConnectionBuilder.create("http://example.com")
+ .withTransportImplementation(transport)
+ .withHttpClient(client)
+ .withHeader("User-Agent", "Updated Value")
+ .build();
+
+ hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
+ assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
+ hubConnection.stop();
+ assertEquals("Updated Value", header.get());
+ }
+
+ @Test
+ public void userAgentCanBeCleared() {
+ AtomicReference header = new AtomicReference<>();
+ TestHttpClient client = new TestHttpClient()
+ .on("POST", "http://example.com/negotiate?negotiateVersion=1",
+ (req) -> {
+ header.set(req.getHeaders().get("User-Agent"));
+ return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"));
+ });
+
+ MockTransport transport = new MockTransport();
+ HubConnection hubConnection = HubConnectionBuilder.create("http://example.com")
+ .withTransportImplementation(transport)
+ .withHttpClient(client)
+ .withHeader("User-Agent", "")
+ .build();
+
+ hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
+ assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
+ hubConnection.stop();
+ assertEquals("", header.get());
+ }
@Test
public void headersAreSetAndSentThroughBuilder() {
AtomicReference header = new AtomicReference<>();
TestHttpClient client = new TestHttpClient()
- .on("POST", "http://example.com/negotiate",
+ .on("POST", "http://example.com/negotiate?negotiateVersion=1",
(req) -> {
header.set(req.getHeaders().get("ExampleHeader"));
return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
@@ -2214,7 +2340,7 @@ class HubConnectionTest {
public void headersAreNotClearedWhenConnectionIsRestarted() {
AtomicReference header = new AtomicReference<>();
TestHttpClient client = new TestHttpClient()
- .on("POST", "http://example.com/negotiate",
+ .on("POST", "http://example.com/negotiate?negotiateVersion=1",
(req) -> {
header.set(req.getHeaders().get("Authorization"));
return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
@@ -2244,12 +2370,12 @@ class HubConnectionTest {
AtomicReference afterRedirectHeader = new AtomicReference<>();
TestHttpClient client = new TestHttpClient()
- .on("POST", "http://example.com/negotiate",
+ .on("POST", "http://example.com/negotiate?negotiateVersion=1",
(req) -> {
beforeRedirectHeader.set(req.getHeaders().get("Authorization"));
return Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\",\"accessToken\":\"redirectToken\"}\"}"));
})
- .on("POST", "http://testexample.com/negotiate",
+ .on("POST", "http://testexample.com/negotiate?negotiateVersion=1",
(req) -> {
afterRedirectHeader.set(req.getHeaders().get("Authorization"));
return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
@@ -2287,7 +2413,7 @@ class HubConnectionTest {
public void sameHeaderSetTwiceGetsOverwritten() {
AtomicReference header = new AtomicReference<>();
TestHttpClient client = new TestHttpClient()
- .on("POST", "http://example.com/negotiate",
+ .on("POST", "http://example.com/negotiate?negotiateVersion=1",
(req) -> {
header.set(req.getHeaders().get("ExampleHeader"));
return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
@@ -2332,8 +2458,8 @@ class HubConnectionTest {
public void hubConnectionCanBeStartedAfterBeingStoppedAndRedirected() {
MockTransport mockTransport = new MockTransport();
TestHttpClient client = new TestHttpClient()
- .on("POST", "http://example.com/negotiate", (req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\"}")))
- .on("POST", "http://testexample.com/negotiate", (req) -> Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ .on("POST", "http://example.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\"}")))
+ .on("POST", "http://testexample.com/negotiate?negotiateVersion=1", (req) -> Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")));
HubConnection hubConnection = HubConnectionBuilder
@@ -2355,7 +2481,7 @@ class HubConnectionTest {
@Test
public void non200FromNegotiateThrowsError() {
TestHttpClient client = new TestHttpClient()
- .on("POST", "http://example.com/negotiate",
+ .on("POST", "http://example.com/negotiate?negotiateVersion=1",
(req) -> Single.just(new HttpResponse(500, "Internal server error", "")));
MockTransport transport = new MockTransport();
diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/NegotiateResponseTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/NegotiateResponseTest.java
index 88175d0ac9..1eaa0a00df 100644
--- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/NegotiateResponseTest.java
+++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/NegotiateResponseTest.java
@@ -15,8 +15,9 @@ import com.google.gson.stream.JsonReader;
class NegotiateResponseTest {
@Test
public void VerifyNegotiateResponse() {
- String stringNegotiateResponse = "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" +
- "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}," +
+ String stringNegotiateResponse = "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," +
+ "\"negotiateVersion\": 99, \"connectionToken\":\"connection-token-value\"," +
+ "\"availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}," +
"{\"transport\":\"ServerSentEvents\",\"transferFormats\":[\"Text\"]}," +
"{\"transport\":\"LongPolling\",\"transferFormats\":[\"Text\",\"Binary\"]}]}";
NegotiateResponse negotiateResponse = new NegotiateResponse(new JsonReader(new StringReader(stringNegotiateResponse)));
@@ -26,6 +27,8 @@ class NegotiateResponseTest {
assertNull(negotiateResponse.getAccessToken());
assertNull(negotiateResponse.getRedirectUrl());
assertEquals("bVOiRPG8-6YiJ6d7ZcTOVQ", negotiateResponse.getConnectionId());
+ assertEquals("connection-token-value", negotiateResponse.getConnectionToken());
+ assertEquals(99, negotiateResponse.getVersion());
}
@Test
@@ -56,4 +59,23 @@ class NegotiateResponseTest {
NegotiateResponse negotiateResponse = new NegotiateResponse(new JsonReader(new StringReader(stringNegotiateResponse)));
assertEquals("bVOiRPG8-6YiJ6d7ZcTOVQ", negotiateResponse.getConnectionId());
}
+
+ @Test
+ public void NegotiateResponseWithNegotiateVersion() {
+ String stringNegotiateResponse = "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," +
+ "\"negotiateVersion\": 99}";
+ NegotiateResponse negotiateResponse = new NegotiateResponse(new JsonReader(new StringReader(stringNegotiateResponse)));
+ assertEquals("bVOiRPG8-6YiJ6d7ZcTOVQ", negotiateResponse.getConnectionId());
+ assertEquals(99, negotiateResponse.getVersion());
+ }
+
+ @Test
+ public void NegotiateResponseWithConnectionToken() {
+ String stringNegotiateResponse = "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\"," +
+ "\"negotiateVersion\": 99, \"connectionToken\":\"connection-token-value\"}";
+ NegotiateResponse negotiateResponse = new NegotiateResponse(new JsonReader(new StringReader(stringNegotiateResponse)));
+ assertEquals("bVOiRPG8-6YiJ6d7ZcTOVQ", negotiateResponse.getConnectionId());
+ assertEquals("connection-token-value", negotiateResponse.getConnectionToken());
+ assertEquals(99, negotiateResponse.getVersion());
+ }
}
diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/UserAgentTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/UserAgentTest.java
new file mode 100644
index 0000000000..1f92edc07b
--- /dev/null
+++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/UserAgentTest.java
@@ -0,0 +1,54 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+package com.microsoft.signalr;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class UserAgentTest {
+
+ private static Stream Versions() {
+ return Stream.of(
+ Arguments.of("1.0.0", "1.0"),
+ Arguments.of("3.1.4-preview9-12345", "3.1"),
+ Arguments.of("3.1.4-preview9-12345-extrastuff", "3.1"),
+ Arguments.of("99.99.99-dev", "99.99"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("Versions")
+ public void getVersionFromDetailedVersion(String detailedVersion, String version) {
+ assertEquals(version, UserAgentHelper.getVersion(detailedVersion));
+ }
+
+ @Test
+ public void verifyJavaVendor() {
+ assertEquals(System.getProperty("java.vendor"), UserAgentHelper.getJavaVendor());
+ }
+
+ @Test
+ public void verifyJavaVersion() {
+ assertEquals(System.getProperty("java.version"), UserAgentHelper.getJavaVersion());
+ }
+
+ @Test
+ public void checkUserAgentString() {
+ String userAgent = UserAgentHelper.createUserAgentString();
+ assertNotNull(userAgent);
+
+ String detailedVersion = Version.getDetailedVersion();
+ String handMadeUserAgent = "Microsoft SignalR/" + UserAgentHelper.getVersion(detailedVersion) +
+ " (" + detailedVersion + "; " + UserAgentHelper.getOS() + "; Java; " +
+ UserAgentHelper.getJavaVersion() + "; " + UserAgentHelper.getJavaVendor() + ")";
+
+ assertEquals(handMadeUserAgent, userAgent);
+ }
+}
diff --git a/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts b/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts
index 5720a4e30e..ad6f65db08 100644
--- a/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts
+++ b/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts
@@ -245,7 +245,7 @@ function runJest(httpsUrl: string, httpUrl: string) {
(async () => {
try {
- const serverPath = path.resolve(ARTIFACTS_DIR, "bin", "SignalR.Client.FunctionalTestApp", configuration, "netcoreapp3.1", "SignalR.Client.FunctionalTestApp.dll");
+ const serverPath = path.resolve(ARTIFACTS_DIR, "bin", "SignalR.Client.FunctionalTestApp", configuration, "netcoreapp5.0", "SignalR.Client.FunctionalTestApp.dll");
debug(`Launching Functional Test Server: ${serverPath}`);
let desiredServerUrl = "https://127.0.0.1:0;http://127.0.0.1:0";
diff --git a/src/SignalR/clients/ts/FunctionalTests/ts/Common.ts b/src/SignalR/clients/ts/FunctionalTests/ts/Common.ts
index 2bb33c1c11..c66bff8e49 100644
--- a/src/SignalR/clients/ts/FunctionalTests/ts/Common.ts
+++ b/src/SignalR/clients/ts/FunctionalTests/ts/Common.ts
@@ -1,8 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-import { HttpTransportType, IHubProtocol, JsonHubProtocol } from "@microsoft/signalr";
+import { HttpClient, HttpTransportType, IHubProtocol, JsonHubProtocol } from "@microsoft/signalr";
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
+import { TestLogger } from "./TestLogger";
+
+import { FetchHttpClient } from "@microsoft/signalr/dist/esm/FetchHttpClient";
+import { NodeHttpClient } from "@microsoft/signalr/dist/esm/NodeHttpClient";
+import { Platform } from "@microsoft/signalr/dist/esm/Utils";
+import { XhrHttpClient } from "@microsoft/signalr/dist/esm/XhrHttpClient";
// On slower CI machines, these tests sometimes take longer than 5s
jasmine.DEFAULT_TIMEOUT_INTERVAL = 20 * 1000;
@@ -97,6 +103,34 @@ export function eachTransportAndProtocol(action: (transport: HttpTransportType,
});
}
+export function eachTransportAndProtocolAndHttpClient(action: (transport: HttpTransportType, protocol: IHubProtocol, httpClient: HttpClient) => void) {
+ eachTransportAndProtocol((transport, protocol) => {
+ getHttpClients().forEach((httpClient) => {
+ action(transport, protocol, httpClient);
+ });
+ });
+}
+
export function getGlobalObject(): any {
return typeof window !== "undefined" ? window : global;
}
+
+export function getHttpClients(): HttpClient[] {
+ const httpClients: HttpClient[] = [];
+ if (typeof XMLHttpRequest !== "undefined") {
+ httpClients.push(new XhrHttpClient(TestLogger.instance));
+ }
+ if (typeof fetch !== "undefined") {
+ httpClients.push(new FetchHttpClient(TestLogger.instance));
+ }
+ if (Platform.isNode) {
+ httpClients.push(new NodeHttpClient(TestLogger.instance));
+ }
+ return httpClients;
+}
+
+export function eachHttpClient(action: (transport: HttpClient) => void) {
+ return getHttpClients().forEach((t) => {
+ return action(t);
+ });
+}
diff --git a/src/SignalR/clients/ts/FunctionalTests/ts/ConnectionTests.ts b/src/SignalR/clients/ts/FunctionalTests/ts/ConnectionTests.ts
index 7760294123..3b559af265 100644
--- a/src/SignalR/clients/ts/FunctionalTests/ts/ConnectionTests.ts
+++ b/src/SignalR/clients/ts/FunctionalTests/ts/ConnectionTests.ts
@@ -5,7 +5,7 @@
// tslint:disable:no-floating-promises
import { HttpTransportType, IHttpConnectionOptions, TransferFormat } from "@microsoft/signalr";
-import { eachTransport, ECHOENDPOINT_URL } from "./Common";
+import { eachHttpClient, eachTransport, ECHOENDPOINT_URL } from "./Common";
import { TestLogger } from "./TestLogger";
// We want to continue testing HttpConnection, but we don't export it anymore. So just pull it in directly from the source file.
@@ -44,109 +44,114 @@ describe("connection", () => {
});
eachTransport((transportType) => {
- describe(`over ${HttpTransportType[transportType]}`, () => {
- it("can send and receive messages", (done) => {
- const message = "Hello World!";
- // the url should be resolved relative to the document.location.host
- // and the leading '/' should be automatically added to the url
- const connection = new HttpConnection(ECHOENDPOINT_URL, {
- ...commonOptions,
- transport: transportType,
- });
+ eachHttpClient((httpClient) => {
+ describe(`over ${HttpTransportType[transportType]} with ${(httpClient.constructor as any).name}`, () => {
+ it("can send and receive messages", (done) => {
+ const message = "Hello World!";
+ // the url should be resolved relative to the document.location.host
+ // and the leading '/' should be automatically added to the url
+ const connection = new HttpConnection(ECHOENDPOINT_URL, {
+ ...commonOptions,
+ httpClient,
+ transport: transportType,
+ });
- connection.onreceive = (data: any) => {
- if (data === message) {
- connection.stop();
- }
- };
-
- connection.onclose = (error: any) => {
- expect(error).toBeUndefined();
- done();
- };
-
- connection.start(TransferFormat.Text).then(() => {
- connection.send(message);
- }).catch((e: any) => {
- fail(e);
- done();
- });
- });
-
- it("does not log content of messages sent or received by default", (done) => {
- TestLogger.saveLogsAndReset();
- const message = "Hello World!";
-
- // DON'T use commonOptions because we want to specifically test the scenario where logMessageContent is not set.
- const connection = new HttpConnection(ECHOENDPOINT_URL, {
- logger: TestLogger.instance,
- transport: transportType,
- });
-
- connection.onreceive = (data: any) => {
- if (data === message) {
- connection.stop();
- }
- };
-
- // @ts-ignore: We don't use the error parameter intentionally.
- connection.onclose = (error) => {
- // Search the logs for the message content
- expect(TestLogger.instance.currentLog.messages.length).toBeGreaterThan(0);
- // @ts-ignore: We don't use the _ or __ parameters intentionally.
- for (const [_, __, logMessage] of TestLogger.instance.currentLog.messages) {
- expect(logMessage).not.toContain(message);
- }
- done();
- };
-
- connection.start(TransferFormat.Text).then(() => {
- connection.send(message);
- }).catch((e) => {
- fail(e);
- done();
- });
- });
-
- it("does log content of messages sent or received when enabled", (done) => {
- TestLogger.saveLogsAndReset();
- const message = "Hello World!";
-
- // DON'T use commonOptions because we want to specifically test the scenario where logMessageContent is set to true (even if commonOptions changes).
- const connection = new HttpConnection(ECHOENDPOINT_URL, {
- logMessageContent: true,
- logger: TestLogger.instance,
- transport: transportType,
- });
-
- connection.onreceive = (data: any) => {
- if (data === message) {
- connection.stop();
- }
- };
-
- // @ts-ignore: We don't use the error parameter intentionally.
- connection.onclose = (error) => {
- // Search the logs for the message content
- let matches = 0;
- expect(TestLogger.instance.currentLog.messages.length).toBeGreaterThan(0);
- // @ts-ignore: We don't use the _ or __ parameters intentionally.
- for (const [_, __, logMessage] of TestLogger.instance.currentLog.messages) {
- if (logMessage.indexOf(message) !== -1) {
- matches += 1;
+ connection.onreceive = (data: any) => {
+ if (data === message) {
+ connection.stop();
}
- }
+ };
- // One match for send, one for receive.
- expect(matches).toEqual(2);
- done();
- };
+ connection.onclose = (error: any) => {
+ expect(error).toBeUndefined();
+ done();
+ };
- connection.start(TransferFormat.Text).then(() => {
- connection.send(message);
- }).catch((e: any) => {
- fail(e);
- done();
+ connection.start(TransferFormat.Text).then(() => {
+ connection.send(message);
+ }).catch((e: any) => {
+ fail(e);
+ done();
+ });
+ });
+
+ it("does not log content of messages sent or received by default", (done) => {
+ TestLogger.saveLogsAndReset();
+ const message = "Hello World!";
+
+ // DON'T use commonOptions because we want to specifically test the scenario where logMessageContent is not set.
+ const connection = new HttpConnection(ECHOENDPOINT_URL, {
+ httpClient,
+ logger: TestLogger.instance,
+ transport: transportType,
+ });
+
+ connection.onreceive = (data: any) => {
+ if (data === message) {
+ connection.stop();
+ }
+ };
+
+ // @ts-ignore: We don't use the error parameter intentionally.
+ connection.onclose = (error) => {
+ // Search the logs for the message content
+ expect(TestLogger.instance.currentLog.messages.length).toBeGreaterThan(0);
+ // @ts-ignore: We don't use the _ or __ parameters intentionally.
+ for (const [_, __, logMessage] of TestLogger.instance.currentLog.messages) {
+ expect(logMessage).not.toContain(message);
+ }
+ done();
+ };
+
+ connection.start(TransferFormat.Text).then(() => {
+ connection.send(message);
+ }).catch((e) => {
+ fail(e);
+ done();
+ });
+ });
+
+ it("does log content of messages sent or received when enabled", (done) => {
+ TestLogger.saveLogsAndReset();
+ const message = "Hello World!";
+
+ // DON'T use commonOptions because we want to specifically test the scenario where logMessageContent is set to true (even if commonOptions changes).
+ const connection = new HttpConnection(ECHOENDPOINT_URL, {
+ httpClient,
+ logMessageContent: true,
+ logger: TestLogger.instance,
+ transport: transportType,
+ });
+
+ connection.onreceive = (data: any) => {
+ if (data === message) {
+ connection.stop();
+ }
+ };
+
+ // @ts-ignore: We don't use the error parameter intentionally.
+ connection.onclose = (error) => {
+ // Search the logs for the message content
+ let matches = 0;
+ expect(TestLogger.instance.currentLog.messages.length).toBeGreaterThan(0);
+ // @ts-ignore: We don't use the _ or __ parameters intentionally.
+ for (const [_, __, logMessage] of TestLogger.instance.currentLog.messages) {
+ if (logMessage.indexOf(message) !== -1) {
+ matches += 1;
+ }
+ }
+
+ // One match for send, one for receive.
+ expect(matches).toEqual(2);
+ done();
+ };
+
+ connection.start(TransferFormat.Text).then(() => {
+ connection.send(message);
+ }).catch((e: any) => {
+ fail(e);
+ done();
+ });
});
});
});
diff --git a/src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts b/src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts
index 3d5f434a17..dbfa1a886f 100644
--- a/src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts
+++ b/src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts
@@ -7,7 +7,7 @@
import { AbortError, DefaultHttpClient, HttpClient, HttpRequest, HttpResponse, HttpTransportType, HubConnectionBuilder, IHttpConnectionOptions, JsonHubProtocol, NullLogger } from "@microsoft/signalr";
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
-import { eachTransport, eachTransportAndProtocol, ENDPOINT_BASE_HTTPS_URL, ENDPOINT_BASE_URL } from "./Common";
+import { eachTransport, eachTransportAndProtocolAndHttpClient, ENDPOINT_BASE_HTTPS_URL, ENDPOINT_BASE_URL } from "./Common";
import "./LogBannerReporter";
import { TestLogger } from "./TestLogger";
@@ -49,12 +49,12 @@ function getConnectionBuilder(transportType?: HttpTransportType, url?: string, o
}
describe("hubConnection", () => {
- eachTransportAndProtocol((transportType, protocol) => {
+ eachTransportAndProtocolAndHttpClient((transportType, protocol, httpClient) => {
describe("using " + protocol.name + " over " + HttpTransportType[transportType] + " transport", () => {
it("can invoke server method and receive result", (done) => {
const message = "你好,世界!";
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -81,7 +81,7 @@ describe("hubConnection", () => {
it("using https, can invoke server method and receive result", (done) => {
const message = "你好,世界!";
- const hubConnection = getConnectionBuilder(transportType, TESTHUBENDPOINT_HTTPS_URL)
+ const hubConnection = getConnectionBuilder(transportType, TESTHUBENDPOINT_HTTPS_URL, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -108,7 +108,7 @@ describe("hubConnection", () => {
it("can invoke server method non-blocking and not receive result", (done) => {
const message = "你好,世界!";
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -130,7 +130,7 @@ describe("hubConnection", () => {
});
it("can invoke server method structural object and receive structural result", (done) => {
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -154,7 +154,7 @@ describe("hubConnection", () => {
});
it("can stream server method and receive result", (done) => {
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -185,7 +185,7 @@ describe("hubConnection", () => {
});
it("can stream server method and cancel stream", (done) => {
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -219,7 +219,7 @@ describe("hubConnection", () => {
it("rethrows an exception from the server when invoking", (done) => {
const errorMessage = "An unexpected error occurred invoking 'ThrowException' on the server. InvalidOperationException: An error occurred.";
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -241,7 +241,7 @@ describe("hubConnection", () => {
});
it("throws an exception when invoking streaming method with invoke", (done) => {
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -263,7 +263,7 @@ describe("hubConnection", () => {
});
it("throws an exception when receiving a streaming result for method called with invoke", (done) => {
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -286,7 +286,7 @@ describe("hubConnection", () => {
it("rethrows an exception from the server when streaming", (done) => {
const errorMessage = "An unexpected error occurred invoking 'StreamThrowException' on the server. InvalidOperationException: An error occurred.";
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -313,7 +313,7 @@ describe("hubConnection", () => {
});
it("throws an exception when invoking hub method with stream", (done) => {
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -340,7 +340,7 @@ describe("hubConnection", () => {
});
it("can receive server calls", (done) => {
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -370,7 +370,7 @@ describe("hubConnection", () => {
});
it("can receive server calls without rebinding handler when restarted", (done) => {
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -425,7 +425,7 @@ describe("hubConnection", () => {
});
it("closed with error or start fails if hub cannot be created", async (done) => {
- const hubConnection = getConnectionBuilder(transportType, ENDPOINT_BASE_URL + "/uncreatable")
+ const hubConnection = getConnectionBuilder(transportType, ENDPOINT_BASE_URL + "/uncreatable", { httpClient })
.withHubProtocol(protocol)
.build();
@@ -446,7 +446,7 @@ describe("hubConnection", () => {
});
it("can handle different types", (done) => {
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -489,7 +489,7 @@ describe("hubConnection", () => {
});
it("can receive different types", (done) => {
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -534,7 +534,7 @@ describe("hubConnection", () => {
it("can be restarted", (done) => {
const message = "你好,世界!";
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -577,7 +577,7 @@ describe("hubConnection", () => {
});
it("can stream from client to server with rxjs", async (done) => {
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@@ -594,7 +594,7 @@ describe("hubConnection", () => {
});
it("can stream from client to server and close with error with rxjs", async (done) => {
- const hubConnection = getConnectionBuilder(transportType)
+ const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
diff --git a/src/SignalR/clients/ts/signalr-protocol-msgpack/README.md b/src/SignalR/clients/ts/signalr-protocol-msgpack/README.md
index e840374319..00856c5496 100644
--- a/src/SignalR/clients/ts/signalr-protocol-msgpack/README.md
+++ b/src/SignalR/clients/ts/signalr-protocol-msgpack/README.md
@@ -12,7 +12,7 @@ yarn add @microsoft/signalr-protocol-msgpack
## Usage
-See the [SignalR Documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr) at docs.microsoft.com for documentation on the latest release. [API Reference Documentation](https://docs.microsoft.com/javascript/api/%40aspnet/signalr-protocol-msgpack/?view=signalr-js-latest) is also available on docs.microsoft.com.
+See the [SignalR Documentation](https://docs.microsoft.com/aspnet/core/signalr) at docs.microsoft.com for documentation on the latest release. [API Reference Documentation](https://docs.microsoft.com/javascript/api/%40aspnet/signalr-protocol-msgpack/?view=signalr-js-latest) is also available on docs.microsoft.com.
### Browser
diff --git a/src/SignalR/clients/ts/signalr-protocol-msgpack/package.json b/src/SignalR/clients/ts/signalr-protocol-msgpack/package.json
index 3d262cc056..d53083aa27 100644
--- a/src/SignalR/clients/ts/signalr-protocol-msgpack/package.json
+++ b/src/SignalR/clients/ts/signalr-protocol-msgpack/package.json
@@ -1,6 +1,6 @@
{
"name": "@microsoft/signalr-protocol-msgpack",
- "version": "3.0.0-dev",
+ "version": "5.0.0-dev",
"description": "MsgPack Protocol support for ASP.NET Core SignalR",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
diff --git a/src/SignalR/clients/ts/signalr-protocol-msgpack/signalr-protocol-msgpack.npmproj b/src/SignalR/clients/ts/signalr-protocol-msgpack/signalr-protocol-msgpack.npmproj
index 72978faa2d..1a2b2deac3 100644
--- a/src/SignalR/clients/ts/signalr-protocol-msgpack/signalr-protocol-msgpack.npmproj
+++ b/src/SignalR/clients/ts/signalr-protocol-msgpack/signalr-protocol-msgpack.npmproj
@@ -13,5 +13,9 @@
+
+
+
+
diff --git a/src/SignalR/clients/ts/signalr/README.md b/src/SignalR/clients/ts/signalr/README.md
index ec8f34b227..3bf0d6534f 100644
--- a/src/SignalR/clients/ts/signalr/README.md
+++ b/src/SignalR/clients/ts/signalr/README.md
@@ -1,4 +1,4 @@
-JavaScript and TypeScript clients for SignalR for ASP.NET Core
+JavaScript and TypeScript clients for SignalR for ASP.NET Core and Azure SignalR Service
## Installation
@@ -12,7 +12,9 @@ yarn add @microsoft/signalr
## Usage
-See the [SignalR Documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr) at docs.microsoft.com for documentation on the latest release. [API Reference Documentation](https://docs.microsoft.com/javascript/api/%40aspnet/signalr/?view=signalr-js-latest) is also available on docs.microsoft.com.
+See the [SignalR Documentation](https://docs.microsoft.com/aspnet/core/signalr) at docs.microsoft.com for documentation on the latest release. [API Reference Documentation](https://docs.microsoft.com/javascript/api/%40aspnet/signalr/?view=signalr-js-latest) is also available on docs.microsoft.com.
+
+For documentation on using this client with Azure SignalR Service and Azure Functions, see the [SignalR Service serverless developer guide](https://docs.microsoft.com/azure/azure-signalr/signalr-concept-serverless-development-config).
### Browser
diff --git a/src/SignalR/clients/ts/signalr/package.json b/src/SignalR/clients/ts/signalr/package.json
index 6b50f24d35..7fcfc9fb29 100644
--- a/src/SignalR/clients/ts/signalr/package.json
+++ b/src/SignalR/clients/ts/signalr/package.json
@@ -1,6 +1,6 @@
{
"name": "@microsoft/signalr",
- "version": "3.0.0-dev",
+ "version": "5.0.0-dev",
"description": "ASP.NET Core SignalR Client",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
diff --git a/src/SignalR/clients/ts/signalr/signalr.npmproj b/src/SignalR/clients/ts/signalr/signalr.npmproj
index e6a6c1d993..dbd62e31c6 100644
--- a/src/SignalR/clients/ts/signalr/signalr.npmproj
+++ b/src/SignalR/clients/ts/signalr/signalr.npmproj
@@ -8,5 +8,10 @@
true
+
+
+
+
+
diff --git a/src/SignalR/clients/ts/signalr/src/DefaultHttpClient.ts b/src/SignalR/clients/ts/signalr/src/DefaultHttpClient.ts
index fece43020d..8058e5716a 100644
--- a/src/SignalR/clients/ts/signalr/src/DefaultHttpClient.ts
+++ b/src/SignalR/clients/ts/signalr/src/DefaultHttpClient.ts
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { AbortError } from "./Errors";
+import { FetchHttpClient } from "./FetchHttpClient";
import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
import { ILogger } from "./ILogger";
import { NodeHttpClient } from "./NodeHttpClient";
@@ -15,7 +16,9 @@ export class DefaultHttpClient extends HttpClient {
public constructor(logger: ILogger) {
super();
- if (typeof XMLHttpRequest !== "undefined") {
+ if (typeof fetch !== "undefined") {
+ this.httpClient = new FetchHttpClient(logger);
+ } else if (typeof XMLHttpRequest !== "undefined") {
this.httpClient = new XhrHttpClient(logger);
} else {
this.httpClient = new NodeHttpClient(logger);
diff --git a/src/SignalR/clients/ts/signalr/src/FetchHttpClient.ts b/src/SignalR/clients/ts/signalr/src/FetchHttpClient.ts
new file mode 100644
index 0000000000..48840b42c2
--- /dev/null
+++ b/src/SignalR/clients/ts/signalr/src/FetchHttpClient.ts
@@ -0,0 +1,121 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+import { AbortError, HttpError, TimeoutError } from "./Errors";
+import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
+import { ILogger, LogLevel } from "./ILogger";
+
+export class FetchHttpClient extends HttpClient {
+ private readonly logger: ILogger;
+
+ public constructor(logger: ILogger) {
+ super();
+ this.logger = logger;
+ }
+
+ /** @inheritDoc */
+ public async send(request: HttpRequest): Promise {
+ // Check that abort was not signaled before calling send
+ if (request.abortSignal && request.abortSignal.aborted) {
+ throw new AbortError();
+ }
+
+ if (!request.method) {
+ throw new Error("No method defined.");
+ }
+ if (!request.url) {
+ throw new Error("No url defined.");
+ }
+
+ const abortController = new AbortController();
+
+ let error: any;
+ // Hook our abortSignal into the abort controller
+ if (request.abortSignal) {
+ request.abortSignal.onabort = () => {
+ abortController.abort();
+ error = new AbortError();
+ };
+ }
+
+ // If a timeout has been passed in, setup a timeout to call abort
+ // Type needs to be any to fit window.setTimeout and NodeJS.setTimeout
+ let timeoutId: any = null;
+ if (request.timeout) {
+ const msTimeout = request.timeout!;
+ timeoutId = setTimeout(() => {
+ abortController.abort();
+ this.logger.log(LogLevel.Warning, `Timeout from HTTP request.`);
+ error = new TimeoutError();
+ }, msTimeout);
+ }
+
+ let response: Response;
+ try {
+ response = await fetch(request.url!, {
+ body: request.content!,
+ cache: "no-cache",
+ credentials: "include",
+ headers: {
+ "Content-Type": "text/plain;charset=UTF-8",
+ "X-Requested-With": "XMLHttpRequest",
+ ...request.headers,
+ },
+ method: request.method!,
+ mode: "cors",
+ redirect: "manual",
+ signal: abortController.signal,
+ });
+ } catch (e) {
+ if (error) {
+ throw error;
+ }
+ this.logger.log(
+ LogLevel.Warning,
+ `Error from HTTP request. ${e}.`,
+ );
+ throw e;
+ } finally {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ if (request.abortSignal) {
+ request.abortSignal.onabort = null;
+ }
+ }
+
+ if (!response.ok) {
+ throw new HttpError(response.statusText, response.status);
+ }
+
+ const content = deserializeContent(response, request.responseType);
+ const payload = await content;
+
+ return new HttpResponse(
+ response.status,
+ response.statusText,
+ payload,
+ );
+ }
+}
+
+function deserializeContent(response: Response, responseType?: XMLHttpRequestResponseType): Promise {
+ let content;
+ switch (responseType) {
+ case "arraybuffer":
+ content = response.arrayBuffer();
+ break;
+ case "text":
+ content = response.text();
+ break;
+ case "blob":
+ case "document":
+ case "json":
+ throw new Error(`${responseType} is not supported.`);
+ default:
+ content = response.text();
+ break;
+ }
+
+ return content;
+}
diff --git a/src/SignalR/clients/ts/signalr/src/HttpClient.ts b/src/SignalR/clients/ts/signalr/src/HttpClient.ts
index 9685feca5e..c50289bbf1 100644
--- a/src/SignalR/clients/ts/signalr/src/HttpClient.ts
+++ b/src/SignalR/clients/ts/signalr/src/HttpClient.ts
@@ -57,6 +57,14 @@ export class HttpResponse {
* @param {ArrayBuffer} content The content of the response.
*/
constructor(statusCode: number, statusText: string, content: ArrayBuffer);
+
+ /** Constructs a new instance of {@link @microsoft/signalr.HttpResponse} with the specified status code, message and binary content.
+ *
+ * @param {number} statusCode The status code of the response.
+ * @param {string} statusText The status message of the response.
+ * @param {string | ArrayBuffer} content The content of the response.
+ */
+ constructor(statusCode: number, statusText: string, content: string | ArrayBuffer);
constructor(
public readonly statusCode: number,
public readonly statusText?: string,
diff --git a/src/SignalR/clients/ts/signalr/src/HubConnectionBuilder.ts b/src/SignalR/clients/ts/signalr/src/HubConnectionBuilder.ts
index 98a23a3b3d..fa5d7432b9 100644
--- a/src/SignalR/clients/ts/signalr/src/HubConnectionBuilder.ts
+++ b/src/SignalR/clients/ts/signalr/src/HubConnectionBuilder.ts
@@ -70,14 +70,14 @@ export class HubConnectionBuilder {
/** Configures custom logging for the {@link @microsoft/signalr.HubConnection}.
*
* @param {string} logLevel A string representing a LogLevel setting a minimum level of messages to log.
- * See {@link https://docs.microsoft.com/en-us/aspnet/core/signalr/configuration#configure-logging|the documentation for client logging configuration} for more details.
+ * See {@link https://docs.microsoft.com/aspnet/core/signalr/configuration#configure-logging|the documentation for client logging configuration} for more details.
*/
public configureLogging(logLevel: string): HubConnectionBuilder;
/** Configures custom logging for the {@link @microsoft/signalr.HubConnection}.
*
* @param {LogLevel | string | ILogger} logging A {@link @microsoft/signalr.LogLevel}, a string representing a LogLevel, or an object implementing the {@link @microsoft/signalr.ILogger} interface.
- * See {@link https://docs.microsoft.com/en-us/aspnet/core/signalr/configuration#configure-logging|the documentation for client logging configuration} for more details.
+ * See {@link https://docs.microsoft.com/aspnet/core/signalr/configuration#configure-logging|the documentation for client logging configuration} for more details.
* @returns The {@link @microsoft/signalr.HubConnectionBuilder} instance, for chaining.
*/
public configureLogging(logging: LogLevel | string | ILogger): HubConnectionBuilder;
diff --git a/src/SignalR/common/Http.Connections.Common/ref/Microsoft.AspNetCore.Http.Connections.Common.netcoreapp.cs b/src/SignalR/common/Http.Connections.Common/ref/Microsoft.AspNetCore.Http.Connections.Common.netcoreapp.cs
index 27d206da33..f557b74f58 100644
--- a/src/SignalR/common/Http.Connections.Common/ref/Microsoft.AspNetCore.Http.Connections.Common.netcoreapp.cs
+++ b/src/SignalR/common/Http.Connections.Common/ref/Microsoft.AspNetCore.Http.Connections.Common.netcoreapp.cs
@@ -34,7 +34,9 @@ namespace Microsoft.AspNetCore.Http.Connections
public string AccessToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.Collections.Generic.IList AvailableTransports { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string ConnectionId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public string ConnectionToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Error { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Url { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public int Version { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
}
diff --git a/src/SignalR/common/Http.Connections.Common/ref/Microsoft.AspNetCore.Http.Connections.Common.netstandard2.0.cs b/src/SignalR/common/Http.Connections.Common/ref/Microsoft.AspNetCore.Http.Connections.Common.netstandard2.0.cs
index 27d206da33..f557b74f58 100644
--- a/src/SignalR/common/Http.Connections.Common/ref/Microsoft.AspNetCore.Http.Connections.Common.netstandard2.0.cs
+++ b/src/SignalR/common/Http.Connections.Common/ref/Microsoft.AspNetCore.Http.Connections.Common.netstandard2.0.cs
@@ -34,7 +34,9 @@ namespace Microsoft.AspNetCore.Http.Connections
public string AccessToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.Collections.Generic.IList AvailableTransports { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string ConnectionId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public string ConnectionToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Error { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Url { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ public int Version { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
}
diff --git a/src/SignalR/common/Http.Connections.Common/src/Microsoft.AspNetCore.Http.Connections.Common.csproj b/src/SignalR/common/Http.Connections.Common/src/Microsoft.AspNetCore.Http.Connections.Common.csproj
index 521467f849..c40fa68e6d 100644
--- a/src/SignalR/common/Http.Connections.Common/src/Microsoft.AspNetCore.Http.Connections.Common.csproj
+++ b/src/SignalR/common/Http.Connections.Common/src/Microsoft.AspNetCore.Http.Connections.Common.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs b/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs
index a23a9d6c0b..ae69b56cdd 100644
--- a/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs
+++ b/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs
@@ -15,6 +15,8 @@ namespace Microsoft.AspNetCore.Http.Connections
{
private const string ConnectionIdPropertyName = "connectionId";
private static JsonEncodedText ConnectionIdPropertyNameBytes = JsonEncodedText.Encode(ConnectionIdPropertyName);
+ private const string ConnectionTokenPropertyName = "connectionToken";
+ private static JsonEncodedText ConnectionTokenPropertyNameBytes = JsonEncodedText.Encode(ConnectionTokenPropertyName);
private const string UrlPropertyName = "url";
private static JsonEncodedText UrlPropertyNameBytes = JsonEncodedText.Encode(UrlPropertyName);
private const string AccessTokenPropertyName = "accessToken";
@@ -27,6 +29,8 @@ namespace Microsoft.AspNetCore.Http.Connections
private static JsonEncodedText TransferFormatsPropertyNameBytes = JsonEncodedText.Encode(TransferFormatsPropertyName);
private const string ErrorPropertyName = "error";
private static JsonEncodedText ErrorPropertyNameBytes = JsonEncodedText.Encode(ErrorPropertyName);
+ private const string NegotiateVersionPropertyName = "negotiateVersion";
+ private static JsonEncodedText NegotiateVersionPropertyNameBytes = JsonEncodedText.Encode(NegotiateVersionPropertyName);
// Use C#7.3's ReadOnlySpan optimization for static data https://vcsjones.com/2019/02/01/csharp-readonly-span-bytes-static/
// Used to detect ASP.NET SignalR Server connection attempt
@@ -41,6 +45,19 @@ namespace Microsoft.AspNetCore.Http.Connections
var writer = reusableWriter.GetJsonWriter();
writer.WriteStartObject();
+ // If we already have an error its due to a protocol version incompatibility.
+ // We can just write the error and complete the JSON object and return.
+ if (!string.IsNullOrEmpty(response.Error))
+ {
+ writer.WriteString(ErrorPropertyNameBytes, response.Error);
+ writer.WriteEndObject();
+ writer.Flush();
+ Debug.Assert(writer.CurrentDepth == 0);
+ return;
+ }
+
+ writer.WriteNumber(NegotiateVersionPropertyNameBytes, response.Version);
+
if (!string.IsNullOrEmpty(response.Url))
{
writer.WriteString(UrlPropertyNameBytes, response.Url);
@@ -56,6 +73,11 @@ namespace Microsoft.AspNetCore.Http.Connections
writer.WriteString(ConnectionIdPropertyNameBytes, response.ConnectionId);
}
+ if (response.Version > 0 && !string.IsNullOrEmpty(response.ConnectionToken))
+ {
+ writer.WriteString(ConnectionTokenPropertyNameBytes, response.ConnectionToken);
+ }
+
writer.WriteStartArray(AvailableTransportsPropertyNameBytes);
if (response.AvailableTransports != null)
@@ -112,10 +134,12 @@ namespace Microsoft.AspNetCore.Http.Connections
reader.EnsureObjectStart();
string connectionId = null;
+ string connectionToken = null;
string url = null;
string accessToken = null;
List availableTransports = null;
string error = null;
+ int version = 0;
var completed = false;
while (!completed && reader.CheckRead())
@@ -135,6 +159,14 @@ namespace Microsoft.AspNetCore.Http.Connections
{
connectionId = reader.ReadAsString(ConnectionIdPropertyName);
}
+ else if (reader.ValueTextEquals(ConnectionTokenPropertyNameBytes.EncodedUtf8Bytes))
+ {
+ connectionToken = reader.ReadAsString(ConnectionTokenPropertyName);
+ }
+ else if (reader.ValueTextEquals(NegotiateVersionPropertyNameBytes.EncodedUtf8Bytes))
+ {
+ version = reader.ReadAsInt32(NegotiateVersionPropertyName).GetValueOrDefault();
+ }
else if (reader.ValueTextEquals(AvailableTransportsPropertyNameBytes.EncodedUtf8Bytes))
{
reader.CheckRead();
@@ -182,6 +214,14 @@ namespace Microsoft.AspNetCore.Http.Connections
throw new InvalidDataException($"Missing required property '{ConnectionIdPropertyName}'.");
}
+ if (version > 0)
+ {
+ if (connectionToken == null)
+ {
+ throw new InvalidDataException($"Missing required property '{ConnectionTokenPropertyName}'.");
+ }
+ }
+
if (availableTransports == null)
{
throw new InvalidDataException($"Missing required property '{AvailableTransportsPropertyName}'.");
@@ -191,10 +231,12 @@ namespace Microsoft.AspNetCore.Http.Connections
return new NegotiationResponse
{
ConnectionId = connectionId,
+ ConnectionToken = connectionToken,
Url = url,
AccessToken = accessToken,
AvailableTransports = availableTransports,
Error = error,
+ Version = version
};
}
catch (Exception ex)
@@ -222,13 +264,11 @@ namespace Microsoft.AspNetCore.Http.Connections
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
- var memberName = reader.ValueSpan;
-
- if (memberName.SequenceEqual(TransportPropertyNameBytes.EncodedUtf8Bytes))
+ if (reader.ValueTextEquals(TransportPropertyNameBytes.EncodedUtf8Bytes))
{
availableTransport.Transport = reader.ReadAsString(TransportPropertyName);
}
- else if (memberName.SequenceEqual(TransferFormatsPropertyNameBytes.EncodedUtf8Bytes))
+ else if (reader.ValueTextEquals(TransferFormatsPropertyNameBytes.EncodedUtf8Bytes))
{
reader.CheckRead();
reader.EnsureArrayStart();
diff --git a/src/SignalR/common/Http.Connections.Common/src/NegotiationResponse.cs b/src/SignalR/common/Http.Connections.Common/src/NegotiationResponse.cs
index a01d2e637c..69810a5a71 100644
--- a/src/SignalR/common/Http.Connections.Common/src/NegotiationResponse.cs
+++ b/src/SignalR/common/Http.Connections.Common/src/NegotiationResponse.cs
@@ -10,6 +10,8 @@ namespace Microsoft.AspNetCore.Http.Connections
public string Url { get; set; }
public string AccessToken { get; set; }
public string ConnectionId { get; set; }
+ public string ConnectionToken { get; set; }
+ public int Version { get; set; }
public IList AvailableTransports { get; set; }
public string Error { get; set; }
}
diff --git a/src/SignalR/common/Http.Connections/ref/Microsoft.AspNetCore.Http.Connections.netcoreapp.cs b/src/SignalR/common/Http.Connections/ref/Microsoft.AspNetCore.Http.Connections.netcoreapp.cs
index 7810a4985d..5ee369727c 100644
--- a/src/SignalR/common/Http.Connections/ref/Microsoft.AspNetCore.Http.Connections.netcoreapp.cs
+++ b/src/SignalR/common/Http.Connections/ref/Microsoft.AspNetCore.Http.Connections.netcoreapp.cs
@@ -53,6 +53,7 @@ namespace Microsoft.AspNetCore.Http.Connections
public long ApplicationMaxBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.Collections.Generic.IList AuthorizationData { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public Microsoft.AspNetCore.Http.Connections.LongPollingOptions LongPolling { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
+ public int MinimumProtocolVersion { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public long TransportMaxBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Http.Connections.HttpTransportType Transports { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Http.Connections.WebSocketOptions WebSockets { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
diff --git a/src/SignalR/common/Http.Connections/src/HttpConnectionDispatcherOptions.cs b/src/SignalR/common/Http.Connections/src/HttpConnectionDispatcherOptions.cs
index eff4ae76e4..e1f97d7183 100644
--- a/src/SignalR/common/Http.Connections/src/HttpConnectionDispatcherOptions.cs
+++ b/src/SignalR/common/Http.Connections/src/HttpConnectionDispatcherOptions.cs
@@ -57,5 +57,11 @@ namespace Microsoft.AspNetCore.Http.Connections
/// Gets or sets the maximum buffer size of the application writer.
///
public long ApplicationMaxBufferSize { get; set; }
+
+ ///
+ /// Gets or sets the minimum protocol verison supported by the server.
+ /// The default value is 0, the lowest possible protocol version.
+ ///
+ public int MinimumProtocolVersion { get; set; } = 0;
}
}
diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs
index 6e21d7a665..6d3fe467e9 100644
--- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs
+++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs
@@ -48,11 +48,13 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
/// Creates the DefaultConnectionContext without Pipes to avoid upfront allocations.
/// The caller is expected to set the and pipes manually.
///
- ///
+ ///
+ ///
///
- public HttpConnectionContext(string id, ILogger logger)
+ public HttpConnectionContext(string connectionId, string connectionToken, ILogger logger)
{
- ConnectionId = id;
+ ConnectionId = connectionId;
+ ConnectionToken = connectionToken;
LastSeenUtc = DateTime.UtcNow;
// The default behavior is that both formats are supported.
@@ -74,8 +76,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
Features.Set(this);
}
- public HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application, ILogger logger = null)
- : this(id, logger)
+ internal HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application, ILogger logger = null)
+ : this(id, null, logger)
{
Transport = transport;
Application = application;
@@ -113,6 +115,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
public override string ConnectionId { get; set; }
+ internal string ConnectionToken { get; set; }
+
public override IFeatureCollection Features { get; }
public ClaimsPrincipal User { get; set; }
diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.Log.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.Log.cs
index af91f08af2..80f3d32800 100644
--- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.Log.cs
+++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.Log.cs
@@ -52,6 +52,12 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
private static readonly Action _failedToReadHttpRequestBody =
LoggerMessage.Define(LogLevel.Debug, new EventId(14, "FailedToReadHttpRequestBody"), "Connection {TransportConnectionId} failed to read the HTTP request body.");
+ private static readonly Action _negotiateProtocolVersionMismatch =
+ LoggerMessage.Define(LogLevel.Debug, new EventId(15, "NegotiateProtocolVersionMismatch"), "The client requested version '{clientProtocolVersion}', but the server does not support this version.");
+
+ private static readonly Action _invalidNegotiateProtocolVersion =
+ LoggerMessage.Define(LogLevel.Debug, new EventId(16, "InvalidNegotiateProtocolVersion"), "The client requested an invalid protocol version '{queryStringVersionValue}'");
+
public static void ConnectionDisposed(ILogger logger, string connectionId)
{
_connectionDisposed(logger, connectionId, null);
@@ -121,6 +127,16 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
{
_failedToReadHttpRequestBody(logger, connectionId, ex);
}
+
+ public static void NegotiateProtocolVersionMismatch(ILogger logger, int clientProtocolVersion)
+ {
+ _negotiateProtocolVersionMismatch(logger, clientProtocolVersion, null);
+ }
+
+ public static void InvalidNegotiateProtocolVersion(ILogger logger, string requestedProtocolVersion)
+ {
+ _invalidNegotiateProtocolVersion(logger, requestedProtocolVersion, null);
+ }
}
}
}
diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs
index bf82562a7b..7bd4acc682 100644
--- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs
+++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs
@@ -45,6 +45,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
private readonly HttpConnectionManager _manager;
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger _logger;
+ private static readonly int _protocolVersion = 1;
public HttpConnectionDispatcher(HttpConnectionManager manager, ILoggerFactory loggerFactory)
{
@@ -58,7 +59,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
// Create the log scope and attempt to pass the Connection ID to it so as many logs as possible contain
// the Connection ID metadata. If this is the negotiate request then the Connection ID for the scope will
// be set a little later.
- var logScope = new ConnectionLogScope(GetConnectionId(context));
+
+ HttpConnectionContext connectionContext = null;
+ var connectionToken = GetConnectionToken(context);
+ if (connectionToken != null)
+ {
+ _manager.TryGetConnection(GetConnectionToken(context), out connectionContext);
+ }
+
+ var logScope = new ConnectionLogScope(connectionContext?.ConnectionId);
using (_logger.BeginScope(logScope))
{
if (HttpMethods.IsPost(context.Request.Method))
@@ -278,13 +287,29 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
private async Task ProcessNegotiate(HttpContext context, HttpConnectionDispatcherOptions options, ConnectionLogScope logScope)
{
context.Response.ContentType = "application/json";
+ string error = null;
+ int clientProtocolVersion = 0;
+ if (context.Request.Query.TryGetValue("NegotiateVersion", out var queryStringVersion))
+ {
+ // Set the negotiate response to the protocol we use.
+ var queryStringVersionValue = queryStringVersion.ToString();
+ if (!int.TryParse(queryStringVersionValue, out clientProtocolVersion))
+ {
+ error = $"The client requested an invalid protocol version '{queryStringVersionValue}'";
+ Log.InvalidNegotiateProtocolVersion(_logger, queryStringVersionValue);
+ }
+ }
// Establish the connection
- var connection = CreateConnection(options);
+ HttpConnectionContext connection = null;
+ if (error == null)
+ {
+ connection = CreateConnection(options, clientProtocolVersion);
+ }
// Set the Connection ID on the logging scope so that logs from now on will have the
// Connection ID metadata set.
- logScope.ConnectionId = connection.ConnectionId;
+ logScope.ConnectionId = connection?.ConnectionId;
// Don't use thread static instance here because writer is used with async
var writer = new MemoryBufferWriter();
@@ -292,7 +317,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
try
{
// Get the bytes for the connection id
- WriteNegotiatePayload(writer, connection.ConnectionId, context, options);
+ WriteNegotiatePayload(writer, connection?.ConnectionId, connection?.ConnectionToken, context, options, clientProtocolVersion, error);
Log.NegotiationRequest(_logger);
@@ -306,10 +331,46 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
}
}
- private static void WriteNegotiatePayload(IBufferWriter writer, string connectionId, HttpContext context, HttpConnectionDispatcherOptions options)
+ private void WriteNegotiatePayload(IBufferWriter writer, string connectionId, string connectionToken, HttpContext context, HttpConnectionDispatcherOptions options,
+ int clientProtocolVersion, string error)
{
var response = new NegotiationResponse();
+
+ if (!string.IsNullOrEmpty(error))
+ {
+ response.Error = error;
+ NegotiateProtocol.WriteResponse(response, writer);
+ return;
+ }
+
+ if (clientProtocolVersion > 0)
+ {
+ if (clientProtocolVersion < options.MinimumProtocolVersion)
+ {
+ response.Error = $"The client requested version '{clientProtocolVersion}', but the server does not support this version.";
+ Log.NegotiateProtocolVersionMismatch(_logger, clientProtocolVersion);
+ NegotiateProtocol.WriteResponse(response, writer);
+ return;
+ }
+ else if (clientProtocolVersion > _protocolVersion)
+ {
+ response.Version = _protocolVersion;
+ }
+ else
+ {
+ response.Version = clientProtocolVersion;
+ }
+ }
+ else if (options.MinimumProtocolVersion > 0)
+ {
+ // NegotiateVersion wasn't parsed meaning the client requests version 0.
+ response.Error = $"The client requested version '0', but the server does not support this version.";
+ NegotiateProtocol.WriteResponse(response, writer);
+ return;
+ }
+
response.ConnectionId = connectionId;
+ response.ConnectionToken = connectionToken;
response.AvailableTransports = new List();
if ((options.Transports & HttpTransportType.WebSockets) != 0 && ServerHasWebSockets(context.Features))
@@ -335,7 +396,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
return features.Get() != null;
}
- private static string GetConnectionId(HttpContext context) => context.Request.Query["id"];
+ private static string GetConnectionToken(HttpContext context) => context.Request.Query["id"];
private async Task ProcessSend(HttpContext context, HttpConnectionDispatcherOptions options)
{
@@ -608,9 +669,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
private async Task GetConnectionAsync(HttpContext context)
{
- var connectionId = GetConnectionId(context);
+ var connectionToken = GetConnectionToken(context);
- if (StringValues.IsNullOrEmpty(connectionId))
+ if (StringValues.IsNullOrEmpty(connectionToken))
{
// There's no connection ID: bad request
context.Response.StatusCode = StatusCodes.Status400BadRequest;
@@ -619,7 +680,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
return null;
}
- if (!_manager.TryGetConnection(connectionId, out var connection))
+ if (!_manager.TryGetConnection(connectionToken, out var connection))
{
// No connection with that ID: Not Found
context.Response.StatusCode = StatusCodes.Status404NotFound;
@@ -634,15 +695,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
// This is only used for WebSockets connections, which can connect directly without negotiating
private async Task GetOrCreateConnectionAsync(HttpContext context, HttpConnectionDispatcherOptions options)
{
- var connectionId = GetConnectionId(context);
+ var connectionToken = GetConnectionToken(context);
HttpConnectionContext connection;
// There's no connection id so this is a brand new connection
- if (StringValues.IsNullOrEmpty(connectionId))
+ if (StringValues.IsNullOrEmpty(connectionToken))
{
connection = CreateConnection(options);
}
- else if (!_manager.TryGetConnection(connectionId, out connection))
+ else if (!_manager.TryGetConnection(connectionToken, out connection))
{
// No connection with that ID: Not Found
context.Response.StatusCode = StatusCodes.Status404NotFound;
@@ -653,12 +714,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
return connection;
}
- private HttpConnectionContext CreateConnection(HttpConnectionDispatcherOptions options)
+ private HttpConnectionContext CreateConnection(HttpConnectionDispatcherOptions options, int clientProtocolVersion = 0)
{
var transportPipeOptions = new PipeOptions(pauseWriterThreshold: options.TransportMaxBufferSize, resumeWriterThreshold: options.TransportMaxBufferSize / 2, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false);
var appPipeOptions = new PipeOptions(pauseWriterThreshold: options.ApplicationMaxBufferSize, resumeWriterThreshold: options.ApplicationMaxBufferSize / 2, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false);
-
- return _manager.CreateConnection(transportPipeOptions, appPipeOptions);
+ return _manager.CreateConnection(transportPipeOptions, appPipeOptions, clientProtocolVersion);
}
private class EmptyServiceProvider : IServiceProvider
diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs
index dda35866a4..4a97681fc0 100644
--- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs
+++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs
@@ -78,18 +78,28 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
/// Creates a connection without Pipes setup to allow saving allocations until Pipes are needed.
///
///
- internal HttpConnectionContext CreateConnection(PipeOptions transportPipeOptions, PipeOptions appPipeOptions)
+ internal HttpConnectionContext CreateConnection(PipeOptions transportPipeOptions, PipeOptions appPipeOptions, int negotiateVersion = 0)
{
+ string connectionToken;
var id = MakeNewConnectionId();
+ if (negotiateVersion > 0)
+ {
+ connectionToken = MakeNewConnectionId();
+ }
+ else
+ {
+ connectionToken = id;
+ }
Log.CreatedNewConnection(_logger, id);
var connectionTimer = HttpConnectionsEventSource.Log.ConnectionStart(id);
- var connection = new HttpConnectionContext(id, _connectionLogger);
+ var connection = new HttpConnectionContext(id, connectionToken, _connectionLogger);
var pair = DuplexPipe.CreateConnectionPair(transportPipeOptions, appPipeOptions);
connection.Transport = pair.Application;
connection.Application = pair.Transport;
- _connections.TryAdd(id, (connection, connectionTimer));
+ _connections.TryAdd(connectionToken, (connection, connectionTimer));
+
return connection;
}
@@ -205,7 +215,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
{
// Remove it from the list after disposal so that's it's easy to see
// connections that might be in a hung state via the connections list
- RemoveConnection(connection.ConnectionId);
+ RemoveConnection(connection.ConnectionToken);
}
}
}
diff --git a/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs b/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs
index ef54b75288..d815b25e54 100644
--- a/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs
+++ b/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs
@@ -155,7 +155,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal.Transports
var memory = _application.Output.GetMemory();
var receiveResult = await socket.ReceiveAsync(memory, token);
- // Need to check again for netcoreapp3.1 because a close can happen between a 0-byte read and the actual read
+ // Need to check again for netcoreapp5.0 because a close can happen between a 0-byte read and the actual read
if (receiveResult.MessageType == WebSocketMessageType.Close)
{
return;
diff --git a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs
index 7b164b8929..6e28f47cc6 100644
--- a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs
+++ b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs
@@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
public class HttpConnectionDispatcherTests : VerifiableLoggedTest
{
[Fact]
- public async Task NegotiateReservesConnectionIdAndReturnsIt()
+ public async Task NegotiateVersionZeroReservesConnectionIdAndReturnsIt()
{
using (StartVerifiableLog())
{
@@ -54,8 +54,35 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions());
var negotiateResponse = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(ms.ToArray()));
var connectionId = negotiateResponse.Value("connectionId");
- Assert.True(manager.TryGetConnection(connectionId, out var connectionContext));
+ var connectionToken = negotiateResponse.Value("connectionToken");
+ Assert.Null(connectionToken);
+ Assert.NotNull(connectionId);
+ }
+ }
+
+ [Fact]
+ public async Task NegotiateReservesConnectionTokenAndConnectionIdAndReturnsIt()
+ {
+ using (StartVerifiableLog())
+ {
+ var manager = CreateConnectionManager(LoggerFactory);
+ var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory);
+ var context = new DefaultHttpContext();
+ var services = new ServiceCollection();
+ services.AddSingleton();
+ services.AddOptions();
+ var ms = new MemoryStream();
+ context.Request.Path = "/foo";
+ context.Request.Method = "POST";
+ context.Response.Body = ms;
+ context.Request.QueryString = new QueryString("?negotiateVersion=1");
+ await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions());
+ var negotiateResponse = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(ms.ToArray()));
+ var connectionId = negotiateResponse.Value("connectionId");
+ var connectionToken = negotiateResponse.Value("connectionToken");
+ Assert.True(manager.TryGetConnection(connectionToken, out var connectionContext));
Assert.Equal(connectionId, connectionContext.ConnectionId);
+ Assert.NotEqual(connectionId, connectionToken);
}
}
@@ -74,12 +101,13 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
context.Response.Body = ms;
+ context.Request.QueryString = new QueryString("?negotiateVersion=1");
var options = new HttpConnectionDispatcherOptions { TransportMaxBufferSize = 4, ApplicationMaxBufferSize = 4 };
await dispatcher.ExecuteNegotiateAsync(context, options);
var negotiateResponse = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(ms.ToArray()));
- var connectionId = negotiateResponse.Value("connectionId");
- context.Request.QueryString = context.Request.QueryString.Add("id", connectionId);
- Assert.True(manager.TryGetConnection(connectionId, out var connection));
+ var connectionToken = negotiateResponse.Value("connectionToken");
+ context.Request.QueryString = context.Request.QueryString.Add("id", connectionToken);
+ Assert.True(manager.TryGetConnection(connectionToken, out var connection));
// Fake actual connection after negotiate to populate the pipes on the connection
await dispatcher.ExecuteAsync(context, options, c => Task.CompletedTask);
@@ -95,6 +123,62 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
}
}
+ [Fact]
+ public async Task InvalidNegotiateProtocolVersionThrows()
+ {
+ using (StartVerifiableLog())
+ {
+ var manager = CreateConnectionManager(LoggerFactory);
+ var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory);
+ var context = new DefaultHttpContext();
+ var services = new ServiceCollection();
+ services.AddSingleton();
+ services.AddOptions();
+ var ms = new MemoryStream();
+ context.Request.Path = "/foo";
+ context.Request.Method = "POST";
+ context.Response.Body = ms;
+ context.Request.QueryString = new QueryString("?negotiateVersion=Invalid");
+ var options = new HttpConnectionDispatcherOptions { TransportMaxBufferSize = 4, ApplicationMaxBufferSize = 4 };
+ await dispatcher.ExecuteNegotiateAsync(context, options);
+ var negotiateResponse = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(ms.ToArray()));
+
+ var error = negotiateResponse.Value("error");
+ Assert.Equal("The client requested an invalid protocol version 'Invalid'", error);
+
+ var connectionId = negotiateResponse.Value("connectionId");
+ Assert.Null(connectionId);
+ }
+ }
+
+ [Fact]
+ public async Task NoNegotiateVersionInQueryStringThrowsWhenMinProtocolVersionIsSet()
+ {
+ using (StartVerifiableLog())
+ {
+ var manager = CreateConnectionManager(LoggerFactory);
+ var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory);
+ var context = new DefaultHttpContext();
+ var services = new ServiceCollection();
+ services.AddSingleton();
+ services.AddOptions();
+ var ms = new MemoryStream();
+ context.Request.Path = "/foo";
+ context.Request.Method = "POST";
+ context.Response.Body = ms;
+ context.Request.QueryString = new QueryString("");
+ var options = new HttpConnectionDispatcherOptions { TransportMaxBufferSize = 4, ApplicationMaxBufferSize = 4, MinimumProtocolVersion = 1 };
+ await dispatcher.ExecuteNegotiateAsync(context, options);
+ var negotiateResponse = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(ms.ToArray()));
+
+ var error = negotiateResponse.Value("error");
+ Assert.Equal("The client requested version '0', but the server does not support this version.", error);
+
+ var connectionId = negotiateResponse.Value("connectionId");
+ Assert.Null(connectionId);
+ }
+ }
+
[Theory]
[InlineData(HttpTransportType.LongPolling)]
[InlineData(HttpTransportType.ServerSentEvents)]
@@ -125,7 +209,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary();
- values["id"] = connection.ConnectionId;
+ values["id"] = connection.ConnectionToken;
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@@ -166,6 +251,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
context.Response.Body = ms;
+ context.Request.QueryString = new QueryString("?negotiateVersion=1");
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions { Transports = transports });
var negotiateResponse = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(ms.ToArray()));
@@ -204,6 +290,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Method = "GET";
var values = new Dictionary();
values["id"] = "unknown";
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
SetTransport(context, transportType);
@@ -240,6 +327,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Method = "POST";
var values = new Dictionary();
values["id"] = "unknown";
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@@ -276,7 +364,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary();
- values["id"] = connection.ConnectionId;
+ values["id"] = connection.ConnectionToken;
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@@ -315,6 +404,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Method = "POST";
var values = new Dictionary();
values["id"] = connection.ConnectionId;
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@@ -354,7 +444,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "GET";
var values = new Dictionary();
- values["id"] = connection.ConnectionId;
+ values["id"] = connection.ConnectionToken;
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@@ -415,7 +506,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "GET";
var values = new Dictionary();
- values["id"] = connection.ConnectionId;
+ values["id"] = connection.ConnectionToken;
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@@ -481,7 +573,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary();
- values["id"] = connection.ConnectionId;
+ values["id"] = connection.ConnectionToken;
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@@ -544,6 +637,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Method = "POST";
var values = new Dictionary();
values["id"] = connection.ConnectionId;
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@@ -613,7 +707,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "GET";
var values = new Dictionary();
- values["id"] = connection.ConnectionId;
+ values["id"] = connection.ConnectionToken;
+ values["negotiateVersion"] = "1";
values["another"] = "value";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@@ -661,8 +756,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
var connectionHttpContext = connection.GetHttpContext();
Assert.NotNull(connectionHttpContext);
- Assert.Equal(2, connectionHttpContext.Request.Query.Count);
- Assert.Equal(connection.ConnectionId, connectionHttpContext.Request.Query["id"]);
+ Assert.Equal(3, connectionHttpContext.Request.Query.Count);
Assert.Equal("value", connectionHttpContext.Request.Query["another"]);
Assert.Equal(3, connectionHttpContext.Request.Headers.Count);
@@ -706,6 +800,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
services.AddSingleton();
context.Request.Path = "/foo";
context.Request.Method = "GET";
+ context.Request.QueryString = new QueryString("?negotiateVersion=1");
SetTransport(context, transportType);
@@ -748,7 +843,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary();
- values["id"] = connection.ConnectionId;
+ values["id"] = connection.ConnectionToken;
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@@ -775,6 +871,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
services.AddSingleton();
context.Request.Path = "/foo";
context.Request.Method = "POST";
+ context.Request.QueryString = new QueryString("?negotiateVersion=1");
var builder = new ConnectionBuilder(services.BuildServiceProvider());
builder.UseConnectionHandler();
@@ -846,6 +943,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory);
var context = MakeRequest("/foo", connection);
+
SetTransport(context, HttpTransportType.ServerSentEvents);
var services = new ServiceCollection();
@@ -857,7 +955,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
- var exists = manager.TryGetConnection(connection.ConnectionId, out _);
+ var exists = manager.TryGetConnection(connection.ConnectionToken, out _);
Assert.False(exists);
}
}
@@ -1221,7 +1319,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
await task;
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
- var exists = manager.TryGetConnection(connection.ConnectionId, out _);
+ var exists = manager.TryGetConnection(connection.ConnectionToken, out _);
Assert.False(exists);
}
}
@@ -1262,7 +1360,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
await task;
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
- var exists = manager.TryGetConnection(connection.ConnectionId, out _);
+ var exists = manager.TryGetConnection(connection.ConnectionToken, out _);
Assert.False(exists);
}
}
@@ -1364,10 +1462,10 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Method = "GET";
context.RequestServices = sp;
var values = new Dictionary();
- values["id"] = connection.ConnectionId;
+ values["id"] = connection.ConnectionToken;
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
-
var builder = new ConnectionBuilder(sp);
builder.UseConnectionHandler();
var app = builder.Build();
@@ -1452,7 +1550,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
// Issue the delete request
var deleteContext = new DefaultHttpContext();
deleteContext.Request.Path = "/foo";
- deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionId}");
+ deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionToken}");
deleteContext.Request.Method = "DELETE";
var ms = new MemoryStream();
deleteContext.Response.Body = ms;
@@ -1495,7 +1593,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
// Issue the delete request and make sure the poll completes
var deleteContext = new DefaultHttpContext();
deleteContext.Request.Path = "/foo";
- deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionId}");
+ deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionToken}");
deleteContext.Request.Method = "DELETE";
Assert.False(pollTask.IsCompleted);
@@ -1513,7 +1611,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
Assert.Equal("text/plain", deleteContext.Response.ContentType);
// Verify the connection was removed from the manager
- Assert.False(manager.TryGetConnection(connection.ConnectionId, out _));
+ Assert.False(manager.TryGetConnection(connection.ConnectionToken, out _));
}
}
@@ -1543,7 +1641,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
// Issue the delete request and make sure the poll completes
var deleteContext = new DefaultHttpContext();
deleteContext.Request.Path = "/foo";
- deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionId}");
+ deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionToken}");
deleteContext.Request.Method = "DELETE";
await dispatcher.ExecuteAsync(deleteContext, options, app).OrTimeout();
@@ -1561,7 +1659,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
await connection.DisposeAndRemoveTask.OrTimeout();
// Verify the connection was removed from the manager
- Assert.False(manager.TryGetConnection(connection.ConnectionId, out _));
+ Assert.False(manager.TryGetConnection(connection.ConnectionToken, out _));
}
}
@@ -1581,6 +1679,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
context.Response.Body = ms;
+ context.Request.QueryString = new QueryString("?negotiateVersion=1");
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions { Transports = HttpTransportType.WebSockets });
var negotiateResponse = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(ms.ToArray()));
@@ -1637,7 +1736,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary();
- values["id"] = connection.ConnectionId;
+ values["id"] = connection.ConnectionToken;
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
var buffer = Encoding.UTF8.GetBytes("Hello, world");
@@ -1693,7 +1793,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary();
- values["id"] = connection.ConnectionId;
+ values["id"] = connection.ConnectionToken;
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
var buffer = Encoding.UTF8.GetBytes("Hello, world");
@@ -1746,7 +1847,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary();
- values["id"] = connection.ConnectionId;
+ values["id"] = connection.ConnectionToken;
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
var buffer = Encoding.UTF8.GetBytes("Hello, world");
@@ -1808,7 +1910,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
await pollTask.OrTimeout();
Assert.Equal(StatusCodes.Status500InternalServerError, pollContext.Response.StatusCode);
- Assert.False(manager.TryGetConnection(connection.ConnectionId, out var _));
+ Assert.False(manager.TryGetConnection(connection.ConnectionToken, out var _));
}
}
@@ -1831,7 +1933,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "GET";
var values = new Dictionary();
- values["id"] = connection.ConnectionId;
+ values["id"] = connection.ConnectionToken;
+ values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@@ -1853,14 +1956,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
}
}
- private static DefaultHttpContext MakeRequest(string path, ConnectionContext connection, string format = null)
+ private static DefaultHttpContext MakeRequest(string path, HttpConnectionContext connection, string format = null)
{
var context = new DefaultHttpContext();
context.Features.Set(new ResponseFeature());
context.Request.Path = path;
context.Request.Method = "GET";
var values = new Dictionary();
- values["id"] = connection.ConnectionId;
+ values["id"] = connection.ConnectionToken;
+ values["negotiateVersion"] = "1";
if (format != null)
{
values["format"] = format;
diff --git a/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs b/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs
index 5c30a490f7..ade605b08a 100644
--- a/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs
+++ b/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs
@@ -131,7 +131,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
Assert.NotNull(connection.ConnectionId);
- Assert.True(connectionManager.TryGetConnection(connection.ConnectionId, out var newConnection));
+ Assert.True(connectionManager.TryGetConnection(connection.ConnectionToken, out var newConnection));
Assert.Same(newConnection, connection);
}
}
@@ -143,13 +143,13 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
{
var connectionManager = CreateConnectionManager(LoggerFactory);
var connection = connectionManager.CreateConnection(PipeOptions.Default, PipeOptions.Default);
-
var transport = connection.Transport;
Assert.NotNull(connection.ConnectionId);
+ Assert.NotNull(connection.ConnectionToken);
Assert.NotNull(transport);
- Assert.True(connectionManager.TryGetConnection(connection.ConnectionId, out var newConnection));
+ Assert.True(connectionManager.TryGetConnection(connection.ConnectionToken, out var newConnection));
Assert.Same(newConnection, connection);
Assert.Same(transport, newConnection.Transport);
}
@@ -168,12 +168,55 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
Assert.NotNull(connection.ConnectionId);
Assert.NotNull(transport);
- Assert.True(connectionManager.TryGetConnection(connection.ConnectionId, out var newConnection));
+ Assert.True(connectionManager.TryGetConnection(connection.ConnectionToken, out var newConnection));
Assert.Same(newConnection, connection);
Assert.Same(transport, newConnection.Transport);
- connectionManager.RemoveConnection(connection.ConnectionId);
- Assert.False(connectionManager.TryGetConnection(connection.ConnectionId, out newConnection));
+ connectionManager.RemoveConnection(connection.ConnectionToken);
+ Assert.False(connectionManager.TryGetConnection(connection.ConnectionToken, out newConnection));
+ }
+ }
+
+ [Fact]
+ public void ConnectionIdAndConnectionTokenAreTheSameForNegotiateVersionZero()
+ {
+ using (StartVerifiableLog())
+ {
+ var connectionManager = CreateConnectionManager(LoggerFactory);
+ var connection = connectionManager.CreateConnection(PipeOptions.Default, PipeOptions.Default, negotiateVersion: 0);
+
+ var transport = connection.Transport;
+
+ Assert.NotNull(connection.ConnectionId);
+ Assert.NotNull(transport);
+
+ Assert.True(connectionManager.TryGetConnection(connection.ConnectionToken, out var newConnection));
+ Assert.Same(newConnection, connection);
+ Assert.Same(transport, newConnection.Transport);
+ Assert.Equal(connection.ConnectionId, connection.ConnectionToken);
+
+ }
+ }
+
+ [Fact]
+ public void ConnectionIdAndConnectionTokenAreDifferentForNegotiateVersionOne()
+ {
+ using (StartVerifiableLog())
+ {
+ var connectionManager = CreateConnectionManager(LoggerFactory);
+ var connection = connectionManager.CreateConnection(PipeOptions.Default, PipeOptions.Default, negotiateVersion: 1);
+
+ var transport = connection.Transport;
+
+ Assert.NotNull(connection.ConnectionId);
+ Assert.NotNull(transport);
+
+ Assert.True(connectionManager.TryGetConnection(connection.ConnectionToken, out var newConnection));
+ Assert.False(connectionManager.TryGetConnection(connection.ConnectionId, out var _));
+ Assert.Same(newConnection, connection);
+ Assert.Same(transport, newConnection.Transport);
+ Assert.NotEqual(connection.ConnectionId, connection.ConnectionToken);
+
}
}
diff --git a/src/SignalR/common/Http.Connections/test/NegotiateProtocolTests.cs b/src/SignalR/common/Http.Connections/test/NegotiateProtocolTests.cs
index e92d3c3b42..00d803ffdd 100644
--- a/src/SignalR/common/Http.Connections/test/NegotiateProtocolTests.cs
+++ b/src/SignalR/common/Http.Connections/test/NegotiateProtocolTests.cs
@@ -13,12 +13,20 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
public class NegotiateProtocolTests
{
[Theory]
- [InlineData("{\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null)]
- [InlineData("{\"connectionId\":\"\",\"availableTransports\":[]}", "", new string[0], null, null)]
- [InlineData("{\"url\": \"http://foo.com/chat\"}", null, null, "http://foo.com/chat", null)]
- [InlineData("{\"url\": \"http://foo.com/chat\", \"accessToken\": \"token\"}", null, null, "http://foo.com/chat", "token")]
- [InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"transport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null)]
- public void ParsingNegotiateResponseMessageSuccessForValid(string json, string connectionId, string[] availableTransports, string url, string accessToken)
+ [InlineData("{\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null, 0, null)]
+ [InlineData("{\"connectionId\":\"\",\"availableTransports\":[]}", "", new string[0], null, null, 0, null)]
+ [InlineData("{\"url\": \"http://foo.com/chat\"}", null, null, "http://foo.com/chat", null, 0, null)]
+ [InlineData("{\"url\": \"http://foo.com/chat\", \"accessToken\": \"token\"}", null, null, "http://foo.com/chat", "token", 0, null)]
+ [InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"transport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null, 0, null)]
+ [InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"\\u0074ransport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null, 0, null)]
+ [InlineData("{\"negotiateVersion\":123,\"connectionId\":\"123\",\"connectionToken\":\"789\",\"availableTransports\":[{\"\\u0074ransport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null, 123, "789")]
+ [InlineData("{\"negotiateVersion\":123,\"negotiateVersion\":321, \"connectionToken\":\"789\",\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null, 321, "789")]
+ [InlineData("{\"ignore\":123,\"negotiateVersion\":123, \"connectionToken\":\"789\",\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null, 123, "789")]
+ [InlineData("{\"connectionId\":\"123\",\"availableTransports\":[],\"negotiateVersion\":123, \"connectionToken\":\"789\"}", "123", new string[0], null, null, 123, "789")]
+ [InlineData("{\"connectionId\":\"123\",\"connectionToken\":\"789\",\"availableTransports\":[]}", "123", new string[0], null, null, 0, "789")]
+ [InlineData("{\"connectionToken\":\"789\",\"connectionId\":\"123\",\"availableTransports\":[],\"negotiateVersion\":123}", "123", new string[0], null, null, 123, "789")]
+ [InlineData("{\"connectionToken\":\"789\",\"connectionId\":\"123\",\"availableTransports\":[],\"negotiateVersion\":123, \"connectionToken\":\"987\"}", "123", new string[0], null, null, 123, "987")]
+ public void ParsingNegotiateResponseMessageSuccessForValid(string json, string connectionId, string[] availableTransports, string url, string accessToken, int version, string connectionToken)
{
var responseData = Encoding.UTF8.GetBytes(json);
var response = NegotiateProtocol.ParseResponse(responseData);
@@ -27,6 +35,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
Assert.Equal(availableTransports?.Length, response.AvailableTransports?.Count);
Assert.Equal(url, response.Url);
Assert.Equal(accessToken, response.AccessToken);
+ Assert.Equal(version, response.Version);
+ Assert.Equal(connectionToken, response.ConnectionToken);
if (response.AvailableTransports != null)
{
@@ -44,6 +54,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":null}", "Unexpected JSON Token Type 'Null'. Expected a JSON Array.")]
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"transferFormats\":[]}]}", "Missing required property 'transport'.")]
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"transport\":\"test\"}]}", "Missing required property 'transferFormats'.")]
+ [InlineData("{\"connectionId\":\"123\",\"negotiateVersion\":123,\"availableTransports\":[]}", "Missing required property 'connectionToken'.")]
public void ParsingNegotiateResponseMessageThrowsForInvalid(string payload, string expectedMessage)
{
var responseData = Encoding.UTF8.GetBytes(payload);
@@ -82,7 +93,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
string json = Encoding.UTF8.GetString(writer.ToArray());
- Assert.Equal("{\"availableTransports\":[]}", json);
+ Assert.Equal("{\"negotiateVersion\":0,\"availableTransports\":[]}", json);
}
}
@@ -101,7 +112,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
string json = Encoding.UTF8.GetString(writer.ToArray());
- Assert.Equal("{\"availableTransports\":[{\"transport\":null,\"transferFormats\":[]}]}", json);
+ Assert.Equal("{\"negotiateVersion\":0,\"availableTransports\":[{\"transport\":null,\"transferFormats\":[]}]}", json);
}
}
}
diff --git a/src/SignalR/common/Protocols.Json/src/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj b/src/SignalR/common/Protocols.Json/src/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj
index 27ac41a9df..2dd22c44a5 100644
--- a/src/SignalR/common/Protocols.Json/src/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj
+++ b/src/SignalR/common/Protocols.Json/src/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs b/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs
index 884e427b68..28596c6210 100644
--- a/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs
+++ b/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.ExceptionServices;
+using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Internal;
@@ -551,19 +552,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
writer.WriteStartArray(ArgumentsPropertyNameBytes);
foreach (var argument in arguments)
{
- var type = argument?.GetType();
- if (type == typeof(DateTime))
- {
- writer.WriteStringValue((DateTime)argument);
- }
- else if (type == typeof(DateTimeOffset))
- {
- writer.WriteStringValue((DateTimeOffset)argument);
- }
- else
- {
- JsonSerializer.Serialize(writer, argument, type, _payloadSerializerOptions);
- }
+ JsonSerializer.Serialize(writer, argument, argument?.GetType(), _payloadSerializerOptions);
}
writer.WriteEndArray();
}
@@ -746,19 +735,20 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
internal static JsonSerializerOptions CreateDefaultSerializerSettings()
{
- var options = new JsonSerializerOptions();
- options.WriteIndented = false;
- options.ReadCommentHandling = JsonCommentHandling.Disallow;
- options.AllowTrailingCommas = false;
- options.IgnoreNullValues = false;
- options.IgnoreReadOnlyProperties = false;
- options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
- options.PropertyNameCaseInsensitive = true;
- options.MaxDepth = 64;
- options.DictionaryKeyPolicy = null;
- options.DefaultBufferSize = 16 * 1024;
-
- return options;
+ return new JsonSerializerOptions()
+ {
+ WriteIndented = false,
+ ReadCommentHandling = JsonCommentHandling.Disallow,
+ AllowTrailingCommas = false,
+ IgnoreNullValues = false,
+ IgnoreReadOnlyProperties = false,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ PropertyNameCaseInsensitive = true,
+ MaxDepth = 64,
+ DictionaryKeyPolicy = null,
+ DefaultBufferSize = 16 * 1024,
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+ };
}
}
}
diff --git a/src/SignalR/common/Shared/ReusableUtf8JsonWriter.cs b/src/SignalR/common/Shared/ReusableUtf8JsonWriter.cs
index 1dc980d750..c05c0397e6 100644
--- a/src/SignalR/common/Shared/ReusableUtf8JsonWriter.cs
+++ b/src/SignalR/common/Shared/ReusableUtf8JsonWriter.cs
@@ -3,6 +3,7 @@
using System;
using System.Buffers;
+using System.Text.Encodings.Web;
using System.Text.Json;
namespace Microsoft.AspNetCore.Internal
@@ -20,7 +21,13 @@ namespace Microsoft.AspNetCore.Internal
public ReusableUtf8JsonWriter(IBufferWriter stream)
{
- _writer = new Utf8JsonWriter(stream, new JsonWriterOptions() { SkipValidation = true });
+ _writer = new Utf8JsonWriter(stream, new JsonWriterOptions()
+ {
+#if !DEBUG
+ SkipValidation = true,
+#endif
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ });
}
public static ReusableUtf8JsonWriter Get(IBufferWriter stream)
diff --git a/src/SignalR/common/SignalR.Common/src/Microsoft.AspNetCore.SignalR.Common.csproj b/src/SignalR/common/SignalR.Common/src/Microsoft.AspNetCore.SignalR.Common.csproj
index fa97b3e082..eab77e2bf1 100644
--- a/src/SignalR/common/SignalR.Common/src/Microsoft.AspNetCore.SignalR.Common.csproj
+++ b/src/SignalR/common/SignalR.Common/src/Microsoft.AspNetCore.SignalR.Common.csproj
@@ -29,7 +29,7 @@
-
+
diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/HandshakeProtocolTests.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/HandshakeProtocolTests.cs
index 27560c502a..9a88d32a57 100644
--- a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/HandshakeProtocolTests.cs
+++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/HandshakeProtocolTests.cs
@@ -15,6 +15,12 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
[InlineData("{\"protocol\":\"dummy\",\"version\":1}\u001e", "dummy", 1)]
[InlineData("{\"protocol\":\"\",\"version\":10}\u001e", "", 10)]
[InlineData("{\"protocol\":\"\",\"version\":10,\"unknown\":null}\u001e", "", 10)]
+ [InlineData("{\"protocol\":\"firstProtocol\",\"protocol\":\"secondProtocol\",\"version\":1}\u001e", "secondProtocol", 1)]
+ [InlineData("{\"protocol\":\"firstProtocol\",\"protocol\":\"secondProtocol\",\"version\":1,\"version\":75}\u001e", "secondProtocol", 75)]
+ [InlineData("{\"protocol\":\"dummy\",\"version\":1,\"ignoredField\":99}\u001e", "dummy", 1)]
+ [InlineData("{\"protocol\":\"dummy\",\"version\":1}{\"protocol\":\"wrong\",\"version\":99}\u001e", "dummy", 1)]
+ [InlineData("{\"protocol\":\"\\u0064ummy\",\"version\":1}\u001e", "dummy", 1)]
+ [InlineData("{\"\\u0070rotoco\\u006c\":\"\\u0064ummy\",\"version\":1}\u001e", "dummy", 1)]
public void ParsingHandshakeRequestMessageSuccessForValidMessages(string json, string protocol, int version)
{
var message = new ReadOnlySequence(Encoding.UTF8.GetBytes(json));
diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTests.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTests.cs
index a2f696ab17..6f9ba5cb60 100644
--- a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTests.cs
+++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTests.cs
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
+using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.SignalR.Protocol;
@@ -28,7 +29,8 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
PayloadSerializerOptions = new JsonSerializerOptions()
{
IgnoreNullValues = ignoreNullValues,
- PropertyNamingPolicy = useCamelCase ? JsonNamingPolicy.CamelCase : null
+ PropertyNamingPolicy = useCamelCase ? JsonNamingPolicy.CamelCase : null,
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
}
};
@@ -39,6 +41,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
[InlineData("", "Error reading JSON.")]
[InlineData("42", "Unexpected JSON Token Type 'Number'. Expected a JSON Object.")]
[InlineData("{\"type\":\"foo\"}", "Expected 'type' to be of type Number.")]
+ [InlineData("{\"type\":3,\"invocationId\":\"42\",\"result\":true", "Error reading JSON.")]
public void CustomInvalidMessages(string input, string expectedMessage)
{
input = Frame(input);
@@ -101,98 +104,18 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
Assert.Equal(expectedMessage, message);
}
- [Fact]
- public void ReadCaseInsensitivePropertiesByDefault()
- {
- var input = Frame("{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StrIngProp\":\"test\",\"DoublePrOp\":3.14159,\"IntProp\":43,\"DateTimeProp\":\"2019-06-03T22:00:00\",\"NuLLProp\":null,\"ByteARRProp\":\"AgQG\"}}");
-
- var binder = new TestBinder(null, typeof(TemporaryCustomObject));
- var data = new ReadOnlySequence(Encoding.UTF8.GetBytes(input));
- JsonHubProtocol.TryParseMessage(ref data, binder, out var message);
-
- var streamItemMessage = Assert.IsType(message);
- Assert.Equal(new TemporaryCustomObject()
- {
- ByteArrProp = new byte[] { 2, 4, 6 },
- IntProp = 43,
- DoubleProp = 3.14159,
- StringProp = "test",
- DateTimeProp = DateTime.Parse("6/3/2019 10:00:00 PM")
- }, streamItemMessage.Item);
- }
-
public static IDictionary CustomProtocolTestData => new[]
{
new JsonProtocolTestData("InvocationMessage_HasFloatArgument", new InvocationMessage(null, "Target", new object[] { 1, "Foo", 2.0f }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[1,\"Foo\",2]}"),
- new JsonProtocolTestData("InvocationMessage_StringIsoDateArgument", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), true, true, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20\\u002B12:34\"]}"),
- new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNoCamelCase", new InvocationMessage(null, "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), false, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"),
- new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnore", new InvocationMessage(null, "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"),
- new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new InvocationMessage(null, "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), false, false, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"),
- new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueInclude", new InvocationMessage(null, "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), true, false, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
new JsonProtocolTestData("InvocationMessage_HasHeaders", AddHeaders(TestHeaders, new InvocationMessage("123", "Target", new object[] { 1, "Foo", 2.0f })), true, true, "{\"type\":1," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2]}"),
- new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNoCamelCase", new StreamItemMessage("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), false, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnore", new StreamItemMessage("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnoreAndNoCamelCase", new StreamItemMessage("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), false, false, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueInclude", new StreamItemMessage("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), true, false, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
new JsonProtocolTestData("StreamItemMessage_HasFloatItem", new StreamItemMessage("123", 2.0f), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":2}"),
- new JsonProtocolTestData("StreamItemMessage_HasHeaders", AddHeaders(TestHeaders, new StreamItemMessage("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } })), true, false, "{\"type\":2," + SerializedHeaders + ",\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
new JsonProtocolTestData("CompletionMessage_HasFloatResult", CompletionMessage.WithResult("123", 2.0f), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":2}"),
- new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNoCamelCase", CompletionMessage.WithResult("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), false, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIgnore", CompletionMessage.WithResult("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIncludeAndNoCamelCase", CompletionMessage.WithResult("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), false, false, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueInclude", CompletionMessage.WithResult("123", new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } }), true, false, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("CompletionMessage_HasErrorAndCamelCase", CompletionMessage.Empty("123"), true, true, "{\"type\":3,\"invocationId\":\"123\"}"),
- new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), false, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"),
- new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnore", new StreamInvocationMessage("123", "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"),
- new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), false, false, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"),
- new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueInclude", new StreamInvocationMessage("123", "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } }), true, false, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
new JsonProtocolTestData("StreamInvocationMessage_HasFloatArgument", new StreamInvocationMessage("123", "Target", new object[] { 1, "Foo", 2.0f }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2]}"),
- new JsonProtocolTestData("StreamInvocationMessage_HasHeaders", AddHeaders(TestHeaders, new StreamInvocationMessage("123", "Target", new object[] { new TemporaryCustomObject() { ByteArrProp = new byte[] { 1, 2, 3 } } })), true, false, "{\"type\":4," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
}.ToDictionary(t => t.Name);
public static IEnumerable CustomProtocolTestDataNames => CustomProtocolTestData.Keys.Select(name => new object[] { name });
}
-
- // Revert back to CustomObject when initial values on arrays are supported
- // e.g. byte[] arr { get; set; } = byte[] { 1, 2, 3 };
- internal class TemporaryCustomObject : IEquatable
- {
- // Not intended to be a full set of things, just a smattering of sample serializations
- public string StringProp { get; set; } = "SignalR!";
-
- public double DoubleProp { get; set; } = 6.2831853071;
-
- public int IntProp { get; set; } = 42;
-
- public DateTime DateTimeProp { get; set; } = new DateTime(2017, 4, 11, 0, 0, 0, DateTimeKind.Utc);
-
- public object NullProp { get; set; } = null;
-
- public byte[] ByteArrProp { get; set; }
-
- public override bool Equals(object obj)
- {
- return obj is TemporaryCustomObject o && Equals(o);
- }
-
- public override int GetHashCode()
- {
- // This is never used in a hash table
- return 0;
- }
-
- public bool Equals(TemporaryCustomObject right)
- {
- // This allows the comparer below to properly compare the object in the test.
- return string.Equals(StringProp, right.StringProp, StringComparison.Ordinal) &&
- DoubleProp == right.DoubleProp &&
- IntProp == right.IntProp &&
- DateTime.Equals(DateTimeProp, right.DateTimeProp) &&
- NullProp == right.NullProp &&
- System.Linq.Enumerable.SequenceEqual(ByteArrProp, right.ByteArrProp);
- }
- }
}
diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTestsBase.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTestsBase.cs
index d0ef4a6e7f..1570b54462 100644
--- a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTestsBase.cs
+++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/JsonHubProtocolTestsBase.cs
@@ -40,12 +40,30 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
new JsonProtocolTestData("InvocationMessage_HasStreamAndNormalArgument", new InvocationMessage(null, "Target", new object[] { 42 }, new string[] { "__test_id__" }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[42],\"streamIds\":[\"__test_id__\"]}"),
new JsonProtocolTestData("InvocationMessage_HasMultipleStreams", new InvocationMessage(null, "Target", Array.Empty(), new string[] { "__test_id__", "__test_id2__" }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[],\"streamIds\":[\"__test_id__\",\"__test_id2__\"]}"),
new JsonProtocolTestData("InvocationMessage_DateTimeOffsetArgument", new InvocationMessage("Method", new object[] { DateTimeOffset.Parse("2016-05-10T13:51:20+12:34") }), true, true, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20+12:34\"]}"),
+ new JsonProtocolTestData("InvocationMessage_StringIsoDateArgument", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), true, true, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20+12:34\"]}"),
+ new JsonProtocolTestData("InvocationMessage_HasNonAsciiArgument", new InvocationMessage("Method", new object[] { "מחרוזת כלשהי" }), true, true, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"מחרוזת כלשהי\"]}"),
+ new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNoCamelCase", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), false, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"),
+ new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnore", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"),
+ new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), false, false, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"),
+ new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueInclude", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, false, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
+ new JsonProtocolTestData("StreamItemMessage_HasHeaders", AddHeaders(TestHeaders, new StreamItemMessage("123", new CustomObject())), true, false, "{\"type\":2," + SerializedHeaders + ",\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
+ new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNoCamelCase", new StreamItemMessage("123", new CustomObject()), false, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"),
+ new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnore", new StreamItemMessage("123", new CustomObject()), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"),
+ new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnoreAndNoCamelCase", new StreamItemMessage("123", new CustomObject()), false, false, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"),
+ new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueInclude", new StreamItemMessage("123", new CustomObject()), true, false, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
new JsonProtocolTestData("StreamItemMessage_HasIntegerItem", new StreamItemMessage("123", 1), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":1}"),
new JsonProtocolTestData("StreamItemMessage_HasStringItem", new StreamItemMessage("123", "Foo"), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":\"Foo\"}"),
new JsonProtocolTestData("StreamItemMessage_HasBoolItem", new StreamItemMessage("123", true), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":true}"),
new JsonProtocolTestData("StreamItemMessage_HasNullItem", new StreamItemMessage("123", null), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":null}"),
+ new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNoCamelCase", CompletionMessage.WithResult("123", new CustomObject()), false, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"),
+ new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIgnore", CompletionMessage.WithResult("123", new CustomObject()), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"),
+ new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIncludeAndNoCamelCase", CompletionMessage.WithResult("123", new CustomObject()), false, false, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"),
+ new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueInclude", CompletionMessage.WithResult("123", new CustomObject()), true, false, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
+ new JsonProtocolTestData("CompletionMessage_HasErrorAndCamelCase", CompletionMessage.Empty("123"), true, true, "{\"type\":3,\"invocationId\":\"123\"}"),
+ new JsonProtocolTestData("CompletionMessage_HasTestHeadersAndCustomItemResult", AddHeaders(TestHeaders, CompletionMessage.WithResult("123", new CustomObject())), true, false, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
+ new JsonProtocolTestData("CompletionMessage_HasErrorAndHeadersAndCamelCase", AddHeaders(TestHeaders, CompletionMessage.Empty("123")), true, true, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\"}"),
new JsonProtocolTestData("CompletionMessage_HasIntegerResult", CompletionMessage.WithResult("123", 1), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":1}"),
new JsonProtocolTestData("CompletionMessage_HasStringResult", CompletionMessage.WithResult("123", "Foo"), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":\"Foo\"}"),
new JsonProtocolTestData("CompletionMessage_HasBoolResult", CompletionMessage.WithResult("123", true), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":true}"),
@@ -53,6 +71,11 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
new JsonProtocolTestData("CompletionMessage_HasError", CompletionMessage.WithError("123", "Whoops!"), true, true, "{\"type\":3,\"invocationId\":\"123\",\"error\":\"Whoops!\"}"),
new JsonProtocolTestData("CompletionMessage_HasErrorAndHeaders", AddHeaders(TestHeaders, CompletionMessage.WithError("123", "Whoops!")), true, true, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\",\"error\":\"Whoops!\"}"),
+ new JsonProtocolTestData("StreamInvocationMessage_HasHeaders", AddHeaders(TestHeaders, new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() })), true, false, "{\"type\":4," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
+ new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), false, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"),
+ new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnore", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"),
+ new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), false, false, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"),
+ new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueInclude", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), true, false, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
new JsonProtocolTestData("StreamInvocationMessage_HasInvocationId", new StreamInvocationMessage("123", "Target", new object[] { 1, "Foo" }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\"]}"),
new JsonProtocolTestData("StreamInvocationMessage_HasBoolArgument", new StreamInvocationMessage("123", "Target", new object[] { true }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[true]}"),
new JsonProtocolTestData("StreamInvocationMessage_HasNullArgument", new StreamInvocationMessage("123", "Target", new object[] { null }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[null]}"),
@@ -156,8 +179,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
[InlineData("{\"type\":4,\"invocationId\":\"42\",\"target\":\"foo\"}", "Missing required property 'arguments'.")]
[InlineData("{\"type\":4,\"invocationId\":\"42\",\"target\":\"foo\",\"arguments\":{}}", "Expected 'arguments' to be of type Array.")]
- //[InlineData("{\"type\":3,\"invocationId\":\"42\",\"error\":\"foo\",\"result\":true}", "The 'error' and 'result' properties are mutually exclusive.")]
- //[InlineData("{\"type\":3,\"invocationId\":\"42\",\"result\":true", "Unexpected end when reading JSON.")]
+ [InlineData("{\"type\":3,\"invocationId\":\"42\",\"error\":\"foo\",\"result\":true}", "The 'error' and 'result' properties are mutually exclusive.")]
public void InvalidMessages(string input, string expectedMessage)
{
input = Frame(input);
@@ -270,6 +292,26 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
Assert.Equal("foo", bindingFailure.Target);
}
+ [Fact]
+ public void ReadCaseInsensitivePropertiesByDefault()
+ {
+ var input = Frame("{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StrIngProp\":\"test\",\"DoublePrOp\":3.14159,\"IntProp\":43,\"DateTimeProp\":\"2019-06-03T22:00:00\",\"NuLLProp\":null,\"ByteARRProp\":\"AgQG\"}}");
+
+ var binder = new TestBinder(null, typeof(CustomObject));
+ var data = new ReadOnlySequence(Encoding.UTF8.GetBytes(input));
+ JsonHubProtocol.TryParseMessage(ref data, binder, out var message);
+
+ var streamItemMessage = Assert.IsType(message);
+ Assert.Equal(new CustomObject()
+ {
+ ByteArrProp = new byte[] { 2, 4, 6 },
+ IntProp = 43,
+ DoubleProp = 3.14159,
+ StringProp = "test",
+ DateTimeProp = DateTime.Parse("6/3/2019 10:00:00 PM")
+ }, streamItemMessage.Item);
+ }
+
public static string Frame(string input)
{
var data = Encoding.UTF8.GetBytes(input);
diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/NewtonsoftJsonHubProtocolTests.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/NewtonsoftJsonHubProtocolTests.cs
index 255cdead83..de10b1dbf6 100644
--- a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/NewtonsoftJsonHubProtocolTests.cs
+++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/NewtonsoftJsonHubProtocolTests.cs
@@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
[InlineData("", "Unexpected end when reading JSON.")]
[InlineData("42", "Unexpected JSON Token Type 'Integer'. Expected a JSON Object.")]
[InlineData("{\"type\":\"foo\"}", "Expected 'type' to be of type Integer.")]
+ [InlineData("{\"type\":3,\"invocationId\":\"42\",\"result\":true", "Unexpected end when reading JSON.")]
public void CustomInvalidMessages(string input, string expectedMessage)
{
input = Frame(input);
@@ -93,35 +94,13 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
public static IDictionary CustomProtocolTestData => new[]
{
new JsonProtocolTestData("InvocationMessage_HasFloatArgument", new InvocationMessage(null, "Target", new object[] { 1, "Foo", 2.0f }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}"),
- new JsonProtocolTestData("InvocationMessage_StringIsoDateArgument", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), false, true, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20+12:34\"]}"),
- new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNoCamelCase", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), false, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"),
- new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnore", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, true, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"),
- new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), false, false, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"),
- new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueInclude", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, false, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
new JsonProtocolTestData("InvocationMessage_HasHeaders", AddHeaders(TestHeaders, new InvocationMessage("123", "Target", new object[] { 1, "Foo", 2.0f })), true, true, "{\"type\":1," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}"),
new JsonProtocolTestData("StreamItemMessage_HasFloatItem", new StreamItemMessage("123", 2.0f), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":2.0}"),
- new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNoCamelCase", new StreamItemMessage("123", new CustomObject()), false, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnore", new StreamItemMessage("123", new CustomObject()), true, true, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueIgnoreAndNoCamelCase", new StreamItemMessage("123", new CustomObject()), false, false, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueInclude", new StreamItemMessage("123", new CustomObject()), true, false, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("StreamItemMessage_HasHeaders", AddHeaders(TestHeaders, new StreamItemMessage("123", new CustomObject())), true, false, "{\"type\":2," + SerializedHeaders + ",\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNoCamelCase", CompletionMessage.WithResult("123", new CustomObject()), false, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIgnore", CompletionMessage.WithResult("123", new CustomObject()), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueIncludeAndNoCamelCase", CompletionMessage.WithResult("123", new CustomObject()), false, false, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("CompletionMessage_HasCustomResultWithNullValueInclude", CompletionMessage.WithResult("123", new CustomObject()), true, false, "{\"type\":3,\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("CompletionMessage_HasTestHeadersAndCustomItemResult", AddHeaders(TestHeaders, CompletionMessage.WithResult("123", new CustomObject())), true, false, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\",\"result\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"),
- new JsonProtocolTestData("CompletionMessage_HasErrorAndCamelCase", CompletionMessage.Empty("123"), true, true, "{\"type\":3,\"invocationId\":\"123\"}"),
- new JsonProtocolTestData("CompletionMessage_HasErrorAndHeadersAndCamelCase", AddHeaders(TestHeaders, CompletionMessage.Empty("123")), true, true, "{\"type\":3," + SerializedHeaders + ",\"invocationId\":\"123\"}"),
new JsonProtocolTestData("CompletionMessage_HasFloatResult", CompletionMessage.WithResult("123", 2.0f), true, true, "{\"type\":3,\"invocationId\":\"123\",\"result\":2.0}"),
new JsonProtocolTestData("StreamInvocationMessage_HasFloatArgument", new StreamInvocationMessage("123", "Target", new object[] { 1, "Foo", 2.0f }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}"),
- new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), false, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"ByteArrProp\":\"AQID\"}]}"),
- new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnore", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), true, true, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"),
- new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), false, false, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"),
- new JsonProtocolTestData("StreamInvocationMessage_HasCustomArgumentWithNullValueInclude", new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() }), true, false, "{\"type\":4,\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
- new JsonProtocolTestData("StreamInvocationMessage_HasHeaders", AddHeaders(TestHeaders, new StreamInvocationMessage("123", "Target", new object[] { new CustomObject() })), true, false, "{\"type\":4," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"),
}.ToDictionary(t => t.Name);
public static IEnumerable CustomProtocolTestDataNames => CustomProtocolTestData.Keys.Select(name => new object[] { name });
diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/Utf8BufferTextWriterTests.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/Utf8BufferTextWriterTests.cs
index b1fe8c76fb..6b7a5563d3 100644
--- a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/Utf8BufferTextWriterTests.cs
+++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/Utf8BufferTextWriterTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@@ -245,7 +245,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
}
[Fact]
- private void WriteMultiByteCharactersToSmallBuffers()
+ public void WriteMultiByteCharactersToSmallBuffers()
{
// Test string breakdown (char => UTF-8 hex values):
// a => 61
diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Commands/Defaults.cs b/src/SignalR/perf/benchmarkapps/Crankier/Commands/Defaults.cs
index 732ccb6af3..84027fff16 100644
--- a/src/SignalR/perf/benchmarkapps/Crankier/Commands/Defaults.cs
+++ b/src/SignalR/perf/benchmarkapps/Crankier/Commands/Defaults.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http.Connections;
+using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.SignalR.Crankier.Commands
{
@@ -11,5 +12,6 @@ namespace Microsoft.AspNetCore.SignalR.Crankier.Commands
public static readonly int NumberOfConnections = 10_000;
public static readonly int SendDurationInSeconds = 300;
public static readonly HttpTransportType TransportType = HttpTransportType.WebSockets;
+ public static readonly LogLevel LogLevel = LogLevel.None;
}
}
diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Commands/ServerCommand.cs b/src/SignalR/perf/benchmarkapps/Crankier/Commands/ServerCommand.cs
new file mode 100644
index 0000000000..cb1ef585b6
--- /dev/null
+++ b/src/SignalR/perf/benchmarkapps/Crankier/Commands/ServerCommand.cs
@@ -0,0 +1,60 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http.Connections;
+using Microsoft.Extensions.CommandLineUtils;
+using static Microsoft.AspNetCore.SignalR.Crankier.Commands.CommandLineUtilities;
+using System.Diagnostics;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.AspNetCore.SignalR.Crankier.Server;
+
+namespace Microsoft.AspNetCore.SignalR.Crankier.Commands
+{
+ internal class ServerCommand
+ {
+ public static void Register(CommandLineApplication app)
+ {
+ app.Command("server", cmd =>
+ {
+ var logLevelOption = cmd.Option("--log ", "The LogLevel to use.", CommandOptionType.SingleValue);
+
+ cmd.OnExecute(() =>
+ {
+ LogLevel logLevel = Defaults.LogLevel;
+
+ if (logLevelOption.HasValue() && !Enum.TryParse(logLevelOption.Value(), out logLevel))
+ {
+ return InvalidArg(logLevelOption);
+ }
+ return Execute(logLevel);
+ });
+ });
+ }
+
+ private static int Execute(LogLevel logLevel)
+ {
+ Console.WriteLine($"Process ID: {Process.GetCurrentProcess().Id}");
+
+ var config = new ConfigurationBuilder()
+ .AddEnvironmentVariables(prefix: "ASPNETCORE_")
+ .Build();
+
+ var host = new WebHostBuilder()
+ .UseConfiguration(config)
+ .ConfigureLogging(loggerFactory =>
+ {
+ loggerFactory.AddConsole().SetMinimumLevel(logLevel);
+ })
+ .UseKestrel()
+ .UseStartup();
+
+ host.Build().Run();
+
+ return 0;
+ }
+ }
+}
diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Crankier.csproj b/src/SignalR/perf/benchmarkapps/Crankier/Crankier.csproj
index 1c1b18a058..1bc1e98bd6 100644
--- a/src/SignalR/perf/benchmarkapps/Crankier/Crankier.csproj
+++ b/src/SignalR/perf/benchmarkapps/Crankier/Crankier.csproj
@@ -9,6 +9,11 @@
+
+
+
+
+
diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Program.cs b/src/SignalR/perf/benchmarkapps/Crankier/Program.cs
index a3443ecd94..93f53ea328 100644
--- a/src/SignalR/perf/benchmarkapps/Crankier/Program.cs
+++ b/src/SignalR/perf/benchmarkapps/Crankier/Program.cs
@@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.SignalR.Crankier
LocalCommand.Register(app);
AgentCommand.Register(app);
WorkerCommand.Register(app);
+ ServerCommand.Register(app);
app.Command("help", cmd =>
{
diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Readme.md b/src/SignalR/perf/benchmarkapps/Crankier/Readme.md
index 20f56cc720..7b0a95fbe9 100644
--- a/src/SignalR/perf/benchmarkapps/Crankier/Readme.md
+++ b/src/SignalR/perf/benchmarkapps/Crankier/Readme.md
@@ -4,6 +4,24 @@ Load testing for ASP.NET Core SignalR
## Commands
+### server
+
+The `server` command runs a web host exposing a single SignalR `Hub` endpoint on `/echo`. After the first client connection, the server will periodically write concurrent connection information to the console.
+
+```
+> dotnet run -- help server
+
+Usage: server [options]
+
+Options:
+ --log The LogLevel to use.
+```
+
+Notes:
+
+* `LOG_LEVEL` switches internal logging only, not concurrent connection information, and defaults to `LogLevel.None`. Use this option to control Kestrel / SignalR Warnings & Errors being logged to console.
+
+
### local
The `local` command launches a set of local worker clients to establish connections to your SignalR server.
@@ -31,13 +49,19 @@ Notes:
#### Examples
-Attempt to make 10,000 connections to the `echo` hub using WebSockets and 10 workers:
+Run the server:
+
+```
+dotnet run -- server
+```
+
+Attempt to make 10,000 connections to the server using WebSockets and 10 workers:
```
dotnet run -- local --target-url https://localhost:5001/echo --workers 10
```
-Attempt to make 5,000 connections to the `echo` hub using Long Polling
+Attempt to make 5,000 connections to the server using Long Polling
```
dotnet run -- local --target-url https://localhost:5001/echo --connections 5000 --transport LongPolling
diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionCounter.cs b/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionCounter.cs
new file mode 100644
index 0000000000..1ab6a25abc
--- /dev/null
+++ b/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionCounter.cs
@@ -0,0 +1,60 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNetCore.SignalR.Crankier.Server
+{
+ public class ConnectionCounter
+ {
+ private int _totalConnectedCount;
+ private int _peakConnectedCount;
+ private int _totalDisconnectedCount;
+ private int _receivedCount;
+
+ private readonly object _lock = new object();
+
+ public ConnectionSummary Summary
+ {
+ get
+ {
+ lock (_lock)
+ {
+ return new ConnectionSummary
+ {
+ CurrentConnections = _totalConnectedCount - _totalDisconnectedCount,
+ PeakConnections = _peakConnectedCount,
+ TotalConnected = _totalConnectedCount,
+ TotalDisconnected = _totalDisconnectedCount,
+ ReceivedCount = _receivedCount
+ };
+ }
+ }
+ }
+
+ public void Receive(string payload)
+ {
+ lock (_lock)
+ {
+ _receivedCount += payload.Length;
+ }
+ }
+
+ public void Connected()
+ {
+ lock (_lock)
+ {
+ _totalConnectedCount++;
+ _peakConnectedCount = Math.Max(_totalConnectedCount - _totalDisconnectedCount, _peakConnectedCount);
+ }
+ }
+
+ public void Disconnected()
+ {
+ lock (_lock)
+ {
+ _totalDisconnectedCount++;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionCounterHostedService.cs b/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionCounterHostedService.cs
new file mode 100644
index 0000000000..44b8bb26f2
--- /dev/null
+++ b/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionCounterHostedService.cs
@@ -0,0 +1,79 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Hosting;
+
+namespace Microsoft.AspNetCore.SignalR.Crankier.Server
+{
+ public class ConnectionCounterHostedService : IHostedService, IDisposable
+ {
+ private Stopwatch _timeSinceFirstConnection;
+ private readonly ConnectionCounter _counter;
+ private ConnectionSummary _lastSummary;
+ private Timer _timer;
+ private int _executingDoWork;
+
+ public ConnectionCounterHostedService(ConnectionCounter counter)
+ {
+ _counter = counter;
+ _timeSinceFirstConnection = new Stopwatch();
+ }
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
+
+ return Task.CompletedTask;
+ }
+
+ private void DoWork(object state)
+ {
+ if (Interlocked.Exchange(ref _executingDoWork, 1) == 0)
+ {
+ var summary = _counter.Summary;
+
+ if (summary.PeakConnections > 0)
+ {
+ if (_timeSinceFirstConnection.ElapsedTicks == 0)
+ {
+ _timeSinceFirstConnection.Start();
+ }
+
+ var elapsed = _timeSinceFirstConnection.Elapsed;
+
+ if (_lastSummary != null)
+ {
+ Console.WriteLine(@"[{0:hh\:mm\:ss}] Current: {1}, peak: {2}, connected: {3}, disconnected: {4}, rate: {5}/s",
+ elapsed,
+ summary.CurrentConnections,
+ summary.PeakConnections,
+ summary.TotalConnected - _lastSummary.TotalConnected,
+ summary.TotalDisconnected - _lastSummary.TotalDisconnected,
+ summary.CurrentConnections - _lastSummary.CurrentConnections
+ );
+ }
+
+ _lastSummary = summary;
+ }
+
+ Interlocked.Exchange(ref _executingDoWork, 0);
+ }
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ _timer?.Change(Timeout.Infinite, 0);
+
+ return Task.CompletedTask;
+ }
+
+ public void Dispose()
+ {
+ _timer?.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionSummary.cs b/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionSummary.cs
new file mode 100644
index 0000000000..83f38aaf62
--- /dev/null
+++ b/src/SignalR/perf/benchmarkapps/Crankier/Server/ConnectionSummary.cs
@@ -0,0 +1,18 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.SignalR.Crankier.Server
+{
+ public class ConnectionSummary
+ {
+ public int TotalConnected { get; set; }
+
+ public int TotalDisconnected { get; set; }
+
+ public int PeakConnections { get; set; }
+
+ public int CurrentConnections { get; set; }
+
+ public int ReceivedCount { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Server/EchoHub.cs b/src/SignalR/perf/benchmarkapps/Crankier/Server/EchoHub.cs
new file mode 100644
index 0000000000..0b24b46e9b
--- /dev/null
+++ b/src/SignalR/perf/benchmarkapps/Crankier/Server/EchoHub.cs
@@ -0,0 +1,72 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.SignalR;
+
+namespace Microsoft.AspNetCore.SignalR.Crankier.Server
+{
+ public class EchoHub : Hub
+ {
+ private ConnectionCounter _counter;
+
+ public EchoHub(ConnectionCounter counter)
+ {
+ _counter = counter;
+ }
+
+ public async Task Broadcast(int duration)
+ {
+ var sent = 0;
+ try
+ {
+ var t = new CancellationTokenSource();
+ t.CancelAfter(TimeSpan.FromSeconds(duration));
+ while (!t.IsCancellationRequested && !Context.ConnectionAborted.IsCancellationRequested)
+ {
+ await Clients.All.SendAsync("send", DateTime.UtcNow);
+ sent++;
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ }
+ Console.WriteLine("Broadcast exited: Sent {0} messages", sent);
+ }
+
+ public override Task OnConnectedAsync()
+ {
+ _counter?.Connected();
+ return Task.CompletedTask;
+ }
+
+ public override Task OnDisconnectedAsync(Exception exception)
+ {
+ _counter?.Disconnected();
+ return Task.CompletedTask;
+ }
+
+ public DateTime Echo(DateTime time)
+ {
+ return time;
+ }
+
+ public Task EchoAll(DateTime time)
+ {
+ return Clients.All.SendAsync("send", time);
+ }
+
+ public void SendPayload(string payload)
+ {
+ _counter?.Receive(payload);
+ }
+
+ public DateTime GetCurrentTime()
+ {
+ return DateTime.UtcNow;
+ }
+ }
+}
diff --git a/src/SignalR/perf/benchmarkapps/Crankier/Server/Startup.cs b/src/SignalR/perf/benchmarkapps/Crankier/Server/Startup.cs
new file mode 100644
index 0000000000..d8d5efc5bf
--- /dev/null
+++ b/src/SignalR/perf/benchmarkapps/Crankier/Server/Startup.cs
@@ -0,0 +1,39 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.SignalR.Crankier.Server
+{
+ public class Startup
+ {
+ private readonly IConfiguration _config;
+ public Startup(IConfiguration configuration)
+ {
+ _config = configuration;
+ }
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ var signalrBuilder = services.AddSignalR()
+ .AddMessagePackProtocol();
+
+ services.AddSingleton();
+
+ services.AddHostedService();
+ }
+
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ app.UseRouting();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapHub("/echo");
+ });
+ }
+ }
+}
diff --git a/src/SignalR/publish-apps.ps1 b/src/SignalR/publish-apps.ps1
index fcb8c99cae..8c7b2de4a4 100644
--- a/src/SignalR/publish-apps.ps1
+++ b/src/SignalR/publish-apps.ps1
@@ -1,4 +1,4 @@
-param($RootDirectory = (Get-Location), $Framework = "netcoreapp3.1", $Runtime = "win-x64", $CommitHash, $BranchName, $BuildNumber)
+param($RootDirectory = (Get-Location), $Framework = "netcoreapp5.0", $Runtime = "win-x64", $CommitHash, $BranchName, $BuildNumber)
# De-Powershell the path
$RootDirectory = (Convert-Path $RootDirectory)
diff --git a/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs b/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs
index 3c835ab933..a8a9fe2095 100644
--- a/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs
+++ b/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs
@@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.SignalR
return SendToAllConnections(methodName, args, null);
}
- private Task SendToAllConnections(string methodName, object[] args, Func include)
+ private Task SendToAllConnections(string methodName, object[] args, Func include, object state = null)
{
List tasks = null;
SerializedHubMessage message = null;
@@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.SignalR
// foreach over HubConnectionStore avoids allocating an enumerator
foreach (var connection in _connections)
{
- if (include != null && !include(connection))
+ if (include != null && !include(connection, state))
{
continue;
}
@@ -127,12 +127,12 @@ namespace Microsoft.AspNetCore.SignalR
// Tasks and message are passed by ref so they can be lazily created inside the method post-filtering,
// while still being re-usable when sending to multiple groups
- private void SendToGroupConnections(string methodName, object[] args, ConcurrentDictionary connections, Func include, ref List tasks, ref SerializedHubMessage message)
+ private void SendToGroupConnections(string methodName, object[] args, ConcurrentDictionary connections, Func include, object state, ref List tasks, ref SerializedHubMessage message)
{
// foreach over ConcurrentDictionary avoids allocating an enumerator
foreach (var connection in connections)
{
- if (include != null && !include(connection.Value))
+ if (include != null && !include(connection.Value, state))
{
continue;
}
@@ -193,7 +193,7 @@ namespace Microsoft.AspNetCore.SignalR
// group might be modified inbetween checking and sending
List tasks = null;
SerializedHubMessage message = null;
- SendToGroupConnections(methodName, args, group, null, ref tasks, ref message);
+ SendToGroupConnections(methodName, args, group, null, null, ref tasks, ref message);
if (tasks != null)
{
@@ -221,7 +221,7 @@ namespace Microsoft.AspNetCore.SignalR
var group = _groups[groupName];
if (group != null)
{
- SendToGroupConnections(methodName, args, group, null, ref tasks, ref message);
+ SendToGroupConnections(methodName, args, group, null, null, ref tasks, ref message);
}
}
@@ -247,7 +247,7 @@ namespace Microsoft.AspNetCore.SignalR
List tasks = null;
SerializedHubMessage message = null;
- SendToGroupConnections(methodName, args, group, connection => !excludedConnectionIds.Contains(connection.ConnectionId), ref tasks, ref message);
+ SendToGroupConnections(methodName, args, group, (connection, state) => !((IReadOnlyList)state).Contains(connection.ConnectionId), excludedConnectionIds, ref tasks, ref message);
if (tasks != null)
{
@@ -271,7 +271,7 @@ namespace Microsoft.AspNetCore.SignalR
///
public override Task SendUserAsync(string userId, string methodName, object[] args, CancellationToken cancellationToken = default)
{
- return SendToAllConnections(methodName, args, connection => string.Equals(connection.UserIdentifier, userId, StringComparison.Ordinal));
+ return SendToAllConnections(methodName, args, (connection, state) => string.Equals(connection.UserIdentifier, (string)state, StringComparison.Ordinal), userId);
}
///
@@ -292,19 +292,19 @@ namespace Microsoft.AspNetCore.SignalR
///
public override Task SendAllExceptAsync(string methodName, object[] args, IReadOnlyList excludedConnectionIds, CancellationToken cancellationToken = default)
{
- return SendToAllConnections(methodName, args, connection => !excludedConnectionIds.Contains(connection.ConnectionId));
+ return SendToAllConnections(methodName, args, (connection, state) => !((IReadOnlyList)state).Contains(connection.ConnectionId), excludedConnectionIds);
}
///
public override Task SendConnectionsAsync(IReadOnlyList connectionIds, string methodName, object[] args, CancellationToken cancellationToken = default)
{
- return SendToAllConnections(methodName, args, connection => connectionIds.Contains(connection.ConnectionId));
+ return SendToAllConnections(methodName, args, (connection, state) => ((IReadOnlyList)state).Contains(connection.ConnectionId), connectionIds);
}
///
public override Task SendUsersAsync(IReadOnlyList userIds, string methodName, object[] args, CancellationToken cancellationToken = default)
{
- return SendToAllConnections(methodName, args, connection => userIds.Contains(connection.UserIdentifier));
+ return SendToAllConnections(methodName, args, (connection, state) => ((IReadOnlyList)state).Contains(connection.UserIdentifier), userIds);
}
}
}
diff --git a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs
index b0a841e1b7..71ea5a687e 100644
--- a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs
+++ b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs
@@ -275,7 +275,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal
{
if (descriptor.OriginalParameterTypes[parameterPointer] == typeof(CancellationToken))
{
- cts = CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted);
+ cts = CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted, default);
arguments[parameterPointer] = cts.Token;
}
else if (isStreamCall && ReflectionHelper.IsStreamingType(descriptor.OriginalParameterTypes[parameterPointer], mustBeDirectType: true))
@@ -308,7 +308,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal
return;
}
- cts = cts ?? CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted);
+ cts = cts ?? CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted, default);
connection.ActiveRequestCancellationSources.TryAdd(hubMethodInvocationMessage.InvocationId, cts);
var enumerable = descriptor.FromReturnedStream(result, cts.Token);
diff --git a/src/SignalR/server/Core/src/Internal/TypedClientBuilder.cs b/src/SignalR/server/Core/src/Internal/TypedClientBuilder.cs
index 511280d636..a333c600a8 100644
--- a/src/SignalR/server/Core/src/Internal/TypedClientBuilder.cs
+++ b/src/SignalR/server/Core/src/Internal/TypedClientBuilder.cs
@@ -132,6 +132,14 @@ namespace Microsoft.AspNetCore.SignalR.Internal
methodBuilder.DefineGenericParameters(genericTypeNames);
}
+ // Check to see if the last parameter of the method is a CancellationToken
+ bool hasCancellationToken = paramTypes.LastOrDefault() == typeof(CancellationToken);
+ if (hasCancellationToken)
+ {
+ // remove CancellationToken from input paramTypes
+ paramTypes = paramTypes.Take(paramTypes.Length - 1).ToArray();
+ }
+
var generator = methodBuilder.GetILGenerator();
// Declare local variable to store the arguments to IClientProxy.SendCoreAsync
@@ -145,7 +153,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal
generator.Emit(OpCodes.Ldstr, interfaceMethodInfo.Name);
// Create an new object array to hold all the parameters to this method
- generator.Emit(OpCodes.Ldc_I4, parameters.Length); // Stack:
+ generator.Emit(OpCodes.Ldc_I4, paramTypes.Length); // Stack:
generator.Emit(OpCodes.Newarr, typeof(object)); // allocate object array
generator.Emit(OpCodes.Stloc_0);
@@ -162,8 +170,16 @@ namespace Microsoft.AspNetCore.SignalR.Internal
// Load parameter array on to the stack.
generator.Emit(OpCodes.Ldloc_0);
- // Get 'CancellationToken.None' and put it on the stack, since we don't support CancellationToken right now
- generator.Emit(OpCodes.Call, CancellationTokenNoneProperty.GetMethod);
+ if (hasCancellationToken)
+ {
+ // Get CancellationToken from input argument and put it on the stack
+ generator.Emit(OpCodes.Ldarg, paramTypes.Length + 1);
+ }
+ else
+ {
+ // Get 'CancellationToken.None' and put it on the stack, for when method does not have CancellationToken
+ generator.Emit(OpCodes.Call, CancellationTokenNoneProperty.GetMethod);
+ }
// Send!
generator.Emit(OpCodes.Callvirt, invokeMethod);
diff --git a/src/SignalR/server/SignalR/test/Internal/TypedClientBuilderTests.cs b/src/SignalR/server/SignalR/test/Internal/TypedClientBuilderTests.cs
index 4f68f6fe74..9f412af8f4 100644
--- a/src/SignalR/server/SignalR/test/Internal/TypedClientBuilderTests.cs
+++ b/src/SignalR/server/SignalR/test/Internal/TypedClientBuilderTests.cs
@@ -75,6 +75,41 @@ namespace Microsoft.AspNetCore.SignalR.Tests.Internal
await task2.OrTimeout();
}
+ [Fact]
+ public async Task SupportsCancellationToken()
+ {
+ var clientProxy = new MockProxy();
+ var typedProxy = TypedClientBuilder.Build(clientProxy);
+ CancellationTokenSource cts1 = new CancellationTokenSource();
+ var task1 = typedProxy.Method("foo", cts1.Token);
+ Assert.False(task1.IsCompleted);
+
+ CancellationTokenSource cts2 = new CancellationTokenSource();
+ var task2 = typedProxy.NoArgumentMethod(cts2.Token);
+ Assert.False(task2.IsCompleted);
+
+ Assert.Collection(clientProxy.Sends,
+ send1 =>
+ {
+ Assert.Equal("Method", send1.Method);
+ Assert.Equal(1, send1.Arguments.Length);
+ Assert.Collection(send1.Arguments,
+ arg1 => Assert.Equal("foo", arg1));
+ Assert.Equal(cts1.Token, send1.CancellationToken);
+ send1.Complete();
+ },
+ send2 =>
+ {
+ Assert.Equal("NoArgumentMethod", send2.Method);
+ Assert.Equal(0, send2.Arguments.Length);
+ Assert.Equal(cts2.Token, send2.CancellationToken);
+ send2.Complete();
+ });
+
+ await task1.OrTimeout();
+ await task2.OrTimeout();
+ }
+
[Fact]
public void ThrowsIfProvidedAClass()
{
@@ -179,6 +214,12 @@ namespace Microsoft.AspNetCore.SignalR.Tests.Internal
Task SubMethod(string foo);
}
+ public interface ICancellationTokenMethod
+ {
+ Task Method(string foo, CancellationToken cancellationToken);
+ Task NoArgumentMethod(CancellationToken cancellationToken);
+ }
+
public interface IPropertiesClient
{
string Property { get; }
diff --git a/src/SignalR/server/SignalR/test/UserAgentHeaderTest.cs b/src/SignalR/server/SignalR/test/UserAgentHeaderTest.cs
new file mode 100644
index 0000000000..6c3ba450d8
--- /dev/null
+++ b/src/SignalR/server/SignalR/test/UserAgentHeaderTest.cs
@@ -0,0 +1,34 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using Microsoft.AspNetCore.Http.Connections.Client;
+using Xunit;
+using Constants = Microsoft.AspNetCore.Http.Connections.Client.Internal.Constants;
+
+namespace Microsoft.AspNetCore.Http.Connections.Tests
+{
+ public class UserAgentHeaderTest
+ {
+ [Fact]
+ public void UserAgentHeaderIsAccurate()
+ {
+ var majorVersion = typeof(HttpConnection).Assembly.GetName().Version.Major;
+ var minorVersion = typeof(HttpConnection).Assembly.GetName().Version.Minor;
+ var version = typeof(HttpConnection).Assembly.GetName().Version;
+ var os = RuntimeInformation.OSDescription;
+ var runtime = ".NET";
+ var runtimeVersion = RuntimeInformation.FrameworkDescription;
+ var assemblyVersion = typeof(Constants)
+ .Assembly
+ .GetCustomAttributes()
+ .FirstOrDefault();
+ var userAgent = Constants.UserAgentHeader;
+ var expectedUserAgent = $"Microsoft SignalR/{majorVersion}.{minorVersion} ({assemblyVersion.InformationalVersion}; {os}; {runtime}; {runtimeVersion})";
+
+ Assert.Equal(expectedUserAgent, userAgent);
+ }
+ }
+}
diff --git a/src/SignalR/server/StackExchangeRedis/src/Internal/RedisProtocol.cs b/src/SignalR/server/StackExchangeRedis/src/Internal/RedisProtocol.cs
index a1594b0fd3..b6f276ab5e 100644
--- a/src/SignalR/server/StackExchangeRedis/src/Internal/RedisProtocol.cs
+++ b/src/SignalR/server/StackExchangeRedis/src/Internal/RedisProtocol.cs
@@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis.Internal
// * Acks are sent to the Acknowledgement channel.
// * See the Write[type] methods for a description of the protocol for each in-depth.
// * The "Variable length integer" is the length-prefixing format used by BinaryReader/BinaryWriter:
- // * https://docs.microsoft.com/en-us/dotnet/api/system.io.binarywriter.write?view=netstandard-2.0
+ // * https://docs.microsoft.com/dotnet/api/system.io.binarywriter.write?view=netcore-2.2
// * The "Length prefixed string" is the string format used by BinaryReader/BinaryWriter:
// * A 7-bit variable length integer encodes the length in bytes, followed by the encoded string in UTF-8.
diff --git a/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj b/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj
index 4ef7283db6..82e665d1c0 100644
--- a/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj
+++ b/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj
@@ -24,6 +24,8 @@
+
+
diff --git a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs
index d230e1bb8d..7e33386d27 100644
--- a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs
+++ b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs
@@ -21,8 +21,7 @@ namespace Microsoft.DotNet.OpenApi.Tests
protected readonly TextWriter _output = new StringWriter();
protected readonly TextWriter _error = new StringWriter();
protected readonly ITestOutputHelper _outputHelper;
- protected const string TestTFM = "netcoreapp3.1";
-
+ protected const string TestTFM = "netcoreapp5.0";
protected const string Content = @"{""x-generator"": ""NSwag""}";
protected const string ActualUrl = "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/api-with-examples.yaml";
diff --git a/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs b/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs
index adcbe32b1e..5a42903675 100644
--- a/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs
+++ b/src/Tools/dotnet-user-secrets/test/UserSecretsTestFixture.cs
@@ -35,7 +35,7 @@ namespace Microsoft.Extensions.Configuration.UserSecrets.Tests
private const string ProjectTemplate = @"
Exe
- netcoreapp3.1
+ netcoreapp5.0
{0}
false
diff --git a/src/Tools/dotnet-watch/test/ProgramTests.cs b/src/Tools/dotnet-watch/test/ProgramTests.cs
index 0e7dff9b82..6e24eb13d2 100644
--- a/src/Tools/dotnet-watch/test/ProgramTests.cs
+++ b/src/Tools/dotnet-watch/test/ProgramTests.cs
@@ -28,7 +28,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
{
_tempDir
.WithCSharpProject("testproj")
- .WithTargetFrameworks("netcoreapp3.1")
+ .WithTargetFrameworks("netcoreapp5.0")
.Dir()
.WithFile("Program.cs")
.Create();
diff --git a/src/Tools/dotnet-watch/test/TestProjects/AppWithDeps/AppWithDeps.csproj b/src/Tools/dotnet-watch/test/TestProjects/AppWithDeps/AppWithDeps.csproj
index d0cde953c7..7399c1018d 100644
--- a/src/Tools/dotnet-watch/test/TestProjects/AppWithDeps/AppWithDeps.csproj
+++ b/src/Tools/dotnet-watch/test/TestProjects/AppWithDeps/AppWithDeps.csproj
@@ -1,7 +1,7 @@
- netcoreapp3.1
+ netcoreapp5.0
exe
true
diff --git a/src/Tools/dotnet-watch/test/TestProjects/GlobbingApp/GlobbingApp.csproj b/src/Tools/dotnet-watch/test/TestProjects/GlobbingApp/GlobbingApp.csproj
index 9f015b1ee4..8f8043d0de 100644
--- a/src/Tools/dotnet-watch/test/TestProjects/GlobbingApp/GlobbingApp.csproj
+++ b/src/Tools/dotnet-watch/test/TestProjects/GlobbingApp/GlobbingApp.csproj
@@ -1,7 +1,7 @@
- netcoreapp3.1
+ netcoreapp5.0
exe
false
true
diff --git a/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/KitchenSink.csproj b/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/KitchenSink.csproj
index af6de1b33f..6de103d382 100644
--- a/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/KitchenSink.csproj
+++ b/src/Tools/dotnet-watch/test/TestProjects/KitchenSink/KitchenSink.csproj
@@ -9,7 +9,7 @@
Exe
- netcoreapp3.1
+ netcoreapp5.0
true
diff --git a/src/Tools/dotnet-watch/test/TestProjects/NoDepsApp/NoDepsApp.csproj b/src/Tools/dotnet-watch/test/TestProjects/NoDepsApp/NoDepsApp.csproj
index 95412443e6..110ff7686b 100644
--- a/src/Tools/dotnet-watch/test/TestProjects/NoDepsApp/NoDepsApp.csproj
+++ b/src/Tools/dotnet-watch/test/TestProjects/NoDepsApp/NoDepsApp.csproj
@@ -1,7 +1,7 @@
- netcoreapp3.1
+ netcoreapp5.0
exe
true