Merge remote-tracking branch 'origin/master' into dotnet-maestro-bot-merge/release/3.1-to-master
This commit is contained in:
commit
772283163e
|
|
@ -487,6 +487,26 @@ jobs:
|
||||||
path: artifacts/TestResults/
|
path: artifacts/TestResults/
|
||||||
publishOnError: true
|
publishOnError: true
|
||||||
|
|
||||||
|
# Helix ARM64
|
||||||
|
- template: jobs/default-build.yml
|
||||||
|
parameters:
|
||||||
|
jobName: Helix_arm64
|
||||||
|
jobDisplayName: "Tests: Helix ARM64"
|
||||||
|
agentOs: Linux
|
||||||
|
timeoutInMinutes: 240
|
||||||
|
steps:
|
||||||
|
- script: ./restore.sh -ci
|
||||||
|
displayName: Restore
|
||||||
|
- script: ./build.sh -ci --arch arm64 -test --no-build-nodejs -projects $(Build.SourcesDirectory)/eng/helix/helix.proj /p:IsHelixJob=true /p:BuildAllProjects=true /p:BuildNative=true -bl
|
||||||
|
displayName: Run build.sh helix arm64 target
|
||||||
|
env:
|
||||||
|
SYSTEM_ACCESSTOKEN: $(System.AccessToken) # We need to set this env var to publish helix results to Azure Dev Ops
|
||||||
|
installNodeJs: false
|
||||||
|
artifacts:
|
||||||
|
- name: Helix_arm64_logs
|
||||||
|
path: artifacts/log/
|
||||||
|
publishOnError: true
|
||||||
|
|
||||||
# Source build
|
# Source build
|
||||||
- job: Source_Build
|
- job: Source_Build
|
||||||
displayName: 'Test: Linux Source Build'
|
displayName: 'Test: Linux Source Build'
|
||||||
|
|
|
||||||
|
|
@ -25,23 +25,3 @@ jobs:
|
||||||
- name: Helix_logs
|
- name: Helix_logs
|
||||||
path: artifacts/log/
|
path: artifacts/log/
|
||||||
publishOnError: true
|
publishOnError: true
|
||||||
|
|
||||||
# Build Helix ARM64
|
|
||||||
- template: jobs/default-build.yml
|
|
||||||
parameters:
|
|
||||||
jobName: Helix_arm64
|
|
||||||
jobDisplayName: "Tests: Helix ARM64"
|
|
||||||
agentOs: Linux
|
|
||||||
timeoutInMinutes: 240
|
|
||||||
steps:
|
|
||||||
- script: ./restore.sh -ci
|
|
||||||
displayName: Restore
|
|
||||||
- script: ./build.sh -ci --arch arm64 -test --no-build-nodejs -projects $(Build.SourcesDirectory)/eng/helix/helix.proj /p:IsHelixJob=true /p:BuildAllProjects=true /p:BuildNative=true -bl
|
|
||||||
displayName: Run build.sh helix arm64 target
|
|
||||||
env:
|
|
||||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken) # We need to set this env var to publish helix results to Azure Dev Ops
|
|
||||||
installNodeJs: false
|
|
||||||
artifacts:
|
|
||||||
- name: Helix_arm64_logs
|
|
||||||
path: artifacts/logs/
|
|
||||||
publishOnError: true
|
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,10 @@ Write-Host "Extracting to $tempDir"
|
||||||
|
|
||||||
if (Get-Command -Name 'Microsoft.PowerShell.Archive\Expand-Archive' -ErrorAction Ignore) {
|
if (Get-Command -Name 'Microsoft.PowerShell.Archive\Expand-Archive' -ErrorAction Ignore) {
|
||||||
# Use built-in commands where possible as they are cross-plat compatible
|
# Use built-in commands where possible as they are cross-plat compatible
|
||||||
Microsoft.PowerShell.Archive\Expand-Archive -Path "nodejs.zip" -DestinationPath $tempDir
|
Microsoft.PowerShell.Archive\Expand-Archive -Path "nodejs.zip" -DestinationPath $tempDir -Force
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
Remove-Item $tempDir -Recurse -ErrorAction Ignore
|
||||||
# Fallback to old approach for old installations of PowerShell
|
# Fallback to old approach for old installations of PowerShell
|
||||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||||
[System.IO.Compression.ZipFile]::ExtractToDirectory("nodejs.zip", $tempDir)
|
[System.IO.Compression.ZipFile]::ExtractToDirectory("nodejs.zip", $tempDir)
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ try {
|
||||||
Write-Host "Run git diff to check for pending changes"
|
Write-Host "Run git diff to check for pending changes"
|
||||||
|
|
||||||
# Redirect stderr to stdout because PowerShell does not consistently handle output to stderr
|
# Redirect stderr to stdout because PowerShell does not consistently handle output to stderr
|
||||||
$changedFiles = & cmd /c 'git --no-pager diff --ignore-space-at-eol --name-only 2>nul'
|
$changedFiles = & cmd /c 'git --no-pager diff --ignore-space-change --name-only 2>nul'
|
||||||
|
|
||||||
# Temporary: Disable check for blazor js file
|
# Temporary: Disable check for blazor js file
|
||||||
$changedFilesExclusion = "src/Components/Web.JS/dist/Release/blazor.server.js"
|
$changedFilesExclusion = "src/Components/Web.JS/dist/Release/blazor.server.js"
|
||||||
|
|
@ -179,7 +179,7 @@ try {
|
||||||
if ($file -eq $changedFilesExclusion) {continue}
|
if ($file -eq $changedFilesExclusion) {continue}
|
||||||
$filePath = Resolve-Path "${repoRoot}/${file}"
|
$filePath = Resolve-Path "${repoRoot}/${file}"
|
||||||
LogError "Generated code is not up to date in $file. You might need to regenerate the reference assemblies or project list (see docs/ReferenceAssemblies.md and docs/ReferenceResolution.md)" -filepath $filePath
|
LogError "Generated code is not up to date in $file. You might need to regenerate the reference assemblies or project list (see docs/ReferenceAssemblies.md and docs/ReferenceResolution.md)" -filepath $filePath
|
||||||
& git --no-pager diff --ignore-space-at-eol $filePath
|
& git --no-pager diff --ignore-space-change $filePath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
<HelixAvailableTargetQueue Include="Windows.81.Amd64.Open" Platform="Windows" />
|
<HelixAvailableTargetQueue Include="Windows.81.Amd64.Open" Platform="Windows" />
|
||||||
<HelixAvailableTargetQueue Include="Windows.7.Amd64.Open" Platform="Windows" />
|
<HelixAvailableTargetQueue Include="Windows.7.Amd64.Open" Platform="Windows" />
|
||||||
<HelixAvailableTargetQueue Include="Windows.10.Amd64.EnterpriseRS3.ASPNET.Open" Platform="Windows" EnableByDefault="false" />
|
<HelixAvailableTargetQueue Include="Windows.10.Amd64.EnterpriseRS3.ASPNET.Open" Platform="Windows" EnableByDefault="false" />
|
||||||
<HelixAvailableTargetQueue Include="OSX.1013.Amd64.Open" Platform="OSX" />
|
|
||||||
<HelixAvailableTargetQueue Include="Ubuntu.1604.Amd64.Open" Platform="Linux" />
|
<HelixAvailableTargetQueue Include="Ubuntu.1604.Amd64.Open" Platform="Linux" />
|
||||||
<HelixAvailableTargetQueue Include="Ubuntu.1804.Amd64.Open" Platform="Linux" />
|
<HelixAvailableTargetQueue Include="Ubuntu.1804.Amd64.Open" Platform="Linux" />
|
||||||
<HelixAvailableTargetQueue Include="Centos.7.Amd64.Open" Platform="Linux" />
|
<HelixAvailableTargetQueue Include="Centos.7.Amd64.Open" Platform="Linux" />
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,20 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<TSFiles Include="$(MSBuildProjectDirectory)\*\*.ts" />
|
<TSFiles Include="src\**\*.ts" />
|
||||||
<TSFiles Include="$(MSBuildProjectDirectory)\package.json" />
|
<TSFiles Include="test\**\*.ts" />
|
||||||
<TSFiles Include="$(MSBuildProjectDirectory)\*.npmproj" />
|
<TSFiles Include="package.json" />
|
||||||
|
<TSFiles Include="*.npmproj" />
|
||||||
|
|
||||||
|
<BuildOutputFiles Include="$(BaseIntermediateOutputPath)\build-sentinel" />
|
||||||
|
<BuildOutputFiles Include="dist\**\*.js" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="_CheckForInvalidConfiguration">
|
<Target Name="_CheckForInvalidConfiguration">
|
||||||
<Error Text="Missing expected property: PackageId" Condition="'$(IsPackable)' != 'false' and '$(PackageId)' == ''" />
|
<Error Text="Missing expected property: PackageId" Condition="'$(IsPackable)' != 'false' and '$(PackageId)' == ''" />
|
||||||
|
|
||||||
<Exec ContinueOnError="true" Command="node -v">
|
<Exec ContinueOnError="true" Command="node -v" StandardOutputImportance="Low">
|
||||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
|
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
|
||||||
</Exec>
|
</Exec>
|
||||||
|
|
||||||
<Error Text="Building *.npmproj but NodeJS was not detected on path. Ensure NodeJS is on path or disable building NodeJS projects with /p:BuildNodeJs=false. Skipping NodeJS projects will also skip managed projects depending on them, including Components, Mvc and Analysers." Condition="'$(ErrorCode)' != '0'"/>
|
<Error Text="Building *.npmproj but NodeJS was not detected on path. Ensure NodeJS is on path or disable building NodeJS projects with /p:BuildNodeJs=false. Skipping NodeJS projects will also skip managed projects depending on them, including Components, Mvc and Analysers." Condition="'$(ErrorCode)' != '0'"/>
|
||||||
|
|
@ -53,12 +57,25 @@
|
||||||
<CallTarget Targets="_Pack" Condition="'$(PackOnBuild)' == 'true'" />
|
<CallTarget Targets="_Pack" Condition="'$(PackOnBuild)' == 'true'" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="GetBuildInputCacheFile">
|
||||||
|
<Hash ItemsToHash="@(TSFiles)">
|
||||||
|
<Output TaskParameter="HashResult" PropertyName="_TSFileHash" />
|
||||||
|
</Hash>
|
||||||
|
|
||||||
|
<WriteLinesToFile
|
||||||
|
Lines="$(_TSFileHash)"
|
||||||
|
File="$(BaseIntermediateOutputPath)tsfiles.cache"
|
||||||
|
Overwrite="True"
|
||||||
|
WriteOnlyWhenDifferent="True" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
<Target Name="_Build"
|
<Target Name="_Build"
|
||||||
Condition="'$(IsBuildable)' != 'false'"
|
Condition="'$(IsBuildable)' != 'false'"
|
||||||
Inputs="@(TSFiles)"
|
DependsOnTargets="GetBuildInputCacheFile"
|
||||||
Outputs="$(BaseIntermediateOutputPath)\build-sentinel" >
|
Inputs="@(TSFiles);$(BaseIntermediateOutputPath)tsfiles.cache"
|
||||||
|
Outputs="@(BuildOutputFiles)">
|
||||||
<Yarn Command="$(NpmBuildArgs)" StandardOutputImportance="High" StandardErrorImportance="High" />
|
<Yarn Command="$(NpmBuildArgs)" StandardOutputImportance="High" StandardErrorImportance="High" />
|
||||||
<WriteLinesToFile Overwrite="true" File="$(BaseIntermediateOutputPath)\build-sentinel" />
|
<WriteLinesToFile Overwrite="true" File="$(BaseIntermediateOutputPath)build-sentinel" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|
@ -73,7 +90,10 @@
|
||||||
</PackDependsOn>
|
</PackDependsOn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Target Name="_Pack" Condition="'$(IsPackable)' == 'true'" >
|
<Target Name="_Pack" Condition="'$(IsPackable)' == 'true'"
|
||||||
|
Inputs="@(TSFiles)"
|
||||||
|
Outputs="$(PackageOutputPath)\$(PackageFileName)">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_PackageTargetPath>$(MSBuildProjectDirectory)\$(PackageFileName)</_PackageTargetPath>
|
<_PackageTargetPath>$(MSBuildProjectDirectory)\$(PackageFileName)</_PackageTargetPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,11 @@ namespace Microsoft.AspNetCore.Blazor.Services
|
||||||
|
|
||||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||||
{
|
{
|
||||||
|
if (!IsEnabled(logLevel))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var formattedMessage = formatter(state, exception);
|
var formattedMessage = formatter(state, exception);
|
||||||
Console.WriteLine($"[{logLevel}] {formattedMessage}");
|
Console.WriteLine($"[{logLevel}] {formattedMessage}");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<BuildOutputFiles Include="dist\release\blazor.server.js" />
|
||||||
|
<BuildOutputFiles Include="dist\release\blazor.webassembly.js" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference
|
<ProjectReference
|
||||||
Include="..\..\SignalR\clients\ts\signalr\signalr.npmproj"
|
Include="..\..\SignalR\clients\ts\signalr\signalr.npmproj"
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -5,8 +5,6 @@
|
||||||
<RootNamespace>Microsoft.AspNetCore</RootNamespace>
|
<RootNamespace>Microsoft.AspNetCore</RootNamespace>
|
||||||
<!-- https://github.com/aspnet/AspNetCore/issues/7939: This unit test requires the shared framework be available in Helix. -->
|
<!-- https://github.com/aspnet/AspNetCore/issues/7939: This unit test requires the shared framework be available in Helix. -->
|
||||||
<BuildHelixPayload>false</BuildHelixPayload>
|
<BuildHelixPayload>false</BuildHelixPayload>
|
||||||
<!-- Test logic needs to be updated to handle when SharedFx version and TFM are not the same -->
|
|
||||||
<SkipTests>true</SkipTests>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -56,34 +56,5 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const int BasePort = 5001;
|
|
||||||
private const int MaxPort = 8000;
|
|
||||||
private static int NextPort = BasePort;
|
|
||||||
|
|
||||||
// GetNextPort doesn't check for HttpSys urlacls.
|
|
||||||
public static int GetNextHttpSysPort(string scheme)
|
|
||||||
{
|
|
||||||
while (NextPort < MaxPort)
|
|
||||||
{
|
|
||||||
var port = NextPort++;
|
|
||||||
|
|
||||||
using (var server = new HttpListener())
|
|
||||||
{
|
|
||||||
server.Prefixes.Add($"{scheme}://localhost:{port}/");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
server.Start();
|
|
||||||
server.Stop();
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
catch (HttpListenerException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NextPort = BasePort;
|
|
||||||
throw new Exception("Failed to locate a free port.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common
|
namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common
|
||||||
{
|
{
|
||||||
public static class TestUriHelper
|
public static class TestUriHelper
|
||||||
|
|
@ -34,7 +36,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common
|
||||||
}
|
}
|
||||||
else if (serverType == ServerType.HttpSys)
|
else if (serverType == ServerType.HttpSys)
|
||||||
{
|
{
|
||||||
return new UriBuilder(scheme, "localhost", TestPortHelper.GetNextHttpSysPort(scheme)).Uri;
|
Debug.Assert(scheme == "http", "Https not supported");
|
||||||
|
return new UriBuilder(scheme, "localhost", 0).Uri;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,7 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
var schemes = await GetAllSignInSchemeNames();
|
var schemes = await GetAllSignInSchemeNames();
|
||||||
|
|
||||||
// CookieAuth is the only implementation of sign-in.
|
// CookieAuth is the only implementation of sign-in.
|
||||||
var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?";
|
var footer = $" Did you forget to call AddAuthentication().AddCookie(\"{scheme}\",...)?";
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(schemes))
|
if (string.IsNullOrEmpty(schemes))
|
||||||
{
|
{
|
||||||
|
|
@ -275,7 +275,7 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
{
|
{
|
||||||
// CookieAuth is the only implementation of sign-in.
|
// CookieAuth is the only implementation of sign-in.
|
||||||
return new InvalidOperationException(mismatchError
|
return new InvalidOperationException(mismatchError
|
||||||
+ $"Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and SignInAsync(\"Cookies\",...)?");
|
+ $"Did you forget to call AddAuthentication().AddCookie(\"Cookies\") and SignInAsync(\"Cookies\",...)?");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new InvalidOperationException(mismatchError + $"The registered sign-in schemes are: {schemes}.");
|
return new InvalidOperationException(mismatchError + $"The registered sign-in schemes are: {schemes}.");
|
||||||
|
|
@ -292,7 +292,7 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
{
|
{
|
||||||
var schemes = await GetAllSignOutSchemeNames();
|
var schemes = await GetAllSignOutSchemeNames();
|
||||||
|
|
||||||
var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?";
|
var footer = $" Did you forget to call AddAuthentication().AddCookie(\"{scheme}\",...)?";
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(schemes))
|
if (string.IsNullOrEmpty(schemes))
|
||||||
{
|
{
|
||||||
|
|
@ -314,7 +314,7 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
{
|
{
|
||||||
// CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it.
|
// CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it.
|
||||||
return new InvalidOperationException(mismatchError
|
return new InvalidOperationException(mismatchError
|
||||||
+ $"Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and {nameof(SignOutAsync)}(\"Cookies\",...)?");
|
+ $"Did you forget to call AddAuthentication().AddCookie(\"Cookies\") and {nameof(SignOutAsync)}(\"Cookies\",...)?");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new InvalidOperationException(mismatchError + $"The registered sign-out schemes are: {schemes}.");
|
return new InvalidOperationException(mismatchError + $"The registered sign-out schemes are: {schemes}.");
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@ namespace Microsoft.AspNetCore.WebUtilities
|
||||||
{
|
{
|
||||||
if (Disposed)
|
if (Disposed)
|
||||||
{
|
{
|
||||||
throw new ObjectDisposedException(nameof(FileBufferingReadStream));
|
throw new ObjectDisposedException(nameof(FileBufferingWriteStream));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
|
||||||
return InvokeCoreAwaited(context, policyTask);
|
return InvokeCoreAwaited(context, policyTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
corsPolicy = policyTask.GetAwaiter().GetResult();
|
corsPolicy = policyTask.Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return EvaluateAndApplyPolicy(context, corsPolicy);
|
return EvaluateAndApplyPolicy(context, corsPolicy);
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -49,8 +49,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||||
{
|
{
|
||||||
return HealthCheckResult.Healthy();
|
return HealthCheckResult.Healthy();
|
||||||
}
|
}
|
||||||
|
|
||||||
return HealthCheckResult.Unhealthy();
|
return new HealthCheckResult(context.Registration.FailureStatus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -61,7 +61,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAsync_CustomTest_Degraded()
|
public async Task CheckAsync_CustomTestWithDegradedFailureStatusSpecified_Degraded()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var services = CreateServices(async (c, ct) =>
|
var services = CreateServices(async (c, ct) =>
|
||||||
|
|
@ -78,12 +78,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||||
var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, });
|
var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(HealthStatus.Unhealthy, result.Status);
|
Assert.Equal(HealthStatus.Degraded, result.Status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckAsync_CustomTest_Unhealthy()
|
public async Task CheckAsync_CustomTestWithUnhealthyFailureStatusSpecified_Unhealthy()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var services = CreateServices(async (c, ct) =>
|
var services = CreateServices(async (c, ct) =>
|
||||||
|
|
@ -104,12 +104,34 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CheckAsync_CustomTestWithNoFailureStatusSpecified_Unhealthy()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var services = CreateServices(async (c, ct) =>
|
||||||
|
{
|
||||||
|
return 0 < await c.Blogs.CountAsync();
|
||||||
|
}, failureStatus: null);
|
||||||
|
|
||||||
|
using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope())
|
||||||
|
{
|
||||||
|
var registration = Assert.Single(services.GetRequiredService<IOptions<HealthCheckServiceOptions>>().Value.Registrations);
|
||||||
|
var check = ActivatorUtilities.CreateInstance<DbContextHealthCheck<TestDbContext>>(scope.ServiceProvider);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HealthStatus.Unhealthy, result.Status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// used to ensure each test uses a unique in-memory database
|
// used to ensure each test uses a unique in-memory database
|
||||||
private static int _testDbCounter;
|
private static int _testDbCounter;
|
||||||
|
|
||||||
private static IServiceProvider CreateServices(
|
private static IServiceProvider CreateServices(
|
||||||
Func<TestDbContext, CancellationToken, Task<bool>> testQuery = null,
|
Func<TestDbContext, CancellationToken, Task<bool>> testQuery = null,
|
||||||
HealthStatus failureStatus = HealthStatus.Unhealthy)
|
HealthStatus? failureStatus = HealthStatus.Unhealthy)
|
||||||
{
|
{
|
||||||
var serviceCollection = new ServiceCollection();
|
var serviceCollection = new ServiceCollection();
|
||||||
serviceCollection.AddDbContext<TestDbContext>(o => o.UseInMemoryDatabase("Test" + Interlocked.Increment(ref _testDbCounter)));
|
serviceCollection.AddDbContext<TestDbContext>(o => o.UseInMemoryDatabase("Test" + Interlocked.Increment(ref _testDbCounter)));
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,12 @@ namespace Microsoft.AspNetCore.NodeServices.Util
|
||||||
var chunkLength = await _streamReader.ReadAsync(buf, 0, buf.Length);
|
var chunkLength = await _streamReader.ReadAsync(buf, 0, buf.Length);
|
||||||
if (chunkLength == 0)
|
if (chunkLength == 0)
|
||||||
{
|
{
|
||||||
|
if (_linesBuffer.Length > 0)
|
||||||
|
{
|
||||||
|
OnCompleteLine(_linesBuffer.ToString());
|
||||||
|
_linesBuffer.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
OnClosed();
|
OnClosed();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
/// Retrieves a value object using the specified key.
|
/// Retrieves a value object using the specified key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">The key of the value object to retrieve.</param>
|
/// <param name="key">The key of the value object to retrieve.</param>
|
||||||
/// <returns>The value object for the specified key. If the exact key is not found, null.</returns>
|
/// <returns>The value object for the specified key. If the exact key is not found, <see cref="ValueProviderResult.None" />.</returns>
|
||||||
ValueProviderResult GetValue(string key);
|
ValueProviderResult GetValue(string key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1848,7 +1848,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an <see cref="ObjectResult"/> that produces a <see cref="ProblemDetails"/> response.
|
/// Creates an <see cref="ObjectResult"/> that produces a <see cref="ProblemDetails"/> response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="statusCode">The value for <see cref="ProblemDetails.Status" />..</param>
|
/// <param name="statusCode">The value for <see cref="ProblemDetails.Status" />.</param>
|
||||||
/// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param>
|
/// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param>
|
||||||
/// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
|
/// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
|
||||||
/// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>
|
/// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
|
||||||
<UserSecretsId Condition="'$(IndividualAuth)' == 'True' OR '$(OrganizationalAuth)' == 'True'">aspnet-BlazorServerWeb_CSharp-53bc9b9d-9d6a-45d4-8429-2a2761773502</UserSecretsId>
|
<UserSecretsId Condition="'$(IndividualAuth)' == 'True' OR '$(OrganizationalAuth)' == 'True'">aspnet-BlazorServerWeb_CSharp-53bc9b9d-9d6a-45d4-8429-2a2761773502</UserSecretsId>
|
||||||
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' != 'True'">0</WebProject_DirectoryAccessLevelKey>
|
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' != 'True'">0</WebProject_DirectoryAccessLevelKey>
|
||||||
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' == 'True'">1</WebProject_DirectoryAccessLevelKey>
|
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' == 'True'">1</WebProject_DirectoryAccessLevelKey>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
|
||||||
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
|
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
|
||||||
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.WebApplication1</RootNamespace>
|
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.WebApplication1</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
|
||||||
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
|
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
|
||||||
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.WebApplication1</RootNamespace>
|
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.WebApplication1</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Lists the versions of dependencies not built in this repo. Packages produced from this repo should be listed as a PackageVersionVariableReference. -->
|
<!-- Lists the versions of dependencies not built in this repo. Packages produced from this repo should be listed as a PackageVersionVariableReference. -->
|
||||||
<GeneratedContentProperties>
|
<GeneratedContentProperties>
|
||||||
|
DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework);
|
||||||
GrpcAspNetCorePackageVersion=$(GrpcAspNetCorePackageVersion);
|
GrpcAspNetCorePackageVersion=$(GrpcAspNetCorePackageVersion);
|
||||||
MicrosoftAspNetCoreMvcRazorRuntimeCompilationPackageVersion=$(MicrosoftAspNetCoreMvcRazorRuntimeCompilationPackageVersion);
|
MicrosoftAspNetCoreMvcRazorRuntimeCompilationPackageVersion=$(MicrosoftAspNetCoreMvcRazorRuntimeCompilationPackageVersion);
|
||||||
MicrosoftEntityFrameworkCoreSqlitePackageVersion=$(MicrosoftEntityFrameworkCoreSqlitePackageVersion);
|
MicrosoftEntityFrameworkCoreSqlitePackageVersion=$(MicrosoftEntityFrameworkCoreSqlitePackageVersion);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
|
||||||
<UserSecretsId Condition="'$(IndividualAuth)' == 'True' OR '$(OrganizationalAuth)' == 'True'">aspnet-Company.WebApplication1-0ce56475-d1db-490f-8af1-a881ea4fcd2d</UserSecretsId>
|
<UserSecretsId Condition="'$(IndividualAuth)' == 'True' OR '$(OrganizationalAuth)' == 'True'">aspnet-Company.WebApplication1-0ce56475-d1db-490f-8af1-a881ea4fcd2d</UserSecretsId>
|
||||||
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' != 'True'">0</WebProject_DirectoryAccessLevelKey>
|
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' != 'True'">0</WebProject_DirectoryAccessLevelKey>
|
||||||
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' == 'True'">1</WebProject_DirectoryAccessLevelKey>
|
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' == 'True'">1</WebProject_DirectoryAccessLevelKey>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
|
||||||
<UserSecretsId Condition="'$(IndividualAuth)' == 'True' OR '$(OrganizationalAuth)' == 'True'">aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502</UserSecretsId>
|
<UserSecretsId Condition="'$(IndividualAuth)' == 'True' OR '$(OrganizationalAuth)' == 'True'">aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502</UserSecretsId>
|
||||||
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' != 'True'">0</WebProject_DirectoryAccessLevelKey>
|
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' != 'True'">0</WebProject_DirectoryAccessLevelKey>
|
||||||
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' == 'True'">1</WebProject_DirectoryAccessLevelKey>
|
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' == 'True'">1</WebProject_DirectoryAccessLevelKey>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
|
||||||
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
|
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
|
||||||
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.WebApplication1</RootNamespace>
|
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.WebApplication1</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
|
||||||
<UserSecretsId Condition="'$(IndividualAuth)' == 'True' OR '$(OrganizationalAuth)' == 'True'">aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502</UserSecretsId>
|
<UserSecretsId Condition="'$(IndividualAuth)' == 'True' OR '$(OrganizationalAuth)' == 'True'">aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502</UserSecretsId>
|
||||||
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' != 'True'">0</WebProject_DirectoryAccessLevelKey>
|
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' != 'True'">0</WebProject_DirectoryAccessLevelKey>
|
||||||
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' == 'True'">1</WebProject_DirectoryAccessLevelKey>
|
<WebProject_DirectoryAccessLevelKey Condition="'$(OrganizationalAuth)' == 'True' AND '$(OrgReadAccess)' == 'True'">1</WebProject_DirectoryAccessLevelKey>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
|
||||||
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
|
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
|
||||||
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.WebApplication1</RootNamespace>
|
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.WebApplication1</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
|
||||||
<UserSecretsId>dotnet-Company.Application1-53bc9b9d-9d6a-45d4-8429-2a2761773502</UserSecretsId>
|
<UserSecretsId>dotnet-Company.Application1-53bc9b9d-9d6a-45d4-8429-2a2761773502</UserSecretsId>
|
||||||
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
|
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
|
||||||
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.Application1</RootNamespace>
|
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.Application1</RootNamespace>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Lists the versions of dependencies not built in this repo. Packages produced from this repo should be listed as a PackageVersionVariableReference. -->
|
<!-- Lists the versions of dependencies not built in this repo. Packages produced from this repo should be listed as a PackageVersionVariableReference. -->
|
||||||
<GeneratedContentProperties>
|
<GeneratedContentProperties>
|
||||||
|
DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework);
|
||||||
MicrosoftEntityFrameworkCoreSqlitePackageVersion=$(MicrosoftEntityFrameworkCoreSqlitePackageVersion);
|
MicrosoftEntityFrameworkCoreSqlitePackageVersion=$(MicrosoftEntityFrameworkCoreSqlitePackageVersion);
|
||||||
MicrosoftEntityFrameworkCoreRelationalPackageVersion=$(MicrosoftEntityFrameworkCoreRelationalPackageVersion);
|
MicrosoftEntityFrameworkCoreRelationalPackageVersion=$(MicrosoftEntityFrameworkCoreRelationalPackageVersion);
|
||||||
MicrosoftEntityFrameworkCoreSqlServerPackageVersion=$(MicrosoftEntityFrameworkCoreSqlServerPackageVersion);
|
MicrosoftEntityFrameworkCoreSqlServerPackageVersion=$(MicrosoftEntityFrameworkCoreSqlServerPackageVersion);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
BROWSER=none
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
BROWSER=none
|
||||||
|
|
@ -2,16 +2,18 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.Contracts;
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Hosting.Server;
|
using Microsoft.AspNetCore.Hosting.Server;
|
||||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.AspNetCore.HttpSys.Internal;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.AspNetCore.HttpSys.Internal;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.HttpSys
|
namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
{
|
{
|
||||||
|
|
@ -74,6 +76,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
}
|
}
|
||||||
|
|
||||||
var hostingUrlsPresent = _serverAddresses.Addresses.Count > 0;
|
var hostingUrlsPresent = _serverAddresses.Addresses.Count > 0;
|
||||||
|
var serverAddressCopy = _serverAddresses.Addresses.ToList();
|
||||||
|
_serverAddresses.Addresses.Clear();
|
||||||
|
|
||||||
if (_serverAddresses.PreferHostingUrls && hostingUrlsPresent)
|
if (_serverAddresses.PreferHostingUrls && hostingUrlsPresent)
|
||||||
{
|
{
|
||||||
|
|
@ -85,10 +89,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
Listener.Options.UrlPrefixes.Clear();
|
Listener.Options.UrlPrefixes.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var value in _serverAddresses.Addresses)
|
UpdateUrlPrefixes(serverAddressCopy);
|
||||||
{
|
|
||||||
Listener.Options.UrlPrefixes.Add(value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (_options.UrlPrefixes.Count > 0)
|
else if (_options.UrlPrefixes.Count > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -100,23 +101,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
_serverAddresses.Addresses.Clear();
|
_serverAddresses.Addresses.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var prefix in _options.UrlPrefixes)
|
|
||||||
{
|
|
||||||
_serverAddresses.Addresses.Add(prefix.FullPrefix);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (hostingUrlsPresent)
|
else if (hostingUrlsPresent)
|
||||||
{
|
{
|
||||||
foreach (var value in _serverAddresses.Addresses)
|
UpdateUrlPrefixes(serverAddressCopy);
|
||||||
{
|
|
||||||
Listener.Options.UrlPrefixes.Add(value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LogHelper.LogDebug(_logger, $"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default.");
|
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);
|
Listener.Options.UrlPrefixes.Add(Constants.DefaultServerAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,6 +122,13 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
|
|
||||||
Listener.Start();
|
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();
|
ActivateRequestProcessingLimits();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
@ -142,6 +142,14 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateUrlPrefixes(IList<string> serverAddressCopy)
|
||||||
|
{
|
||||||
|
foreach (var value in serverAddressCopy)
|
||||||
|
{
|
||||||
|
Listener.Options.UrlPrefixes.Add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The message pump.
|
// The message pump.
|
||||||
// When we start listening for the next request on one thread, we may need to be sure that the
|
// 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.
|
// completion continues on another thread as to not block the current request processing.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -73,7 +73,6 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
{
|
{
|
||||||
LogHelper.LogInfo(_logger, "Listening on prefix: " + uriPrefix);
|
LogHelper.LogInfo(_logger, "Listening on prefix: " + uriPrefix);
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
var statusCode = HttpApi.HttpAddUrlToUrlGroup(Id, uriPrefix, (ulong)contextId, 0);
|
var statusCode = HttpApi.HttpAddUrlToUrlGroup(Id, uriPrefix, (ulong)contextId, 0);
|
||||||
|
|
||||||
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
|
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// 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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Microsoft.AspNetCore.HttpSys.Internal;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.HttpSys
|
namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
{
|
{
|
||||||
|
|
@ -15,6 +20,12 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
private UrlGroup _urlGroup;
|
private UrlGroup _urlGroup;
|
||||||
private int _nextId = 1;
|
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()
|
internal UrlPrefixCollection()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -138,10 +149,55 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
{
|
{
|
||||||
_urlGroup = urlGroup;
|
_urlGroup = urlGroup;
|
||||||
// go through the uri list and register for each one of them
|
// 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.
|
var urlPrefix = pair.Value;
|
||||||
_urlGroup.RegisterPrefix(pair.Value.FullPrefix, pair.Key);
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
%~dp0..\..\..\startvs.cmd %~dp0HttpSysServer.sln
|
||||||
|
|
@ -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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -114,7 +114,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
{
|
{
|
||||||
server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
|
server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
|
||||||
|
|
||||||
Assert.Equal(Constants.DefaultServerAddress, server.Features.Get<IServerAddressesFeature>().Addresses.Single());
|
// Trailing slash is added when put in UrlPrefix.
|
||||||
|
Assert.StartsWith(Constants.DefaultServerAddress, server.Features.Get<IServerAddressesFeature>().Addresses.Single());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
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
|
// When tests projects are run in parallel, overlapping port ranges can cause a race condition when looking for free
|
||||||
// ports during dynamic port allocation.
|
// ports during dynamic port allocation.
|
||||||
private const int BasePort = 5001;
|
|
||||||
private const int MaxPort = 8000;
|
|
||||||
private const int BaseHttpsPort = 44300;
|
private const int BaseHttpsPort = 44300;
|
||||||
private const int MaxHttpsPort = 44399;
|
private const int MaxHttpsPort = 44399;
|
||||||
private static int NextPort = BasePort;
|
|
||||||
private static int NextHttpsPort = BaseHttpsPort;
|
private static int NextHttpsPort = BaseHttpsPort;
|
||||||
private static object PortLock = new object();
|
private static object PortLock = new object();
|
||||||
internal static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15);
|
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<HttpSysOptions> configureOptions, RequestDelegate app)
|
internal static IWebHost CreateDynamicHost(string basePath, out string root, out string baseAddress, Action<HttpSysOptions> configureOptions, RequestDelegate app)
|
||||||
{
|
{
|
||||||
lock (PortLock)
|
var prefix = UrlPrefix.Create("http", "localhost", "0", basePath);
|
||||||
{
|
|
||||||
while (NextPort < MaxPort)
|
var builder = new WebHostBuilder()
|
||||||
|
.UseHttpSys(options =>
|
||||||
{
|
{
|
||||||
var port = NextPort++;
|
options.UrlPrefixes.Add(prefix);
|
||||||
var prefix = UrlPrefix.Create("http", "localhost", port, basePath);
|
configureOptions(options);
|
||||||
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
|
})
|
||||||
baseAddress = prefix.ToString();
|
.Configure(appBuilder => appBuilder.Run(app));
|
||||||
|
|
||||||
var builder = new WebHostBuilder()
|
var host = builder.Build();
|
||||||
.UseHttpSys(options =>
|
|
||||||
{
|
|
||||||
options.UrlPrefixes.Add(prefix);
|
|
||||||
configureOptions(options);
|
|
||||||
})
|
|
||||||
.Configure(appBuilder => appBuilder.Run(app));
|
|
||||||
|
|
||||||
var host = builder.Build();
|
host.Start();
|
||||||
|
|
||||||
|
var options = host.Services.GetRequiredService<IOptions<HttpSysOptions>>();
|
||||||
|
prefix = options.Value.UrlPrefixes.First(); // Has new port
|
||||||
|
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
|
||||||
|
baseAddress = prefix.ToString();
|
||||||
|
|
||||||
try
|
return host;
|
||||||
{
|
|
||||||
host.Start();
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
catch (HttpSysException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
NextPort = BasePort;
|
|
||||||
}
|
|
||||||
throw new Exception("Failed to locate a free port.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static MessagePump CreatePump()
|
internal static MessagePump CreatePump()
|
||||||
|
|
@ -124,31 +109,18 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
|
|
||||||
internal static IServer CreateDynamicHttpServer(string basePath, out string root, out string baseAddress, Action<HttpSysOptions> configureOptions, RequestDelegate app)
|
internal static IServer CreateDynamicHttpServer(string basePath, out string root, out string baseAddress, Action<HttpSysOptions> configureOptions, RequestDelegate app)
|
||||||
{
|
{
|
||||||
lock (PortLock)
|
var prefix = UrlPrefix.Create("http", "localhost", "0", basePath);
|
||||||
{
|
|
||||||
while (NextPort < MaxPort)
|
|
||||||
{
|
|
||||||
|
|
||||||
var port = NextPort++;
|
var server = CreatePump();
|
||||||
var prefix = UrlPrefix.Create("http", "localhost", port, basePath);
|
server.Features.Get<IServerAddressesFeature>().Addresses.Add(prefix.ToString());
|
||||||
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
|
configureOptions(server.Listener.Options);
|
||||||
baseAddress = prefix.ToString();
|
server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
|
||||||
|
|
||||||
var server = CreatePump();
|
prefix = server.Listener.Options.UrlPrefixes.First(); // Has new port
|
||||||
server.Features.Get<IServerAddressesFeature>().Addresses.Add(baseAddress);
|
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
|
||||||
configureOptions(server.Listener.Options);
|
baseAddress = prefix.ToString();
|
||||||
try
|
|
||||||
{
|
return server;
|
||||||
server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
catch (HttpSysException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NextPort = BasePort;
|
|
||||||
}
|
|
||||||
throw new Exception("Failed to locate a free port.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static IServer CreateDynamicHttpsServer(out string baseAddress, RequestDelegate app)
|
internal static IServer CreateDynamicHttpsServer(out string baseAddress, RequestDelegate app)
|
||||||
|
|
@ -184,6 +156,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
||||||
throw new Exception("Failed to locate a free port.");
|
throw new Exception("Failed to locate a free port.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout);
|
internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout);
|
||||||
|
|
||||||
internal static Task<T> WithTimeout<T>(this Task<T> task) => task.TimeoutAfter(DefaultTimeout);
|
internal static Task<T> WithTimeout<T>(this Task<T> task) => task.TimeoutAfter(DefaultTimeout);
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
||||||
|
|
@ -204,27 +204,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
while (_isClosed == 0)
|
while (_isClosed == 0)
|
||||||
{
|
{
|
||||||
var result = await Input.ReadAsync();
|
var result = await Input.ReadAsync();
|
||||||
var readableBuffer = result.Buffer;
|
var buffer = result.Buffer;
|
||||||
var consumed = readableBuffer.Start;
|
|
||||||
var examined = readableBuffer.Start;
|
|
||||||
|
|
||||||
// Call UpdateCompletedStreams() prior to frame processing in order to remove any streams that have exceded their drain timeouts.
|
// Call UpdateCompletedStreams() prior to frame processing in order to remove any streams that have exceded their drain timeouts.
|
||||||
UpdateCompletedStreams();
|
UpdateCompletedStreams();
|
||||||
|
|
||||||
try
|
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);
|
||||||
{
|
await ProcessFrameAsync(application, framePayload);
|
||||||
Log.Http2FrameReceived(ConnectionId, _incomingFrame);
|
|
||||||
consumed = examined = framePayload.End;
|
|
||||||
await ProcessFrameAsync(application, framePayload);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
examined = readableBuffer.End;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.IsCompleted)
|
if (result.IsCompleted)
|
||||||
|
|
@ -242,7 +232,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
Input.AdvanceTo(consumed, examined);
|
Input.AdvanceTo(buffer.Start, buffer.End);
|
||||||
|
|
||||||
UpdateConnectionState();
|
UpdateConnectionState();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 const int SettingSize = 6; // 2 bytes for the id, 4 bytes for the value.
|
||||||
|
|
||||||
public static bool ReadFrame(in ReadOnlySequence<byte> readableBuffer, Http2Frame frame, uint maxFrameSize, out ReadOnlySequence<byte> framePayload)
|
public static bool TryReadFrame(ref ReadOnlySequence<byte> buffer, Http2Frame frame, uint maxFrameSize, out ReadOnlySequence<byte> framePayload)
|
||||||
{
|
{
|
||||||
framePayload = ReadOnlySequence<byte>.Empty;
|
framePayload = ReadOnlySequence<byte>.Empty;
|
||||||
|
|
||||||
if (readableBuffer.Length < HeaderLength)
|
if (buffer.Length < HeaderLength)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var headerSlice = readableBuffer.Slice(0, HeaderLength);
|
var headerSlice = buffer.Slice(0, HeaderLength);
|
||||||
var header = headerSlice.ToSpan();
|
var header = headerSlice.ToSpan();
|
||||||
|
|
||||||
var payloadLength = (int)Bitshifter.ReadUInt24BigEndian(header);
|
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
|
// Make sure the whole frame is buffered
|
||||||
var frameLength = HeaderLength + payloadLength;
|
var frameLength = HeaderLength + payloadLength;
|
||||||
if (readableBuffer.Length < frameLength)
|
if (buffer.Length < frameLength)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -61,10 +61,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
frame.Flags = header[FlagsOffset];
|
frame.Flags = header[FlagsOffset];
|
||||||
frame.StreamId = (int)Bitshifter.ReadUInt31BigEndian(header.Slice(StreamIdOffset));
|
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
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,20 +59,16 @@ namespace System.IO.Pipelines
|
||||||
AvailableMemory = arrayPoolBuffer;
|
AvailableMemory = arrayPoolBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetUnownedMemory(Memory<byte> memory)
|
|
||||||
{
|
|
||||||
AvailableMemory = memory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ResetMemory()
|
public void ResetMemory()
|
||||||
{
|
{
|
||||||
if (_memoryOwner is IMemoryOwner<byte> owner)
|
if (_memoryOwner is IMemoryOwner<byte> owner)
|
||||||
{
|
{
|
||||||
owner.Dispose();
|
owner.Dispose();
|
||||||
}
|
}
|
||||||
else if (_memoryOwner is byte[] array)
|
else
|
||||||
{
|
{
|
||||||
ArrayPool<byte>.Shared.Return(array);
|
byte[] poolArray = (byte[])_memoryOwner;
|
||||||
|
ArrayPool<byte>.Shared.Return(poolArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order of below field clears is significant as it clears in a sequential order
|
// Order of below field clears is significant as it clears in a sequential order
|
||||||
|
|
|
||||||
|
|
@ -341,8 +341,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeW
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// We can't use the pool so allocate an array
|
// We can't use the recommended pool so use the ArrayPool
|
||||||
newSegment.SetUnownedMemory(new byte[sizeHint]);
|
newSegment.SetOwnedMemory(ArrayPool<byte>.Shared.Rent(sizeHint));
|
||||||
}
|
}
|
||||||
|
|
||||||
_tailMemory = newSegment.AvailableMemory;
|
_tailMemory = newSegment.AvailableMemory;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
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.Core.Internal.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
|
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
|
||||||
|
|
@ -755,6 +756,176 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
};
|
};
|
||||||
|
|
||||||
testContext.InitializeHeartbeat();
|
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<byte>(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<string>(), It.IsAny<string>()), Times.Never());
|
||||||
|
mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny<string>()), 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<IKestrelTrace>();
|
||||||
|
|
||||||
|
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<string>(), It.IsAny<string>()), Times.Never());
|
||||||
|
mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny<string>()), 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<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var mockKestrelTrace = new Mock<IKestrelTrace>();
|
||||||
|
|
||||||
|
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));
|
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.
|
// Make sure consuming a single chunk exceeds the 2 second timeout.
|
||||||
var targetBytesPerSecond = chunkSize / 4;
|
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 appFuncCompleted.Task.DefaultTimeout();
|
||||||
}
|
}
|
||||||
await server.StopAsync();
|
await server.StopAsync();
|
||||||
|
|
@ -800,87 +977,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
Assert.False(requestAborted);
|
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<IKestrelTrace>();
|
|
||||||
|
|
||||||
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<string>(), It.IsAny<string>()), Times.Never());
|
|
||||||
mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny<string>()), Times.Once());
|
|
||||||
Assert.False(requestAborted);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AssertStreamAborted(Stream stream, int totalBytes)
|
private async Task AssertStreamAborted(Stream stream, int totalBytes)
|
||||||
{
|
{
|
||||||
var receiveBuffer = new byte[64 * 1024];
|
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.");
|
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 receiveBuffer = new byte[64 * 1024];
|
||||||
var received = 0;
|
var received = 0;
|
||||||
|
|
@ -928,7 +1047,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
}
|
}
|
||||||
} while (received > 0);
|
} while (received > 0);
|
||||||
|
|
||||||
Assert.True(totalReceived >= minimumBytes, $"{nameof(AssertStreamCompleted)} Stream aborted prematurely.");
|
Assert.Equal(expectedBytes, totalReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TheoryData<string, StringValues, string> NullHeaderData
|
public static TheoryData<string, StringValues, string> NullHeaderData
|
||||||
|
|
|
||||||
|
|
@ -1112,12 +1112,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
var buffer = result.Buffer;
|
var buffer = result.Buffer;
|
||||||
var consumed = buffer.Start;
|
var consumed = buffer.Start;
|
||||||
var examined = buffer.Start;
|
var examined = buffer.Start;
|
||||||
|
var copyBuffer = buffer;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Assert.True(buffer.Length > 0);
|
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;
|
consumed = examined = framePayload.End;
|
||||||
frame.Payload = framePayload.ToArray();
|
frame.Payload = framePayload.ToArray();
|
||||||
|
|
@ -1135,7 +1136,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_bytesReceived += buffer.Slice(buffer.Start, consumed).Length;
|
_bytesReceived += copyBuffer.Slice(copyBuffer.Start, consumed).Length;
|
||||||
_pair.Application.Input.AdvanceTo(consumed, examined);
|
_pair.Application.Input.AdvanceTo(consumed, examined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.Http2
|
||||||
|
|
||||||
try
|
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;
|
consumed = examined = framePayload.End;
|
||||||
return frame;
|
return frame;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
|
||||||
internal static class ErrorCodes
|
internal static class ErrorCodes
|
||||||
{
|
{
|
||||||
internal const uint ERROR_SUCCESS = 0;
|
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_HANDLE_EOF = 38;
|
||||||
internal const uint ERROR_NOT_SUPPORTED = 50;
|
internal const uint ERROR_NOT_SUPPORTED = 50;
|
||||||
internal const uint ERROR_INVALID_PARAMETER = 87;
|
internal const uint ERROR_INVALID_PARAMETER = 87;
|
||||||
|
|
|
||||||
|
|
@ -539,7 +539,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public IAsyncEnumerable<TResult> StreamAsyncCore<TResult>(string methodName, object[] args, CancellationToken cancellationToken = default)
|
public IAsyncEnumerable<TResult> StreamAsyncCore<TResult>(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<TResult>(methodName, args, cts);
|
var stream = CastIAsyncEnumerable<TResult>(methodName, args, cts);
|
||||||
var cancelableStream = AsyncEnumerableAdapters.MakeCancelableTypedAsyncEnumerable(stream, cts);
|
var cancelableStream = AsyncEnumerableAdapters.MakeCancelableTypedAsyncEnumerable(stream, cts);
|
||||||
return cancelableStream;
|
return cancelableStream;
|
||||||
|
|
|
||||||
|
|
@ -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<Startup>(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<Exception>(() => 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<HubConnectionTests>().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<Startup>(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<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||||
public async Task CanSendAndReceiveMessage(string protocolName, HttpTransportType transportType, string path)
|
public async Task CanSendAndReceiveMessage(string protocolName, HttpTransportType transportType, string path)
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,16 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
||||||
|
|
||||||
endpoints.MapHub<TestHub>("/default-nowebsockets", options => options.Transports = HttpTransportType.LongPolling | HttpTransportType.ServerSentEvents);
|
endpoints.MapHub<TestHub>("/default-nowebsockets", options => options.Transports = HttpTransportType.LongPolling | HttpTransportType.ServerSentEvents);
|
||||||
|
|
||||||
|
endpoints.MapHub<TestHub>("/negotiateProtocolVersion12", options =>
|
||||||
|
{
|
||||||
|
options.MinimumProtocolVersion = 12;
|
||||||
|
});
|
||||||
|
|
||||||
|
endpoints.MapHub<TestHub>("/negotiateProtocolVersionNegative", options =>
|
||||||
|
{
|
||||||
|
options.MinimumProtocolVersion = -1;
|
||||||
|
});
|
||||||
|
|
||||||
endpoints.MapGet("/generateJwtToken", context =>
|
endpoints.MapGet("/generateJwtToken", context =>
|
||||||
{
|
{
|
||||||
return context.Response.WriteAsync(GenerateJwtToken());
|
return context.Response.WriteAsync(GenerateJwtToken());
|
||||||
|
|
|
||||||
|
|
@ -359,7 +359,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
var httpHandler = new TestHttpMessageHandler();
|
var httpHandler = new TestHttpMessageHandler();
|
||||||
|
|
||||||
var connectResponseTcs = new TaskCompletionSource<object>();
|
var connectResponseTcs = new TaskCompletionSource<object>();
|
||||||
httpHandler.OnGet("/?id=00000000-0000-0000-0000-000000000000", async (_, __) =>
|
httpHandler.OnGet("/?negotiateVersion=1&id=00000000-0000-0000-0000-000000000000", async (_, __) =>
|
||||||
{
|
{
|
||||||
await connectResponseTcs.Task;
|
await connectResponseTcs.Task;
|
||||||
return ResponseUtils.CreateResponse(HttpStatusCode.Accepted);
|
return ResponseUtils.CreateResponse(HttpStatusCode.Accepted);
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
return RunInvalidNegotiateResponseTest<FormatException>(ResponseUtils.CreateNegotiationContent(connectionId: string.Empty), "Invalid connection id.");
|
return RunInvalidNegotiateResponseTest<FormatException>(ResponseUtils.CreateNegotiationContent(connectionId: string.Empty), "Invalid connection id.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task NegotiateResponseWithNegotiateVersionRequiresConnectionToken()
|
||||||
|
{
|
||||||
|
return RunInvalidNegotiateResponseTest<InvalidDataException>(ResponseUtils.CreateNegotiationContent(negotiateVersion: 1, connectionToken: null), "Invalid negotiation response received.");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task ConnectionCannotBeStartedIfNoCommonTransportsBetweenClientAndServer()
|
public Task ConnectionCannotBeStartedIfNoCommonTransportsBetweenClientAndServer()
|
||||||
{
|
{
|
||||||
|
|
@ -50,12 +56,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("http://fakeuri.org/", "http://fakeuri.org/negotiate")]
|
[InlineData("http://fakeuri.org/", "http://fakeuri.org/negotiate?negotiateVersion=1")]
|
||||||
[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&negotiateVersion=1")]
|
||||||
[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&negotiateVersion=1")]
|
||||||
[InlineData("http://fakeuri.org/endpoint", "http://fakeuri.org/endpoint/negotiate")]
|
[InlineData("http://fakeuri.org/endpoint", "http://fakeuri.org/endpoint/negotiate?negotiateVersion=1")]
|
||||||
[InlineData("http://fakeuri.org/endpoint/", "http://fakeuri.org/endpoint/negotiate")]
|
[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")]
|
[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)
|
public async Task CorrectlyHandlesQueryStringWhenAppendingNegotiateToUrl(string requestedUrl, string expectedNegotiate)
|
||||||
{
|
{
|
||||||
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
|
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
|
||||||
|
|
@ -119,6 +125,124 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
|
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]
|
[Fact]
|
||||||
public async Task NegotiateThatReturnsUrlGetFollowed()
|
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("http://fakeuri.org/negotiate?negotiateVersion=1", 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/negotiate?negotiateVersion=1", 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?negotiateVersion=1&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("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
|
||||||
Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
|
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("http://fakeuri.org/negotiate?negotiateVersion=1", 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/negotiate?negotiateVersion=1", 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?negotiateVersion=1&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("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
|
||||||
// Delete request
|
// Delete request
|
||||||
Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
|
Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string CreateNegotiationContent(string connectionId = "00000000-0000-0000-0000-000000000000",
|
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<object>();
|
var availableTransports = new List<object>();
|
||||||
|
|
||||||
|
|
@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonConvert.SerializeObject(new { connectionId, availableTransports });
|
return JsonConvert.SerializeObject(new { connectionId, availableTransports, connectionToken, negotiateVersion });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
@ -117,7 +120,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
});
|
});
|
||||||
testHttpMessageHandler.OnRequest((request, next, cancellationToken) =>
|
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();
|
deleteCts.Cancel();
|
||||||
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
|
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
|
||||||
// Not configurable on purpose, high enough that if we reach here, it's likely
|
// Not configurable on purpose, high enough that if we reach here, it's likely
|
||||||
// a buggy server
|
// a buggy server
|
||||||
private static readonly int _maxRedirects = 100;
|
private static readonly int _maxRedirects = 100;
|
||||||
|
private static readonly int _protocolVersionNumber = 1;
|
||||||
private static readonly Task<string> _noAccessToken = Task.FromResult<string>(null);
|
private static readonly Task<string> _noAccessToken = Task.FromResult<string>(null);
|
||||||
|
|
||||||
private static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(120);
|
private static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(120);
|
||||||
|
|
@ -41,6 +42,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
|
||||||
private readonly HttpConnectionOptions _httpConnectionOptions;
|
private readonly HttpConnectionOptions _httpConnectionOptions;
|
||||||
private ITransport _transport;
|
private ITransport _transport;
|
||||||
private readonly ITransportFactory _transportFactory;
|
private readonly ITransportFactory _transportFactory;
|
||||||
|
private string _connectionToken;
|
||||||
private string _connectionId;
|
private string _connectionId;
|
||||||
private readonly ConnectionLogScope _logScope;
|
private readonly ConnectionLogScope _logScope;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
|
@ -341,7 +343,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should only need to happen once
|
// 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
|
// 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
|
// 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)
|
if (negotiationResponse == null)
|
||||||
{
|
{
|
||||||
negotiationResponse = await GetNegotiationResponseAsync(uri, cancellationToken);
|
negotiationResponse = await GetNegotiationResponseAsync(uri, cancellationToken);
|
||||||
connectUrl = CreateConnectUrl(uri, negotiationResponse.ConnectionId);
|
connectUrl = CreateConnectUrl(uri, _connectionToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.StartingTransport(_logger, transportType, connectUrl);
|
Log.StartingTransport(_logger, transportType, connectUrl);
|
||||||
|
|
@ -428,8 +430,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
|
||||||
urlBuilder.Path += "/";
|
urlBuilder.Path += "/";
|
||||||
}
|
}
|
||||||
urlBuilder.Path += "negotiate";
|
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
|
// Corefx changed the default version and High Sierra curlhandler tries to upgrade request
|
||||||
request.Version = new Version(1, 1);
|
request.Version = new Version(1, 1);
|
||||||
|
|
@ -466,7 +469,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
|
||||||
throw new FormatException("Invalid connection id.");
|
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)
|
private async Task StartTransport(Uri connectUrl, HttpTransportType transportType, TransferFormat transferFormat, CancellationToken cancellationToken)
|
||||||
|
|
@ -627,7 +630,19 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
|
||||||
private async Task<NegotiationResponse> GetNegotiationResponseAsync(Uri uri, CancellationToken cancellationToken)
|
private async Task<NegotiationResponse> GetNegotiationResponseAsync(Uri uri, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var negotiationResponse = await NegotiateAsync(uri, _httpClient, _logger, 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;
|
_logScope.ConnectionId = _connectionId;
|
||||||
return negotiationResponse;
|
return negotiationResponse;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ public class UserAgentHelper {
|
||||||
// Parsing version numbers
|
// Parsing version numbers
|
||||||
String detailedVersion = Version.getDetailedVersion();
|
String detailedVersion = Version.getDetailedVersion();
|
||||||
agentBuilder.append(getVersion(detailedVersion));
|
agentBuilder.append(getVersion(detailedVersion));
|
||||||
agentBuilder.append("; (");
|
agentBuilder.append(" (");
|
||||||
agentBuilder.append(detailedVersion);
|
agentBuilder.append(detailedVersion);
|
||||||
agentBuilder.append("; ");
|
agentBuilder.append("; ");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1116,7 +1116,7 @@ class HubConnectionTest {
|
||||||
hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
|
hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
|
||||||
|
|
||||||
AtomicBoolean done = new AtomicBoolean();
|
AtomicBoolean done = new AtomicBoolean();
|
||||||
Single<String> result = hubConnection.invoke(String.class, "fixedMessage", null);
|
Single<String> result = hubConnection.invoke(String.class, "fixedMessage", (Object)null);
|
||||||
result.doOnSuccess(value -> done.set(true)).subscribe();
|
result.doOnSuccess(value -> done.set(true)).subscribe();
|
||||||
assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"fixedMessage\",\"arguments\":[null]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]);
|
assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"fixedMessage\",\"arguments\":[null]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]);
|
||||||
assertFalse(done.get());
|
assertFalse(done.get());
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ public class UserAgentTest {
|
||||||
|
|
||||||
String detailedVersion = Version.getDetailedVersion();
|
String detailedVersion = Version.getDetailedVersion();
|
||||||
String handMadeUserAgent = "Microsoft SignalR/" + UserAgentHelper.getVersion(detailedVersion) +
|
String handMadeUserAgent = "Microsoft SignalR/" + UserAgentHelper.getVersion(detailedVersion) +
|
||||||
"; (" + detailedVersion + "; " + UserAgentHelper.getOS() + "; Java; " +
|
" (" + detailedVersion + "; " + UserAgentHelper.getOS() + "; Java; " +
|
||||||
UserAgentHelper.getJavaVersion() + "; " + UserAgentHelper.getJavaVendor() + ")";
|
UserAgentHelper.getJavaVersion() + "; " + UserAgentHelper.getJavaVendor() + ")";
|
||||||
|
|
||||||
assertEquals(handMadeUserAgent, userAgent);
|
assertEquals(handMadeUserAgent, userAgent);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
// 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.
|
// 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 { 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
|
// On slower CI machines, these tests sometimes take longer than 5s
|
||||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 20 * 1000;
|
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 {
|
export function getGlobalObject(): any {
|
||||||
return typeof window !== "undefined" ? window : global;
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
// tslint:disable:no-floating-promises
|
// tslint:disable:no-floating-promises
|
||||||
|
|
||||||
import { HttpTransportType, IHttpConnectionOptions, TransferFormat } from "@microsoft/signalr";
|
import { HttpTransportType, IHttpConnectionOptions, TransferFormat } from "@microsoft/signalr";
|
||||||
import { eachTransport, ECHOENDPOINT_URL } from "./Common";
|
import { eachHttpClient, eachTransport, ECHOENDPOINT_URL } from "./Common";
|
||||||
import { TestLogger } from "./TestLogger";
|
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.
|
// 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) => {
|
eachTransport((transportType) => {
|
||||||
describe(`over ${HttpTransportType[transportType]}`, () => {
|
eachHttpClient((httpClient) => {
|
||||||
it("can send and receive messages", (done) => {
|
describe(`over ${HttpTransportType[transportType]} with ${(httpClient.constructor as any).name}`, () => {
|
||||||
const message = "Hello World!";
|
it("can send and receive messages", (done) => {
|
||||||
// the url should be resolved relative to the document.location.host
|
const message = "Hello World!";
|
||||||
// and the leading '/' should be automatically added to the url
|
// the url should be resolved relative to the document.location.host
|
||||||
const connection = new HttpConnection(ECHOENDPOINT_URL, {
|
// and the leading '/' should be automatically added to the url
|
||||||
...commonOptions,
|
const connection = new HttpConnection(ECHOENDPOINT_URL, {
|
||||||
transport: transportType,
|
...commonOptions,
|
||||||
});
|
httpClient,
|
||||||
|
transport: transportType,
|
||||||
|
});
|
||||||
|
|
||||||
connection.onreceive = (data: any) => {
|
connection.onreceive = (data: any) => {
|
||||||
if (data === message) {
|
if (data === message) {
|
||||||
connection.stop();
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// One match for send, one for receive.
|
connection.onclose = (error: any) => {
|
||||||
expect(matches).toEqual(2);
|
expect(error).toBeUndefined();
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
|
|
||||||
connection.start(TransferFormat.Text).then(() => {
|
connection.start(TransferFormat.Text).then(() => {
|
||||||
connection.send(message);
|
connection.send(message);
|
||||||
}).catch((e: any) => {
|
}).catch((e: any) => {
|
||||||
fail(e);
|
fail(e);
|
||||||
done();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
import { AbortError, DefaultHttpClient, HttpClient, HttpRequest, HttpResponse, HttpTransportType, HubConnectionBuilder, IHttpConnectionOptions, JsonHubProtocol, NullLogger } from "@microsoft/signalr";
|
import { AbortError, DefaultHttpClient, HttpClient, HttpRequest, HttpResponse, HttpTransportType, HubConnectionBuilder, IHttpConnectionOptions, JsonHubProtocol, NullLogger } from "@microsoft/signalr";
|
||||||
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
|
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 "./LogBannerReporter";
|
||||||
import { TestLogger } from "./TestLogger";
|
import { TestLogger } from "./TestLogger";
|
||||||
|
|
||||||
|
|
@ -49,12 +49,12 @@ function getConnectionBuilder(transportType?: HttpTransportType, url?: string, o
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("hubConnection", () => {
|
describe("hubConnection", () => {
|
||||||
eachTransportAndProtocol((transportType, protocol) => {
|
eachTransportAndProtocolAndHttpClient((transportType, protocol, httpClient) => {
|
||||||
describe("using " + protocol.name + " over " + HttpTransportType[transportType] + " transport", () => {
|
describe("using " + protocol.name + " over " + HttpTransportType[transportType] + " transport", () => {
|
||||||
it("can invoke server method and receive result", (done) => {
|
it("can invoke server method and receive result", (done) => {
|
||||||
const message = "你好,世界!";
|
const message = "你好,世界!";
|
||||||
|
|
||||||
const hubConnection = getConnectionBuilder(transportType)
|
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
|
||||||
.withHubProtocol(protocol)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@ describe("hubConnection", () => {
|
||||||
it("using https, can invoke server method and receive result", (done) => {
|
it("using https, can invoke server method and receive result", (done) => {
|
||||||
const message = "你好,世界!";
|
const message = "你好,世界!";
|
||||||
|
|
||||||
const hubConnection = getConnectionBuilder(transportType, TESTHUBENDPOINT_HTTPS_URL)
|
const hubConnection = getConnectionBuilder(transportType, TESTHUBENDPOINT_HTTPS_URL, { httpClient })
|
||||||
.withHubProtocol(protocol)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -108,7 +108,7 @@ describe("hubConnection", () => {
|
||||||
it("can invoke server method non-blocking and not receive result", (done) => {
|
it("can invoke server method non-blocking and not receive result", (done) => {
|
||||||
const message = "你好,世界!";
|
const message = "你好,世界!";
|
||||||
|
|
||||||
const hubConnection = getConnectionBuilder(transportType)
|
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
|
||||||
.withHubProtocol(protocol)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -130,7 +130,7 @@ describe("hubConnection", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can invoke server method structural object and receive structural result", (done) => {
|
it("can invoke server method structural object and receive structural result", (done) => {
|
||||||
const hubConnection = getConnectionBuilder(transportType)
|
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
|
||||||
.withHubProtocol(protocol)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -154,7 +154,7 @@ describe("hubConnection", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can stream server method and receive result", (done) => {
|
it("can stream server method and receive result", (done) => {
|
||||||
const hubConnection = getConnectionBuilder(transportType)
|
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
|
||||||
.withHubProtocol(protocol)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -185,7 +185,7 @@ describe("hubConnection", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can stream server method and cancel stream", (done) => {
|
it("can stream server method and cancel stream", (done) => {
|
||||||
const hubConnection = getConnectionBuilder(transportType)
|
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
|
||||||
.withHubProtocol(protocol)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -219,7 +219,7 @@ describe("hubConnection", () => {
|
||||||
|
|
||||||
it("rethrows an exception from the server when invoking", (done) => {
|
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 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)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -241,7 +241,7 @@ describe("hubConnection", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws an exception when invoking streaming method with invoke", (done) => {
|
it("throws an exception when invoking streaming method with invoke", (done) => {
|
||||||
const hubConnection = getConnectionBuilder(transportType)
|
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
|
||||||
.withHubProtocol(protocol)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -263,7 +263,7 @@ describe("hubConnection", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws an exception when receiving a streaming result for method called with invoke", (done) => {
|
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)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -286,7 +286,7 @@ describe("hubConnection", () => {
|
||||||
|
|
||||||
it("rethrows an exception from the server when streaming", (done) => {
|
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 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)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -313,7 +313,7 @@ describe("hubConnection", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws an exception when invoking hub method with stream", (done) => {
|
it("throws an exception when invoking hub method with stream", (done) => {
|
||||||
const hubConnection = getConnectionBuilder(transportType)
|
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
|
||||||
.withHubProtocol(protocol)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -340,7 +340,7 @@ describe("hubConnection", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can receive server calls", (done) => {
|
it("can receive server calls", (done) => {
|
||||||
const hubConnection = getConnectionBuilder(transportType)
|
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
|
||||||
.withHubProtocol(protocol)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -370,7 +370,7 @@ describe("hubConnection", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can receive server calls without rebinding handler when restarted", (done) => {
|
it("can receive server calls without rebinding handler when restarted", (done) => {
|
||||||
const hubConnection = getConnectionBuilder(transportType)
|
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
|
||||||
.withHubProtocol(protocol)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -425,7 +425,7 @@ describe("hubConnection", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("closed with error or start fails if hub cannot be created", async (done) => {
|
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)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -446,7 +446,7 @@ describe("hubConnection", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can handle different types", (done) => {
|
it("can handle different types", (done) => {
|
||||||
const hubConnection = getConnectionBuilder(transportType)
|
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
|
||||||
.withHubProtocol(protocol)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -489,7 +489,7 @@ describe("hubConnection", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can receive different types", (done) => {
|
it("can receive different types", (done) => {
|
||||||
const hubConnection = getConnectionBuilder(transportType)
|
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
|
||||||
.withHubProtocol(protocol)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -534,7 +534,7 @@ describe("hubConnection", () => {
|
||||||
it("can be restarted", (done) => {
|
it("can be restarted", (done) => {
|
||||||
const message = "你好,世界!";
|
const message = "你好,世界!";
|
||||||
|
|
||||||
const hubConnection = getConnectionBuilder(transportType)
|
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
|
||||||
.withHubProtocol(protocol)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -577,7 +577,7 @@ describe("hubConnection", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can stream from client to server with rxjs", async (done) => {
|
it("can stream from client to server with rxjs", async (done) => {
|
||||||
const hubConnection = getConnectionBuilder(transportType)
|
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
|
||||||
.withHubProtocol(protocol)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -594,7 +594,7 @@ describe("hubConnection", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can stream from client to server and close with error with rxjs", async (done) => {
|
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)
|
.withHubProtocol(protocol)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,9 @@
|
||||||
<ProjectReference Include="..\signalr\signalr.npmproj" />
|
<ProjectReference Include="..\signalr\signalr.npmproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<BuildOutputFiles Include="dist\browser\signalr-protocol-msgpack.js" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Directory.Build.targets))\Directory.Build.targets" />
|
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Directory.Build.targets))\Directory.Build.targets" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -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
|
## Installation
|
||||||
|
|
||||||
|
|
@ -14,6 +14,8 @@ yarn add @microsoft/signalr
|
||||||
|
|
||||||
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.
|
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
|
### Browser
|
||||||
|
|
||||||
To use the client in a browser, copy `*.js` files from the `dist/browser` folder to your script folder include on your page using the `<script>` tag.
|
To use the client in a browser, copy `*.js` files from the `dist/browser` folder to your script folder include on your page using the `<script>` tag.
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,10 @@
|
||||||
<IsShippingPackage>true</IsShippingPackage>
|
<IsShippingPackage>true</IsShippingPackage>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<BuildOutputFiles Include="dist\browser\signalr.js" />
|
||||||
|
<BuildOutputFiles Include="dist\webworker\signalr.js" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Directory.Build.targets))\Directory.Build.targets" />
|
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Directory.Build.targets))\Directory.Build.targets" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
import { AbortError } from "./Errors";
|
import { AbortError } from "./Errors";
|
||||||
|
import { FetchHttpClient } from "./FetchHttpClient";
|
||||||
import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
|
import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
|
||||||
import { ILogger } from "./ILogger";
|
import { ILogger } from "./ILogger";
|
||||||
import { NodeHttpClient } from "./NodeHttpClient";
|
import { NodeHttpClient } from "./NodeHttpClient";
|
||||||
|
|
@ -15,7 +16,9 @@ export class DefaultHttpClient extends HttpClient {
|
||||||
public constructor(logger: ILogger) {
|
public constructor(logger: ILogger) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
if (typeof XMLHttpRequest !== "undefined") {
|
if (typeof fetch !== "undefined") {
|
||||||
|
this.httpClient = new FetchHttpClient(logger);
|
||||||
|
} else if (typeof XMLHttpRequest !== "undefined") {
|
||||||
this.httpClient = new XhrHttpClient(logger);
|
this.httpClient = new XhrHttpClient(logger);
|
||||||
} else {
|
} else {
|
||||||
this.httpClient = new NodeHttpClient(logger);
|
this.httpClient = new NodeHttpClient(logger);
|
||||||
|
|
|
||||||
|
|
@ -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<HttpResponse> {
|
||||||
|
// 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<string | ArrayBuffer> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,14 @@ export class HttpResponse {
|
||||||
* @param {ArrayBuffer} content The content of the response.
|
* @param {ArrayBuffer} content The content of the response.
|
||||||
*/
|
*/
|
||||||
constructor(statusCode: number, statusText: string, content: ArrayBuffer);
|
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(
|
constructor(
|
||||||
public readonly statusCode: number,
|
public readonly statusCode: number,
|
||||||
public readonly statusText?: string,
|
public readonly statusText?: string,
|
||||||
|
|
|
||||||
|
|
@ -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 string AccessToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
public System.Collections.Generic.IList<Microsoft.AspNetCore.Http.Connections.AvailableTransport> AvailableTransports { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public System.Collections.Generic.IList<Microsoft.AspNetCore.Http.Connections.AvailableTransport> 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 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 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 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 { } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 string AccessToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
public System.Collections.Generic.IList<Microsoft.AspNetCore.Http.Connections.AvailableTransport> AvailableTransports { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public System.Collections.Generic.IList<Microsoft.AspNetCore.Http.Connections.AvailableTransport> 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 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 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 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 { } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
{
|
{
|
||||||
private const string ConnectionIdPropertyName = "connectionId";
|
private const string ConnectionIdPropertyName = "connectionId";
|
||||||
private static JsonEncodedText ConnectionIdPropertyNameBytes = JsonEncodedText.Encode(ConnectionIdPropertyName);
|
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 const string UrlPropertyName = "url";
|
||||||
private static JsonEncodedText UrlPropertyNameBytes = JsonEncodedText.Encode(UrlPropertyName);
|
private static JsonEncodedText UrlPropertyNameBytes = JsonEncodedText.Encode(UrlPropertyName);
|
||||||
private const string AccessTokenPropertyName = "accessToken";
|
private const string AccessTokenPropertyName = "accessToken";
|
||||||
|
|
@ -27,6 +29,8 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
private static JsonEncodedText TransferFormatsPropertyNameBytes = JsonEncodedText.Encode(TransferFormatsPropertyName);
|
private static JsonEncodedText TransferFormatsPropertyNameBytes = JsonEncodedText.Encode(TransferFormatsPropertyName);
|
||||||
private const string ErrorPropertyName = "error";
|
private const string ErrorPropertyName = "error";
|
||||||
private static JsonEncodedText ErrorPropertyNameBytes = JsonEncodedText.Encode(ErrorPropertyName);
|
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<byte> optimization for static data https://vcsjones.com/2019/02/01/csharp-readonly-span-bytes-static/
|
// Use C#7.3's ReadOnlySpan<byte> optimization for static data https://vcsjones.com/2019/02/01/csharp-readonly-span-bytes-static/
|
||||||
// Used to detect ASP.NET SignalR Server connection attempt
|
// Used to detect ASP.NET SignalR Server connection attempt
|
||||||
|
|
@ -41,6 +45,19 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
var writer = reusableWriter.GetJsonWriter();
|
var writer = reusableWriter.GetJsonWriter();
|
||||||
writer.WriteStartObject();
|
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))
|
if (!string.IsNullOrEmpty(response.Url))
|
||||||
{
|
{
|
||||||
writer.WriteString(UrlPropertyNameBytes, response.Url);
|
writer.WriteString(UrlPropertyNameBytes, response.Url);
|
||||||
|
|
@ -56,6 +73,11 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
writer.WriteString(ConnectionIdPropertyNameBytes, response.ConnectionId);
|
writer.WriteString(ConnectionIdPropertyNameBytes, response.ConnectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.Version > 0 && !string.IsNullOrEmpty(response.ConnectionToken))
|
||||||
|
{
|
||||||
|
writer.WriteString(ConnectionTokenPropertyNameBytes, response.ConnectionToken);
|
||||||
|
}
|
||||||
|
|
||||||
writer.WriteStartArray(AvailableTransportsPropertyNameBytes);
|
writer.WriteStartArray(AvailableTransportsPropertyNameBytes);
|
||||||
|
|
||||||
if (response.AvailableTransports != null)
|
if (response.AvailableTransports != null)
|
||||||
|
|
@ -112,10 +134,12 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
reader.EnsureObjectStart();
|
reader.EnsureObjectStart();
|
||||||
|
|
||||||
string connectionId = null;
|
string connectionId = null;
|
||||||
|
string connectionToken = null;
|
||||||
string url = null;
|
string url = null;
|
||||||
string accessToken = null;
|
string accessToken = null;
|
||||||
List<AvailableTransport> availableTransports = null;
|
List<AvailableTransport> availableTransports = null;
|
||||||
string error = null;
|
string error = null;
|
||||||
|
int version = 0;
|
||||||
|
|
||||||
var completed = false;
|
var completed = false;
|
||||||
while (!completed && reader.CheckRead())
|
while (!completed && reader.CheckRead())
|
||||||
|
|
@ -135,6 +159,14 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
{
|
{
|
||||||
connectionId = reader.ReadAsString(ConnectionIdPropertyName);
|
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))
|
else if (reader.ValueTextEquals(AvailableTransportsPropertyNameBytes.EncodedUtf8Bytes))
|
||||||
{
|
{
|
||||||
reader.CheckRead();
|
reader.CheckRead();
|
||||||
|
|
@ -182,6 +214,14 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
throw new InvalidDataException($"Missing required property '{ConnectionIdPropertyName}'.");
|
throw new InvalidDataException($"Missing required property '{ConnectionIdPropertyName}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version > 0)
|
||||||
|
{
|
||||||
|
if (connectionToken == null)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Missing required property '{ConnectionTokenPropertyName}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (availableTransports == null)
|
if (availableTransports == null)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException($"Missing required property '{AvailableTransportsPropertyName}'.");
|
throw new InvalidDataException($"Missing required property '{AvailableTransportsPropertyName}'.");
|
||||||
|
|
@ -191,10 +231,12 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
return new NegotiationResponse
|
return new NegotiationResponse
|
||||||
{
|
{
|
||||||
ConnectionId = connectionId,
|
ConnectionId = connectionId,
|
||||||
|
ConnectionToken = connectionToken,
|
||||||
Url = url,
|
Url = url,
|
||||||
AccessToken = accessToken,
|
AccessToken = accessToken,
|
||||||
AvailableTransports = availableTransports,
|
AvailableTransports = availableTransports,
|
||||||
Error = error,
|
Error = error,
|
||||||
|
Version = version
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
public string AccessToken { get; set; }
|
public string AccessToken { get; set; }
|
||||||
public string ConnectionId { get; set; }
|
public string ConnectionId { get; set; }
|
||||||
|
public string ConnectionToken { get; set; }
|
||||||
|
public int Version { get; set; }
|
||||||
public IList<AvailableTransport> AvailableTransports { get; set; }
|
public IList<AvailableTransport> AvailableTransports { get; set; }
|
||||||
public string Error { get; set; }
|
public string Error { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 long ApplicationMaxBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
public System.Collections.Generic.IList<Microsoft.AspNetCore.Authorization.IAuthorizeData> AuthorizationData { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
public System.Collections.Generic.IList<Microsoft.AspNetCore.Authorization.IAuthorizeData> AuthorizationData { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||||
public Microsoft.AspNetCore.Http.Connections.LongPollingOptions LongPolling { [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 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.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; } }
|
public Microsoft.AspNetCore.Http.Connections.WebSocketOptions WebSockets { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||||
|
|
|
||||||
|
|
@ -57,5 +57,11 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
/// Gets or sets the maximum buffer size of the application writer.
|
/// Gets or sets the maximum buffer size of the application writer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long ApplicationMaxBufferSize { get; set; }
|
public long ApplicationMaxBufferSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the minimum protocol verison supported by the server.
|
||||||
|
/// The default value is 0, the lowest possible protocol version.
|
||||||
|
/// </summary>
|
||||||
|
public int MinimumProtocolVersion { get; set; } = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,13 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
/// Creates the DefaultConnectionContext without Pipes to avoid upfront allocations.
|
/// Creates the DefaultConnectionContext without Pipes to avoid upfront allocations.
|
||||||
/// The caller is expected to set the <see cref="Transport"/> and <see cref="Application"/> pipes manually.
|
/// The caller is expected to set the <see cref="Transport"/> and <see cref="Application"/> pipes manually.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id"></param>
|
/// <param name="connectionId"></param>
|
||||||
|
/// <param name="connectionToken"></param>
|
||||||
/// <param name="logger"></param>
|
/// <param name="logger"></param>
|
||||||
public HttpConnectionContext(string id, ILogger logger)
|
public HttpConnectionContext(string connectionId, string connectionToken, ILogger logger)
|
||||||
{
|
{
|
||||||
ConnectionId = id;
|
ConnectionId = connectionId;
|
||||||
|
ConnectionToken = connectionToken;
|
||||||
LastSeenUtc = DateTime.UtcNow;
|
LastSeenUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
// The default behavior is that both formats are supported.
|
// The default behavior is that both formats are supported.
|
||||||
|
|
@ -74,8 +76,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
Features.Set<IConnectionInherentKeepAliveFeature>(this);
|
Features.Set<IConnectionInherentKeepAliveFeature>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application, ILogger logger = null)
|
internal HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application, ILogger logger = null)
|
||||||
: this(id, logger)
|
: this(id, null, logger)
|
||||||
{
|
{
|
||||||
Transport = transport;
|
Transport = transport;
|
||||||
Application = application;
|
Application = application;
|
||||||
|
|
@ -113,6 +115,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
|
|
||||||
public override string ConnectionId { get; set; }
|
public override string ConnectionId { get; set; }
|
||||||
|
|
||||||
|
internal string ConnectionToken { get; set; }
|
||||||
|
|
||||||
public override IFeatureCollection Features { get; }
|
public override IFeatureCollection Features { get; }
|
||||||
|
|
||||||
public ClaimsPrincipal User { get; set; }
|
public ClaimsPrincipal User { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,12 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
private static readonly Action<ILogger, string, Exception> _failedToReadHttpRequestBody =
|
private static readonly Action<ILogger, string, Exception> _failedToReadHttpRequestBody =
|
||||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(14, "FailedToReadHttpRequestBody"), "Connection {TransportConnectionId} failed to read the HTTP request body.");
|
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(14, "FailedToReadHttpRequestBody"), "Connection {TransportConnectionId} failed to read the HTTP request body.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, int, Exception> _negotiateProtocolVersionMismatch =
|
||||||
|
LoggerMessage.Define<int>(LogLevel.Debug, new EventId(15, "NegotiateProtocolVersionMismatch"), "The client requested version '{clientProtocolVersion}', but the server does not support this version.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, string, Exception> _invalidNegotiateProtocolVersion =
|
||||||
|
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(16, "InvalidNegotiateProtocolVersion"), "The client requested an invalid protocol version '{queryStringVersionValue}'");
|
||||||
|
|
||||||
public static void ConnectionDisposed(ILogger logger, string connectionId)
|
public static void ConnectionDisposed(ILogger logger, string connectionId)
|
||||||
{
|
{
|
||||||
_connectionDisposed(logger, connectionId, null);
|
_connectionDisposed(logger, connectionId, null);
|
||||||
|
|
@ -121,6 +127,16 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
{
|
{
|
||||||
_failedToReadHttpRequestBody(logger, connectionId, ex);
|
_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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
private readonly HttpConnectionManager _manager;
|
private readonly HttpConnectionManager _manager;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private static readonly int _protocolVersion = 1;
|
||||||
|
|
||||||
public HttpConnectionDispatcher(HttpConnectionManager manager, ILoggerFactory loggerFactory)
|
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
|
// 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
|
// the Connection ID metadata. If this is the negotiate request then the Connection ID for the scope will
|
||||||
// be set a little later.
|
// 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))
|
using (_logger.BeginScope(logScope))
|
||||||
{
|
{
|
||||||
if (HttpMethods.IsPost(context.Request.Method))
|
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)
|
private async Task ProcessNegotiate(HttpContext context, HttpConnectionDispatcherOptions options, ConnectionLogScope logScope)
|
||||||
{
|
{
|
||||||
context.Response.ContentType = "application/json";
|
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
|
// 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
|
// Set the Connection ID on the logging scope so that logs from now on will have the
|
||||||
// Connection ID metadata set.
|
// 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
|
// Don't use thread static instance here because writer is used with async
|
||||||
var writer = new MemoryBufferWriter();
|
var writer = new MemoryBufferWriter();
|
||||||
|
|
@ -292,7 +317,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get the bytes for the connection id
|
// 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);
|
Log.NegotiationRequest(_logger);
|
||||||
|
|
||||||
|
|
@ -306,10 +331,46 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void WriteNegotiatePayload(IBufferWriter<byte> writer, string connectionId, HttpContext context, HttpConnectionDispatcherOptions options)
|
private void WriteNegotiatePayload(IBufferWriter<byte> writer, string connectionId, string connectionToken, HttpContext context, HttpConnectionDispatcherOptions options,
|
||||||
|
int clientProtocolVersion, string error)
|
||||||
{
|
{
|
||||||
var response = new NegotiationResponse();
|
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.ConnectionId = connectionId;
|
||||||
|
response.ConnectionToken = connectionToken;
|
||||||
response.AvailableTransports = new List<AvailableTransport>();
|
response.AvailableTransports = new List<AvailableTransport>();
|
||||||
|
|
||||||
if ((options.Transports & HttpTransportType.WebSockets) != 0 && ServerHasWebSockets(context.Features))
|
if ((options.Transports & HttpTransportType.WebSockets) != 0 && ServerHasWebSockets(context.Features))
|
||||||
|
|
@ -335,7 +396,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
return features.Get<IHttpWebSocketFeature>() != null;
|
return features.Get<IHttpWebSocketFeature>() != 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)
|
private async Task ProcessSend(HttpContext context, HttpConnectionDispatcherOptions options)
|
||||||
{
|
{
|
||||||
|
|
@ -608,9 +669,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
|
|
||||||
private async Task<HttpConnectionContext> GetConnectionAsync(HttpContext context)
|
private async Task<HttpConnectionContext> 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
|
// There's no connection ID: bad request
|
||||||
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||||
|
|
@ -619,7 +680,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_manager.TryGetConnection(connectionId, out var connection))
|
if (!_manager.TryGetConnection(connectionToken, out var connection))
|
||||||
{
|
{
|
||||||
// No connection with that ID: Not Found
|
// No connection with that ID: Not Found
|
||||||
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
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
|
// This is only used for WebSockets connections, which can connect directly without negotiating
|
||||||
private async Task<HttpConnectionContext> GetOrCreateConnectionAsync(HttpContext context, HttpConnectionDispatcherOptions options)
|
private async Task<HttpConnectionContext> GetOrCreateConnectionAsync(HttpContext context, HttpConnectionDispatcherOptions options)
|
||||||
{
|
{
|
||||||
var connectionId = GetConnectionId(context);
|
var connectionToken = GetConnectionToken(context);
|
||||||
HttpConnectionContext connection;
|
HttpConnectionContext connection;
|
||||||
|
|
||||||
// There's no connection id so this is a brand new connection
|
// There's no connection id so this is a brand new connection
|
||||||
if (StringValues.IsNullOrEmpty(connectionId))
|
if (StringValues.IsNullOrEmpty(connectionToken))
|
||||||
{
|
{
|
||||||
connection = CreateConnection(options);
|
connection = CreateConnection(options);
|
||||||
}
|
}
|
||||||
else if (!_manager.TryGetConnection(connectionId, out connection))
|
else if (!_manager.TryGetConnection(connectionToken, out connection))
|
||||||
{
|
{
|
||||||
// No connection with that ID: Not Found
|
// No connection with that ID: Not Found
|
||||||
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||||
|
|
@ -653,12 +714,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
return connection;
|
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 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);
|
var appPipeOptions = new PipeOptions(pauseWriterThreshold: options.ApplicationMaxBufferSize, resumeWriterThreshold: options.ApplicationMaxBufferSize / 2, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false);
|
||||||
|
return _manager.CreateConnection(transportPipeOptions, appPipeOptions, clientProtocolVersion);
|
||||||
return _manager.CreateConnection(transportPipeOptions, appPipeOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class EmptyServiceProvider : IServiceProvider
|
private class EmptyServiceProvider : IServiceProvider
|
||||||
|
|
|
||||||
|
|
@ -78,18 +78,28 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
/// Creates a connection without Pipes setup to allow saving allocations until Pipes are needed.
|
/// Creates a connection without Pipes setup to allow saving allocations until Pipes are needed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
internal HttpConnectionContext CreateConnection(PipeOptions transportPipeOptions, PipeOptions appPipeOptions)
|
internal HttpConnectionContext CreateConnection(PipeOptions transportPipeOptions, PipeOptions appPipeOptions, int negotiateVersion = 0)
|
||||||
{
|
{
|
||||||
|
string connectionToken;
|
||||||
var id = MakeNewConnectionId();
|
var id = MakeNewConnectionId();
|
||||||
|
if (negotiateVersion > 0)
|
||||||
|
{
|
||||||
|
connectionToken = MakeNewConnectionId();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
connectionToken = id;
|
||||||
|
}
|
||||||
|
|
||||||
Log.CreatedNewConnection(_logger, id);
|
Log.CreatedNewConnection(_logger, id);
|
||||||
var connectionTimer = HttpConnectionsEventSource.Log.ConnectionStart(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);
|
var pair = DuplexPipe.CreateConnectionPair(transportPipeOptions, appPipeOptions);
|
||||||
connection.Transport = pair.Application;
|
connection.Transport = pair.Application;
|
||||||
connection.Application = pair.Transport;
|
connection.Application = pair.Transport;
|
||||||
|
|
||||||
_connections.TryAdd(id, (connection, connectionTimer));
|
_connections.TryAdd(connectionToken, (connection, connectionTimer));
|
||||||
|
|
||||||
return connection;
|
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
|
// 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
|
// connections that might be in a hung state via the connections list
|
||||||
RemoveConnection(connection.ConnectionId);
|
RemoveConnection(connection.ConnectionToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
public class HttpConnectionDispatcherTests : VerifiableLoggedTest
|
public class HttpConnectionDispatcherTests : VerifiableLoggedTest
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task NegotiateReservesConnectionIdAndReturnsIt()
|
public async Task NegotiateVersionZeroReservesConnectionIdAndReturnsIt()
|
||||||
{
|
{
|
||||||
using (StartVerifiableLog())
|
using (StartVerifiableLog())
|
||||||
{
|
{
|
||||||
|
|
@ -54,8 +54,35 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions());
|
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions());
|
||||||
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
|
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
|
||||||
var connectionId = negotiateResponse.Value<string>("connectionId");
|
var connectionId = negotiateResponse.Value<string>("connectionId");
|
||||||
Assert.True(manager.TryGetConnection(connectionId, out var connectionContext));
|
var connectionToken = negotiateResponse.Value<string>("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<TestConnectionHandler>();
|
||||||
|
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<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
|
||||||
|
var connectionId = negotiateResponse.Value<string>("connectionId");
|
||||||
|
var connectionToken = negotiateResponse.Value<string>("connectionToken");
|
||||||
|
Assert.True(manager.TryGetConnection(connectionToken, out var connectionContext));
|
||||||
Assert.Equal(connectionId, connectionContext.ConnectionId);
|
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.Path = "/foo";
|
||||||
context.Request.Method = "POST";
|
context.Request.Method = "POST";
|
||||||
context.Response.Body = ms;
|
context.Response.Body = ms;
|
||||||
|
context.Request.QueryString = new QueryString("?negotiateVersion=1");
|
||||||
var options = new HttpConnectionDispatcherOptions { TransportMaxBufferSize = 4, ApplicationMaxBufferSize = 4 };
|
var options = new HttpConnectionDispatcherOptions { TransportMaxBufferSize = 4, ApplicationMaxBufferSize = 4 };
|
||||||
await dispatcher.ExecuteNegotiateAsync(context, options);
|
await dispatcher.ExecuteNegotiateAsync(context, options);
|
||||||
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
|
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
|
||||||
var connectionId = negotiateResponse.Value<string>("connectionId");
|
var connectionToken = negotiateResponse.Value<string>("connectionToken");
|
||||||
context.Request.QueryString = context.Request.QueryString.Add("id", connectionId);
|
context.Request.QueryString = context.Request.QueryString.Add("id", connectionToken);
|
||||||
Assert.True(manager.TryGetConnection(connectionId, out var connection));
|
Assert.True(manager.TryGetConnection(connectionToken, out var connection));
|
||||||
// Fake actual connection after negotiate to populate the pipes on the connection
|
// Fake actual connection after negotiate to populate the pipes on the connection
|
||||||
await dispatcher.ExecuteAsync(context, options, c => Task.CompletedTask);
|
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<TestConnectionHandler>();
|
||||||
|
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<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
|
||||||
|
|
||||||
|
var error = negotiateResponse.Value<string>("error");
|
||||||
|
Assert.Equal("The client requested an invalid protocol version 'Invalid'", error);
|
||||||
|
|
||||||
|
var connectionId = negotiateResponse.Value<string>("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<TestConnectionHandler>();
|
||||||
|
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<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
|
||||||
|
|
||||||
|
var error = negotiateResponse.Value<string>("error");
|
||||||
|
Assert.Equal("The client requested version '0', but the server does not support this version.", error);
|
||||||
|
|
||||||
|
var connectionId = negotiateResponse.Value<string>("connectionId");
|
||||||
|
Assert.Null(connectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(HttpTransportType.LongPolling)]
|
[InlineData(HttpTransportType.LongPolling)]
|
||||||
[InlineData(HttpTransportType.ServerSentEvents)]
|
[InlineData(HttpTransportType.ServerSentEvents)]
|
||||||
|
|
@ -125,7 +209,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Path = "/foo";
|
context.Request.Path = "/foo";
|
||||||
context.Request.Method = "POST";
|
context.Request.Method = "POST";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionToken;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
|
|
||||||
|
|
@ -166,6 +251,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Path = "/foo";
|
context.Request.Path = "/foo";
|
||||||
context.Request.Method = "POST";
|
context.Request.Method = "POST";
|
||||||
context.Response.Body = ms;
|
context.Response.Body = ms;
|
||||||
|
context.Request.QueryString = new QueryString("?negotiateVersion=1");
|
||||||
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions { Transports = transports });
|
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions { Transports = transports });
|
||||||
|
|
||||||
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
|
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
|
||||||
|
|
@ -204,6 +290,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Method = "GET";
|
context.Request.Method = "GET";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = "unknown";
|
values["id"] = "unknown";
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
SetTransport(context, transportType);
|
SetTransport(context, transportType);
|
||||||
|
|
@ -240,6 +327,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Method = "POST";
|
context.Request.Method = "POST";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = "unknown";
|
values["id"] = "unknown";
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
|
|
||||||
|
|
@ -276,7 +364,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Path = "/foo";
|
context.Request.Path = "/foo";
|
||||||
context.Request.Method = "POST";
|
context.Request.Method = "POST";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionToken;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
|
|
||||||
|
|
@ -315,6 +404,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Method = "POST";
|
context.Request.Method = "POST";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionId;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
|
|
||||||
|
|
@ -354,7 +444,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Path = "/foo";
|
context.Request.Path = "/foo";
|
||||||
context.Request.Method = "GET";
|
context.Request.Method = "GET";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionToken;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
|
|
||||||
|
|
@ -415,7 +506,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Path = "/foo";
|
context.Request.Path = "/foo";
|
||||||
context.Request.Method = "GET";
|
context.Request.Method = "GET";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionToken;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
|
|
||||||
|
|
@ -481,7 +573,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Path = "/foo";
|
context.Request.Path = "/foo";
|
||||||
context.Request.Method = "POST";
|
context.Request.Method = "POST";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionToken;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
|
|
||||||
|
|
@ -544,6 +637,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Method = "POST";
|
context.Request.Method = "POST";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionId;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
|
|
||||||
|
|
@ -613,7 +707,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Path = "/foo";
|
context.Request.Path = "/foo";
|
||||||
context.Request.Method = "GET";
|
context.Request.Method = "GET";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionToken;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
values["another"] = "value";
|
values["another"] = "value";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
|
|
@ -661,8 +756,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
var connectionHttpContext = connection.GetHttpContext();
|
var connectionHttpContext = connection.GetHttpContext();
|
||||||
Assert.NotNull(connectionHttpContext);
|
Assert.NotNull(connectionHttpContext);
|
||||||
|
|
||||||
Assert.Equal(2, connectionHttpContext.Request.Query.Count);
|
Assert.Equal(3, connectionHttpContext.Request.Query.Count);
|
||||||
Assert.Equal(connection.ConnectionId, connectionHttpContext.Request.Query["id"]);
|
|
||||||
Assert.Equal("value", connectionHttpContext.Request.Query["another"]);
|
Assert.Equal("value", connectionHttpContext.Request.Query["another"]);
|
||||||
|
|
||||||
Assert.Equal(3, connectionHttpContext.Request.Headers.Count);
|
Assert.Equal(3, connectionHttpContext.Request.Headers.Count);
|
||||||
|
|
@ -706,6 +800,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
services.AddSingleton<TestConnectionHandler>();
|
services.AddSingleton<TestConnectionHandler>();
|
||||||
context.Request.Path = "/foo";
|
context.Request.Path = "/foo";
|
||||||
context.Request.Method = "GET";
|
context.Request.Method = "GET";
|
||||||
|
context.Request.QueryString = new QueryString("?negotiateVersion=1");
|
||||||
|
|
||||||
SetTransport(context, transportType);
|
SetTransport(context, transportType);
|
||||||
|
|
||||||
|
|
@ -748,7 +843,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Path = "/foo";
|
context.Request.Path = "/foo";
|
||||||
context.Request.Method = "POST";
|
context.Request.Method = "POST";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionToken;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
|
|
||||||
|
|
@ -775,6 +871,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
services.AddSingleton<TestConnectionHandler>();
|
services.AddSingleton<TestConnectionHandler>();
|
||||||
context.Request.Path = "/foo";
|
context.Request.Path = "/foo";
|
||||||
context.Request.Method = "POST";
|
context.Request.Method = "POST";
|
||||||
|
context.Request.QueryString = new QueryString("?negotiateVersion=1");
|
||||||
|
|
||||||
var builder = new ConnectionBuilder(services.BuildServiceProvider());
|
var builder = new ConnectionBuilder(services.BuildServiceProvider());
|
||||||
builder.UseConnectionHandler<TestConnectionHandler>();
|
builder.UseConnectionHandler<TestConnectionHandler>();
|
||||||
|
|
@ -846,6 +943,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory);
|
var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory);
|
||||||
|
|
||||||
var context = MakeRequest("/foo", connection);
|
var context = MakeRequest("/foo", connection);
|
||||||
|
|
||||||
SetTransport(context, HttpTransportType.ServerSentEvents);
|
SetTransport(context, HttpTransportType.ServerSentEvents);
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
|
|
@ -857,7 +955,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||||
|
|
||||||
var exists = manager.TryGetConnection(connection.ConnectionId, out _);
|
var exists = manager.TryGetConnection(connection.ConnectionToken, out _);
|
||||||
Assert.False(exists);
|
Assert.False(exists);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1221,7 +1319,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
await task;
|
await task;
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||||
var exists = manager.TryGetConnection(connection.ConnectionId, out _);
|
var exists = manager.TryGetConnection(connection.ConnectionToken, out _);
|
||||||
Assert.False(exists);
|
Assert.False(exists);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1262,7 +1360,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
await task;
|
await task;
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||||
var exists = manager.TryGetConnection(connection.ConnectionId, out _);
|
var exists = manager.TryGetConnection(connection.ConnectionToken, out _);
|
||||||
Assert.False(exists);
|
Assert.False(exists);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1364,10 +1462,10 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Method = "GET";
|
context.Request.Method = "GET";
|
||||||
context.RequestServices = sp;
|
context.RequestServices = sp;
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionToken;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
|
|
||||||
var builder = new ConnectionBuilder(sp);
|
var builder = new ConnectionBuilder(sp);
|
||||||
builder.UseConnectionHandler<TestConnectionHandler>();
|
builder.UseConnectionHandler<TestConnectionHandler>();
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
@ -1452,7 +1550,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
// Issue the delete request
|
// Issue the delete request
|
||||||
var deleteContext = new DefaultHttpContext();
|
var deleteContext = new DefaultHttpContext();
|
||||||
deleteContext.Request.Path = "/foo";
|
deleteContext.Request.Path = "/foo";
|
||||||
deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionId}");
|
deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionToken}");
|
||||||
deleteContext.Request.Method = "DELETE";
|
deleteContext.Request.Method = "DELETE";
|
||||||
var ms = new MemoryStream();
|
var ms = new MemoryStream();
|
||||||
deleteContext.Response.Body = ms;
|
deleteContext.Response.Body = ms;
|
||||||
|
|
@ -1495,7 +1593,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
// Issue the delete request and make sure the poll completes
|
// Issue the delete request and make sure the poll completes
|
||||||
var deleteContext = new DefaultHttpContext();
|
var deleteContext = new DefaultHttpContext();
|
||||||
deleteContext.Request.Path = "/foo";
|
deleteContext.Request.Path = "/foo";
|
||||||
deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionId}");
|
deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionToken}");
|
||||||
deleteContext.Request.Method = "DELETE";
|
deleteContext.Request.Method = "DELETE";
|
||||||
|
|
||||||
Assert.False(pollTask.IsCompleted);
|
Assert.False(pollTask.IsCompleted);
|
||||||
|
|
@ -1513,7 +1611,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
Assert.Equal("text/plain", deleteContext.Response.ContentType);
|
Assert.Equal("text/plain", deleteContext.Response.ContentType);
|
||||||
|
|
||||||
// Verify the connection was removed from the manager
|
// 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
|
// Issue the delete request and make sure the poll completes
|
||||||
var deleteContext = new DefaultHttpContext();
|
var deleteContext = new DefaultHttpContext();
|
||||||
deleteContext.Request.Path = "/foo";
|
deleteContext.Request.Path = "/foo";
|
||||||
deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionId}");
|
deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionToken}");
|
||||||
deleteContext.Request.Method = "DELETE";
|
deleteContext.Request.Method = "DELETE";
|
||||||
|
|
||||||
await dispatcher.ExecuteAsync(deleteContext, options, app).OrTimeout();
|
await dispatcher.ExecuteAsync(deleteContext, options, app).OrTimeout();
|
||||||
|
|
@ -1561,7 +1659,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
await connection.DisposeAndRemoveTask.OrTimeout();
|
await connection.DisposeAndRemoveTask.OrTimeout();
|
||||||
|
|
||||||
// Verify the connection was removed from the manager
|
// 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.Path = "/foo";
|
||||||
context.Request.Method = "POST";
|
context.Request.Method = "POST";
|
||||||
context.Response.Body = ms;
|
context.Response.Body = ms;
|
||||||
|
context.Request.QueryString = new QueryString("?negotiateVersion=1");
|
||||||
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions { Transports = HttpTransportType.WebSockets });
|
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions { Transports = HttpTransportType.WebSockets });
|
||||||
|
|
||||||
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
|
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
|
||||||
|
|
@ -1637,7 +1736,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Path = "/foo";
|
context.Request.Path = "/foo";
|
||||||
context.Request.Method = "POST";
|
context.Request.Method = "POST";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionToken;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
var buffer = Encoding.UTF8.GetBytes("Hello, world");
|
var buffer = Encoding.UTF8.GetBytes("Hello, world");
|
||||||
|
|
@ -1693,7 +1793,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Path = "/foo";
|
context.Request.Path = "/foo";
|
||||||
context.Request.Method = "POST";
|
context.Request.Method = "POST";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionToken;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
var buffer = Encoding.UTF8.GetBytes("Hello, world");
|
var buffer = Encoding.UTF8.GetBytes("Hello, world");
|
||||||
|
|
@ -1746,7 +1847,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
context.Request.Path = "/foo";
|
context.Request.Path = "/foo";
|
||||||
context.Request.Method = "POST";
|
context.Request.Method = "POST";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionToken;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
context.Request.Query = qs;
|
||||||
var buffer = Encoding.UTF8.GetBytes("Hello, world");
|
var buffer = Encoding.UTF8.GetBytes("Hello, world");
|
||||||
|
|
@ -1808,7 +1910,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
await pollTask.OrTimeout();
|
await pollTask.OrTimeout();
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status500InternalServerError, pollContext.Response.StatusCode);
|
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.Path = "/foo";
|
||||||
context.Request.Method = "GET";
|
context.Request.Method = "GET";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionToken;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
var qs = new QueryCollection(values);
|
var qs = new QueryCollection(values);
|
||||||
context.Request.Query = qs;
|
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();
|
var context = new DefaultHttpContext();
|
||||||
context.Features.Set<IHttpResponseFeature>(new ResponseFeature());
|
context.Features.Set<IHttpResponseFeature>(new ResponseFeature());
|
||||||
context.Request.Path = path;
|
context.Request.Path = path;
|
||||||
context.Request.Method = "GET";
|
context.Request.Method = "GET";
|
||||||
var values = new Dictionary<string, StringValues>();
|
var values = new Dictionary<string, StringValues>();
|
||||||
values["id"] = connection.ConnectionId;
|
values["id"] = connection.ConnectionToken;
|
||||||
|
values["negotiateVersion"] = "1";
|
||||||
if (format != null)
|
if (format != null)
|
||||||
{
|
{
|
||||||
values["format"] = format;
|
values["format"] = format;
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
|
|
||||||
Assert.NotNull(connection.ConnectionId);
|
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);
|
Assert.Same(newConnection, connection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -143,13 +143,13 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
{
|
{
|
||||||
var connectionManager = CreateConnectionManager(LoggerFactory);
|
var connectionManager = CreateConnectionManager(LoggerFactory);
|
||||||
var connection = connectionManager.CreateConnection(PipeOptions.Default, PipeOptions.Default);
|
var connection = connectionManager.CreateConnection(PipeOptions.Default, PipeOptions.Default);
|
||||||
|
|
||||||
var transport = connection.Transport;
|
var transport = connection.Transport;
|
||||||
|
|
||||||
Assert.NotNull(connection.ConnectionId);
|
Assert.NotNull(connection.ConnectionId);
|
||||||
|
Assert.NotNull(connection.ConnectionToken);
|
||||||
Assert.NotNull(transport);
|
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(newConnection, connection);
|
||||||
Assert.Same(transport, newConnection.Transport);
|
Assert.Same(transport, newConnection.Transport);
|
||||||
}
|
}
|
||||||
|
|
@ -168,12 +168,55 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
Assert.NotNull(connection.ConnectionId);
|
Assert.NotNull(connection.ConnectionId);
|
||||||
Assert.NotNull(transport);
|
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(newConnection, connection);
|
||||||
Assert.Same(transport, newConnection.Transport);
|
Assert.Same(transport, newConnection.Transport);
|
||||||
|
|
||||||
connectionManager.RemoveConnection(connection.ConnectionId);
|
connectionManager.RemoveConnection(connection.ConnectionToken);
|
||||||
Assert.False(connectionManager.TryGetConnection(connection.ConnectionId, out newConnection));
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,20 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
public class NegotiateProtocolTests
|
public class NegotiateProtocolTests
|
||||||
{
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null)]
|
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null, 0, null)]
|
||||||
[InlineData("{\"connectionId\":\"\",\"availableTransports\":[]}", "", new string[0], null, null)]
|
[InlineData("{\"connectionId\":\"\",\"availableTransports\":[]}", "", new string[0], null, null, 0, null)]
|
||||||
[InlineData("{\"url\": \"http://foo.com/chat\"}", null, null, "http://foo.com/chat", 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")]
|
[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)]
|
[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)]
|
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"\\u0074ransport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null, 0, null)]
|
||||||
public void ParsingNegotiateResponseMessageSuccessForValid(string json, string connectionId, string[] availableTransports, string url, string accessToken)
|
[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 responseData = Encoding.UTF8.GetBytes(json);
|
||||||
var response = NegotiateProtocol.ParseResponse(responseData);
|
var response = NegotiateProtocol.ParseResponse(responseData);
|
||||||
|
|
@ -28,6 +35,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
Assert.Equal(availableTransports?.Length, response.AvailableTransports?.Count);
|
Assert.Equal(availableTransports?.Length, response.AvailableTransports?.Count);
|
||||||
Assert.Equal(url, response.Url);
|
Assert.Equal(url, response.Url);
|
||||||
Assert.Equal(accessToken, response.AccessToken);
|
Assert.Equal(accessToken, response.AccessToken);
|
||||||
|
Assert.Equal(version, response.Version);
|
||||||
|
Assert.Equal(connectionToken, response.ConnectionToken);
|
||||||
|
|
||||||
if (response.AvailableTransports != null)
|
if (response.AvailableTransports != null)
|
||||||
{
|
{
|
||||||
|
|
@ -45,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\":null}", "Unexpected JSON Token Type 'Null'. Expected a JSON Array.")]
|
||||||
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"transferFormats\":[]}]}", "Missing required property 'transport'.")]
|
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"transferFormats\":[]}]}", "Missing required property 'transport'.")]
|
||||||
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"transport\":\"test\"}]}", "Missing required property 'transferFormats'.")]
|
[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)
|
public void ParsingNegotiateResponseMessageThrowsForInvalid(string payload, string expectedMessage)
|
||||||
{
|
{
|
||||||
var responseData = Encoding.UTF8.GetBytes(payload);
|
var responseData = Encoding.UTF8.GetBytes(payload);
|
||||||
|
|
@ -83,7 +93,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
|
|
||||||
string json = Encoding.UTF8.GetString(writer.ToArray());
|
string json = Encoding.UTF8.GetString(writer.ToArray());
|
||||||
|
|
||||||
Assert.Equal("{\"availableTransports\":[]}", json);
|
Assert.Equal("{\"negotiateVersion\":0,\"availableTransports\":[]}", json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,7 +112,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
|
|
||||||
string json = Encoding.UTF8.GetString(writer.ToArray());
|
string json = Encoding.UTF8.GetString(writer.ToArray());
|
||||||
|
|
||||||
Assert.Equal("{\"availableTransports\":[{\"transport\":null,\"transferFormats\":[]}]}", json);
|
Assert.Equal("{\"negotiateVersion\":0,\"availableTransports\":[{\"transport\":null,\"transferFormats\":[]}]}", json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,24 @@ Load testing for ASP.NET Core SignalR
|
||||||
|
|
||||||
## Commands
|
## 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 <LOG_LEVEL> 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
|
### local
|
||||||
|
|
||||||
The `local` command launches a set of local worker clients to establish connections to your SignalR server.
|
The `local` command launches a set of local worker clients to establish connections to your SignalR server.
|
||||||
|
|
@ -31,13 +49,19 @@ Notes:
|
||||||
|
|
||||||
#### Examples
|
#### 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
|
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
|
dotnet run -- local --target-url https://localhost:5001/echo --connections 5000 --transport LongPolling
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.SignalR
|
||||||
return SendToAllConnections(methodName, args, null);
|
return SendToAllConnections(methodName, args, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task SendToAllConnections(string methodName, object[] args, Func<HubConnectionContext, bool> include)
|
private Task SendToAllConnections(string methodName, object[] args, Func<HubConnectionContext, object, bool> include, object state = null)
|
||||||
{
|
{
|
||||||
List<Task> tasks = null;
|
List<Task> tasks = null;
|
||||||
SerializedHubMessage message = null;
|
SerializedHubMessage message = null;
|
||||||
|
|
@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.SignalR
|
||||||
// foreach over HubConnectionStore avoids allocating an enumerator
|
// foreach over HubConnectionStore avoids allocating an enumerator
|
||||||
foreach (var connection in _connections)
|
foreach (var connection in _connections)
|
||||||
{
|
{
|
||||||
if (include != null && !include(connection))
|
if (include != null && !include(connection, state))
|
||||||
{
|
{
|
||||||
continue;
|
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,
|
// 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
|
// while still being re-usable when sending to multiple groups
|
||||||
private void SendToGroupConnections(string methodName, object[] args, ConcurrentDictionary<string, HubConnectionContext> connections, Func<HubConnectionContext, bool> include, ref List<Task> tasks, ref SerializedHubMessage message)
|
private void SendToGroupConnections(string methodName, object[] args, ConcurrentDictionary<string, HubConnectionContext> connections, Func<HubConnectionContext, object, bool> include, object state, ref List<Task> tasks, ref SerializedHubMessage message)
|
||||||
{
|
{
|
||||||
// foreach over ConcurrentDictionary avoids allocating an enumerator
|
// foreach over ConcurrentDictionary avoids allocating an enumerator
|
||||||
foreach (var connection in connections)
|
foreach (var connection in connections)
|
||||||
{
|
{
|
||||||
if (include != null && !include(connection.Value))
|
if (include != null && !include(connection.Value, state))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -193,7 +193,7 @@ namespace Microsoft.AspNetCore.SignalR
|
||||||
// group might be modified inbetween checking and sending
|
// group might be modified inbetween checking and sending
|
||||||
List<Task> tasks = null;
|
List<Task> tasks = null;
|
||||||
SerializedHubMessage message = 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)
|
if (tasks != null)
|
||||||
{
|
{
|
||||||
|
|
@ -221,7 +221,7 @@ namespace Microsoft.AspNetCore.SignalR
|
||||||
var group = _groups[groupName];
|
var group = _groups[groupName];
|
||||||
if (group != null)
|
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<Task> tasks = null;
|
List<Task> tasks = null;
|
||||||
SerializedHubMessage message = null;
|
SerializedHubMessage message = null;
|
||||||
|
|
||||||
SendToGroupConnections(methodName, args, group, connection => !excludedConnectionIds.Contains(connection.ConnectionId), ref tasks, ref message);
|
SendToGroupConnections(methodName, args, group, (connection, state) => !((IReadOnlyList<string>)state).Contains(connection.ConnectionId), excludedConnectionIds, ref tasks, ref message);
|
||||||
|
|
||||||
if (tasks != null)
|
if (tasks != null)
|
||||||
{
|
{
|
||||||
|
|
@ -271,7 +271,7 @@ namespace Microsoft.AspNetCore.SignalR
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task SendUserAsync(string userId, string methodName, object[] args, CancellationToken cancellationToken = default)
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -292,19 +292,19 @@ namespace Microsoft.AspNetCore.SignalR
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task SendAllExceptAsync(string methodName, object[] args, IReadOnlyList<string> excludedConnectionIds, CancellationToken cancellationToken = default)
|
public override Task SendAllExceptAsync(string methodName, object[] args, IReadOnlyList<string> excludedConnectionIds, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return SendToAllConnections(methodName, args, connection => !excludedConnectionIds.Contains(connection.ConnectionId));
|
return SendToAllConnections(methodName, args, (connection, state) => !((IReadOnlyList<string>)state).Contains(connection.ConnectionId), excludedConnectionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task SendConnectionsAsync(IReadOnlyList<string> connectionIds, string methodName, object[] args, CancellationToken cancellationToken = default)
|
public override Task SendConnectionsAsync(IReadOnlyList<string> connectionIds, string methodName, object[] args, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return SendToAllConnections(methodName, args, connection => connectionIds.Contains(connection.ConnectionId));
|
return SendToAllConnections(methodName, args, (connection, state) => ((IReadOnlyList<string>)state).Contains(connection.ConnectionId), connectionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task SendUsersAsync(IReadOnlyList<string> userIds, string methodName, object[] args, CancellationToken cancellationToken = default)
|
public override Task SendUsersAsync(IReadOnlyList<string> userIds, string methodName, object[] args, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return SendToAllConnections(methodName, args, connection => userIds.Contains(connection.UserIdentifier));
|
return SendToAllConnections(methodName, args, (connection, state) => ((IReadOnlyList<string>)state).Contains(connection.UserIdentifier), userIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
{
|
{
|
||||||
if (descriptor.OriginalParameterTypes[parameterPointer] == typeof(CancellationToken))
|
if (descriptor.OriginalParameterTypes[parameterPointer] == typeof(CancellationToken))
|
||||||
{
|
{
|
||||||
cts = CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted);
|
cts = CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted, default);
|
||||||
arguments[parameterPointer] = cts.Token;
|
arguments[parameterPointer] = cts.Token;
|
||||||
}
|
}
|
||||||
else if (isStreamCall && ReflectionHelper.IsStreamingType(descriptor.OriginalParameterTypes[parameterPointer], mustBeDirectType: true))
|
else if (isStreamCall && ReflectionHelper.IsStreamingType(descriptor.OriginalParameterTypes[parameterPointer], mustBeDirectType: true))
|
||||||
|
|
@ -308,7 +308,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cts = cts ?? CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted);
|
cts = cts ?? CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted, default);
|
||||||
connection.ActiveRequestCancellationSources.TryAdd(hubMethodInvocationMessage.InvocationId, cts);
|
connection.ActiveRequestCancellationSources.TryAdd(hubMethodInvocationMessage.InvocationId, cts);
|
||||||
var enumerable = descriptor.FromReturnedStream(result, cts.Token);
|
var enumerable = descriptor.FromReturnedStream(result, cts.Token);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,14 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
methodBuilder.DefineGenericParameters(genericTypeNames);
|
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();
|
var generator = methodBuilder.GetILGenerator();
|
||||||
|
|
||||||
// Declare local variable to store the arguments to IClientProxy.SendCoreAsync
|
// 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);
|
generator.Emit(OpCodes.Ldstr, interfaceMethodInfo.Name);
|
||||||
|
|
||||||
// Create an new object array to hold all the parameters to this method
|
// 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.Newarr, typeof(object)); // allocate object array
|
||||||
generator.Emit(OpCodes.Stloc_0);
|
generator.Emit(OpCodes.Stloc_0);
|
||||||
|
|
||||||
|
|
@ -162,8 +170,16 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
// Load parameter array on to the stack.
|
// Load parameter array on to the stack.
|
||||||
generator.Emit(OpCodes.Ldloc_0);
|
generator.Emit(OpCodes.Ldloc_0);
|
||||||
|
|
||||||
// Get 'CancellationToken.None' and put it on the stack, since we don't support CancellationToken right now
|
if (hasCancellationToken)
|
||||||
generator.Emit(OpCodes.Call, CancellationTokenNoneProperty.GetMethod);
|
{
|
||||||
|
// 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!
|
// Send!
|
||||||
generator.Emit(OpCodes.Callvirt, invokeMethod);
|
generator.Emit(OpCodes.Callvirt, invokeMethod);
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,41 @@ namespace Microsoft.AspNetCore.SignalR.Tests.Internal
|
||||||
await task2.OrTimeout();
|
await task2.OrTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SupportsCancellationToken()
|
||||||
|
{
|
||||||
|
var clientProxy = new MockProxy();
|
||||||
|
var typedProxy = TypedClientBuilder<ICancellationTokenMethod>.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]
|
[Fact]
|
||||||
public void ThrowsIfProvidedAClass()
|
public void ThrowsIfProvidedAClass()
|
||||||
{
|
{
|
||||||
|
|
@ -179,6 +214,12 @@ namespace Microsoft.AspNetCore.SignalR.Tests.Internal
|
||||||
Task SubMethod(string foo);
|
Task SubMethod(string foo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface ICancellationTokenMethod
|
||||||
|
{
|
||||||
|
Task Method(string foo, CancellationToken cancellationToken);
|
||||||
|
Task NoArgumentMethod(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
public interface IPropertiesClient
|
public interface IPropertiesClient
|
||||||
{
|
{
|
||||||
string Property { get; }
|
string Property { get; }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue