Merge remote-tracking branch 'origin/master' into dotnet-maestro-bot-merge/release/3.1-to-master

This commit is contained in:
Javier Calvarro Nelson 2019-09-17 06:39:56 -07:00
commit 772283163e
90 changed files with 1498 additions and 533 deletions

View File

@ -487,6 +487,26 @@ jobs:
path: artifacts/TestResults/
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
- job: Source_Build
displayName: 'Test: Linux Source Build'

View File

@ -25,23 +25,3 @@ jobs:
- name: Helix_logs
path: artifacts/log/
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

View File

@ -48,9 +48,10 @@ Write-Host "Extracting to $tempDir"
if (Get-Command -Name 'Microsoft.PowerShell.Archive\Expand-Archive' -ErrorAction Ignore) {
# 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 {
Remove-Item $tempDir -Recurse -ErrorAction Ignore
# Fallback to old approach for old installations of PowerShell
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory("nodejs.zip", $tempDir)

View File

@ -169,7 +169,7 @@ try {
Write-Host "Run git diff to check for pending changes"
# 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
$changedFilesExclusion = "src/Components/Web.JS/dist/Release/blazor.server.js"
@ -179,7 +179,7 @@ try {
if ($file -eq $changedFilesExclusion) {continue}
$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
& git --no-pager diff --ignore-space-at-eol $filePath
& git --no-pager diff --ignore-space-change $filePath
}
}
}

View File

@ -18,7 +18,6 @@
<HelixAvailableTargetQueue Include="Windows.81.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="OSX.1013.Amd64.Open" Platform="OSX" />
<HelixAvailableTargetQueue Include="Ubuntu.1604.Amd64.Open" Platform="Linux" />
<HelixAvailableTargetQueue Include="Ubuntu.1804.Amd64.Open" Platform="Linux" />
<HelixAvailableTargetQueue Include="Centos.7.Amd64.Open" Platform="Linux" />

View File

@ -20,16 +20,20 @@
</PropertyGroup>
<ItemGroup>
<TSFiles Include="$(MSBuildProjectDirectory)\*\*.ts" />
<TSFiles Include="$(MSBuildProjectDirectory)\package.json" />
<TSFiles Include="$(MSBuildProjectDirectory)\*.npmproj" />
<TSFiles Include="src\**\*.ts" />
<TSFiles Include="test\**\*.ts" />
<TSFiles Include="package.json" />
<TSFiles Include="*.npmproj" />
<BuildOutputFiles Include="$(BaseIntermediateOutputPath)\build-sentinel" />
<BuildOutputFiles Include="dist\**\*.js" />
</ItemGroup>
<Target Name="_CheckForInvalidConfiguration">
<Error Text="Missing expected property: PackageId" Condition="'$(IsPackable)' != 'false' and '$(PackageId)' == ''" />
<Exec ContinueOnError="true" Command="node -v">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
<Exec ContinueOnError="true" Command="node -v" StandardOutputImportance="Low">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
</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'"/>
@ -53,12 +57,25 @@
<CallTarget Targets="_Pack" Condition="'$(PackOnBuild)' == 'true'" />
</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"
Condition="'$(IsBuildable)' != 'false'"
Inputs="@(TSFiles)"
Outputs="$(BaseIntermediateOutputPath)\build-sentinel" >
DependsOnTargets="GetBuildInputCacheFile"
Inputs="@(TSFiles);$(BaseIntermediateOutputPath)tsfiles.cache"
Outputs="@(BuildOutputFiles)">
<Yarn Command="$(NpmBuildArgs)" StandardOutputImportance="High" StandardErrorImportance="High" />
<WriteLinesToFile Overwrite="true" File="$(BaseIntermediateOutputPath)\build-sentinel" />
<WriteLinesToFile Overwrite="true" File="$(BaseIntermediateOutputPath)build-sentinel" />
</Target>
<PropertyGroup>
@ -73,7 +90,10 @@
</PackDependsOn>
</PropertyGroup>
<Target Name="_Pack" Condition="'$(IsPackable)' == 'true'" >
<Target Name="_Pack" Condition="'$(IsPackable)' == 'true'"
Inputs="@(TSFiles)"
Outputs="$(PackageOutputPath)\$(PackageFileName)">
<PropertyGroup>
<_PackageTargetPath>$(MSBuildProjectDirectory)\$(PackageFileName)</_PackageTargetPath>
</PropertyGroup>

View File

@ -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)
{
if (!IsEnabled(logLevel))
{
return;
}
var formattedMessage = formatter(state, exception);
Console.WriteLine($"[{logLevel}] {formattedMessage}");
}

View File

@ -6,6 +6,11 @@
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<BuildOutputFiles Include="dist\release\blazor.server.js" />
<BuildOutputFiles Include="dist\release\blazor.webassembly.js" />
</ItemGroup>
<ItemGroup>
<ProjectReference
Include="..\..\SignalR\clients\ts\signalr\signalr.npmproj"

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,6 @@
<RootNamespace>Microsoft.AspNetCore</RootNamespace>
<!-- https://github.com/aspnet/AspNetCore/issues/7939: This unit test requires the shared framework be available in Helix. -->
<BuildHelixPayload>false</BuildHelixPayload>
<!-- Test logic needs to be updated to handle when SharedFx version and TFM are not the same -->
<SkipTests>true</SkipTests>
</PropertyGroup>
<ItemGroup>

View File

@ -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.");
}
}
}

View File

@ -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.
using System;
using System.Diagnostics;
namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common
{
public static class TestUriHelper
@ -34,7 +36,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common
}
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
{

View File

@ -253,7 +253,7 @@ namespace Microsoft.AspNetCore.Authentication
var schemes = await GetAllSignInSchemeNames();
// 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))
{
@ -275,7 +275,7 @@ namespace Microsoft.AspNetCore.Authentication
{
// CookieAuth is the only implementation of sign-in.
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}.");
@ -292,7 +292,7 @@ namespace Microsoft.AspNetCore.Authentication
{
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))
{
@ -314,7 +314,7 @@ namespace Microsoft.AspNetCore.Authentication
{
// CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it.
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}.");

View File

@ -238,7 +238,7 @@ namespace Microsoft.AspNetCore.WebUtilities
{
if (Disposed)
{
throw new ObjectDisposedException(nameof(FileBufferingReadStream));
throw new ObjectDisposedException(nameof(FileBufferingWriteStream));
}
}

View File

@ -181,7 +181,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
return InvokeCoreAwaited(context, policyTask);
}
corsPolicy = policyTask.GetAwaiter().GetResult();
corsPolicy = policyTask.Result;
}
return EvaluateAndApplyPolicy(context, corsPolicy);

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -49,8 +49,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
return HealthCheckResult.Healthy();
}
return HealthCheckResult.Unhealthy();
return new HealthCheckResult(context.Registration.FailureStatus);
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -61,7 +61,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
}
[Fact]
public async Task CheckAsync_CustomTest_Degraded()
public async Task CheckAsync_CustomTestWithDegradedFailureStatusSpecified_Degraded()
{
// Arrange
var services = CreateServices(async (c, ct) =>
@ -78,12 +78,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, });
// Assert
Assert.Equal(HealthStatus.Unhealthy, result.Status);
Assert.Equal(HealthStatus.Degraded, result.Status);
}
}
[Fact]
public async Task CheckAsync_CustomTest_Unhealthy()
public async Task CheckAsync_CustomTestWithUnhealthyFailureStatusSpecified_Unhealthy()
{
// Arrange
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
private static int _testDbCounter;
private static IServiceProvider CreateServices(
Func<TestDbContext, CancellationToken, Task<bool>> testQuery = null,
HealthStatus failureStatus = HealthStatus.Unhealthy)
HealthStatus? failureStatus = HealthStatus.Unhealthy)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddDbContext<TestDbContext>(o => o.UseInMemoryDatabase("Test" + Interlocked.Increment(ref _testDbCounter)));

View File

@ -83,6 +83,12 @@ namespace Microsoft.AspNetCore.NodeServices.Util
var chunkLength = await _streamReader.ReadAsync(buf, 0, buf.Length);
if (chunkLength == 0)
{
if (_linesBuffer.Length > 0)
{
OnCompleteLine(_linesBuffer.ToString());
_linesBuffer.Clear();
}
OnClosed();
break;
}

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// Retrieves a value object using the specified key.
/// </summary>
/// <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);
}
}

View File

@ -1848,7 +1848,7 @@ namespace Microsoft.AspNetCore.Mvc
/// <summary>
/// Creates an <see cref="ObjectResult"/> that produces a <see cref="ProblemDetails"/> response.
/// </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="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
/// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
<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'">1</WebProject_DirectoryAccessLevelKey>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.WebApplication1</RootNamespace>
</PropertyGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.WebApplication1</RootNamespace>
</PropertyGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@ -10,6 +10,7 @@
<PropertyGroup>
<!-- Lists the versions of dependencies not built in this repo. Packages produced from this repo should be listed as a PackageVersionVariableReference. -->
<GeneratedContentProperties>
DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework);
GrpcAspNetCorePackageVersion=$(GrpcAspNetCorePackageVersion);
MicrosoftAspNetCoreMvcRazorRuntimeCompilationPackageVersion=$(MicrosoftAspNetCoreMvcRazorRuntimeCompilationPackageVersion);
MicrosoftEntityFrameworkCoreSqlitePackageVersion=$(MicrosoftEntityFrameworkCoreSqlitePackageVersion);

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
<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'">1</WebProject_DirectoryAccessLevelKey>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
<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'">1</WebProject_DirectoryAccessLevelKey>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.WebApplication1</RootNamespace>
</PropertyGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
<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'">1</WebProject_DirectoryAccessLevelKey>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.WebApplication1</RootNamespace>
</PropertyGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
<UserSecretsId>dotnet-Company.Application1-53bc9b9d-9d6a-45d4-8429-2a2761773502</UserSecretsId>
<NoDefaultLaunchSettingsFile Condition="'$(ExcludeLaunchSettings)' == 'True'">True</NoDefaultLaunchSettingsFile>
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Company.Application1</RootNamespace>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<IsPackable>false</IsPackable>

View File

@ -11,6 +11,7 @@
<PropertyGroup>
<!-- Lists the versions of dependencies not built in this repo. Packages produced from this repo should be listed as a PackageVersionVariableReference. -->
<GeneratedContentProperties>
DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework);
MicrosoftEntityFrameworkCoreSqlitePackageVersion=$(MicrosoftEntityFrameworkCoreSqlitePackageVersion);
MicrosoftEntityFrameworkCoreRelationalPackageVersion=$(MicrosoftEntityFrameworkCoreRelationalPackageVersion);
MicrosoftEntityFrameworkCoreSqlServerPackageVersion=$(MicrosoftEntityFrameworkCoreSqlServerPackageVersion);

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<IsPackable>false</IsPackable>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<IsPackable>false</IsPackable>

View File

@ -2,16 +2,18 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.HttpSys.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.HttpSys.Internal;
namespace Microsoft.AspNetCore.Server.HttpSys
{
@ -74,6 +76,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
}
var hostingUrlsPresent = _serverAddresses.Addresses.Count > 0;
var serverAddressCopy = _serverAddresses.Addresses.ToList();
_serverAddresses.Addresses.Clear();
if (_serverAddresses.PreferHostingUrls && hostingUrlsPresent)
{
@ -85,10 +89,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
Listener.Options.UrlPrefixes.Clear();
}
foreach (var value in _serverAddresses.Addresses)
{
Listener.Options.UrlPrefixes.Add(value);
}
UpdateUrlPrefixes(serverAddressCopy);
}
else if (_options.UrlPrefixes.Count > 0)
{
@ -100,23 +101,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys
_serverAddresses.Addresses.Clear();
}
foreach (var prefix in _options.UrlPrefixes)
{
_serverAddresses.Addresses.Add(prefix.FullPrefix);
}
}
else if (hostingUrlsPresent)
{
foreach (var value in _serverAddresses.Addresses)
{
Listener.Options.UrlPrefixes.Add(value);
}
UpdateUrlPrefixes(serverAddressCopy);
}
else
{
LogHelper.LogDebug(_logger, $"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default.");
_serverAddresses.Addresses.Add(Constants.DefaultServerAddress);
Listener.Options.UrlPrefixes.Add(Constants.DefaultServerAddress);
}
@ -129,6 +122,13 @@ namespace Microsoft.AspNetCore.Server.HttpSys
Listener.Start();
// Update server addresses after we start listening as port 0
// needs to be selected at the point of binding.
foreach (var prefix in _options.UrlPrefixes)
{
_serverAddresses.Addresses.Add(prefix.FullPrefix);
}
ActivateRequestProcessingLimits();
return Task.CompletedTask;
@ -142,6 +142,14 @@ namespace Microsoft.AspNetCore.Server.HttpSys
}
}
private void UpdateUrlPrefixes(IList<string> serverAddressCopy)
{
foreach (var value in serverAddressCopy)
{
Listener.Options.UrlPrefixes.Add(value);
}
}
// The message pump.
// When we start listening for the next request on one thread, we may need to be sure that the
// completion continues on another thread as to not block the current request processing.

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -73,7 +73,6 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
LogHelper.LogInfo(_logger, "Listening on prefix: " + uriPrefix);
CheckDisposed();
var statusCode = HttpApi.HttpAddUrlToUrlGroup(Id, uriPrefix, (ulong)contextId, 0);
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)

View File

@ -1,8 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.HttpSys.Internal;
namespace Microsoft.AspNetCore.Server.HttpSys
{
@ -15,6 +20,12 @@ namespace Microsoft.AspNetCore.Server.HttpSys
private UrlGroup _urlGroup;
private int _nextId = 1;
// Valid port range of 5000 - 48000.
private const int BasePort = 5000;
private const int MaxPortIndex = 43000;
private const int MaxRetries = 1000;
private static int NextPortIndex;
internal UrlPrefixCollection()
{
}
@ -138,10 +149,55 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
_urlGroup = urlGroup;
// go through the uri list and register for each one of them
foreach (var pair in _prefixes)
// Call ToList to avoid modification when enumerating.
foreach (var pair in _prefixes.ToList())
{
// We'll get this index back on each request and use it to look up the prefix to calculate PathBase.
_urlGroup.RegisterPrefix(pair.Value.FullPrefix, pair.Key);
var urlPrefix = pair.Value;
if (urlPrefix.PortValue == 0)
{
if (urlPrefix.IsHttps)
{
throw new InvalidOperationException("Cannot bind to port 0 with https.");
}
FindHttpPortUnsynchronized(pair.Key, urlPrefix);
}
else
{
// We'll get this index back on each request and use it to look up the prefix to calculate PathBase.
_urlGroup.RegisterPrefix(pair.Value.FullPrefix, pair.Key);
}
}
}
}
private void FindHttpPortUnsynchronized(int key, UrlPrefix urlPrefix)
{
for (var index = 0; index < MaxRetries; index++)
{
try
{
// Bit of complicated math to always try 3000 ports, starting from NextPortIndex + 5000,
// circling back around if we go above 8000 back to 5000, and so on.
var port = ((index + NextPortIndex) % MaxPortIndex) + BasePort;
Debug.Assert(port >= 5000 || port < 8000);
var newPrefix = UrlPrefix.Create(urlPrefix.Scheme, urlPrefix.Host, port, urlPrefix.Path);
_urlGroup.RegisterPrefix(newPrefix.FullPrefix, key);
_prefixes[key] = newPrefix;
NextPortIndex += index + 1;
return;
}
catch (HttpSysException ex)
{
if ((ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_ACCESS_DENIED
&& ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SHARING_VIOLATION
&& ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_ALREADY_EXISTS) || index == MaxRetries - 1)
{
throw;
}
}
}
}
@ -159,4 +215,4 @@ namespace Microsoft.AspNetCore.Server.HttpSys
}
}
}
}
}

View File

@ -0,0 +1,3 @@
@ECHO OFF
%~dp0..\..\..\startvs.cmd %~dp0HttpSysServer.sln

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
@ -114,7 +114,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
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());
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
@ -21,11 +22,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
// When tests projects are run in parallel, overlapping port ranges can cause a race condition when looking for free
// ports during dynamic port allocation.
private const int BasePort = 5001;
private const int MaxPort = 8000;
private const int BaseHttpsPort = 44300;
private const int MaxHttpsPort = 44399;
private static int NextPort = BasePort;
private static int NextHttpsPort = BaseHttpsPort;
private static object PortLock = new object();
internal static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15);
@ -84,39 +82,26 @@ namespace Microsoft.AspNetCore.Server.HttpSys
internal static IWebHost CreateDynamicHost(string basePath, out string root, out string baseAddress, Action<HttpSysOptions> configureOptions, RequestDelegate app)
{
lock (PortLock)
{
while (NextPort < MaxPort)
var prefix = UrlPrefix.Create("http", "localhost", "0", basePath);
var builder = new WebHostBuilder()
.UseHttpSys(options =>
{
var port = NextPort++;
var prefix = UrlPrefix.Create("http", "localhost", port, basePath);
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
baseAddress = prefix.ToString();
options.UrlPrefixes.Add(prefix);
configureOptions(options);
})
.Configure(appBuilder => appBuilder.Run(app));
var builder = new WebHostBuilder()
.UseHttpSys(options =>
{
options.UrlPrefixes.Add(prefix);
configureOptions(options);
})
.Configure(appBuilder => appBuilder.Run(app));
var host = builder.Build();
var host = builder.Build();
host.Start();
var options = host.Services.GetRequiredService<IOptions<HttpSysOptions>>();
prefix = options.Value.UrlPrefixes.First(); // Has new port
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
baseAddress = prefix.ToString();
try
{
host.Start();
return host;
}
catch (HttpSysException)
{
}
}
NextPort = BasePort;
}
throw new Exception("Failed to locate a free port.");
return host;
}
internal static MessagePump CreatePump()
@ -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)
{
lock (PortLock)
{
while (NextPort < MaxPort)
{
var prefix = UrlPrefix.Create("http", "localhost", "0", basePath);
var port = NextPort++;
var prefix = UrlPrefix.Create("http", "localhost", port, basePath);
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
baseAddress = prefix.ToString();
var server = CreatePump();
server.Features.Get<IServerAddressesFeature>().Addresses.Add(prefix.ToString());
configureOptions(server.Listener.Options);
server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
var server = CreatePump();
server.Features.Get<IServerAddressesFeature>().Addresses.Add(baseAddress);
configureOptions(server.Listener.Options);
try
{
server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
return server;
}
catch (HttpSysException)
{
}
}
NextPort = BasePort;
}
throw new Exception("Failed to locate a free port.");
prefix = server.Listener.Options.UrlPrefixes.First(); // Has new port
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
baseAddress = prefix.ToString();
return server;
}
internal static IServer CreateDynamicHttpsServer(out string baseAddress, RequestDelegate app)
@ -184,6 +156,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
throw new Exception("Failed to locate a free port.");
}
internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout);
internal static Task<T> WithTimeout<T>(this Task<T> task) => task.TimeoutAfter(DefaultTimeout);

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;

View File

@ -204,27 +204,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
while (_isClosed == 0)
{
var result = await Input.ReadAsync();
var readableBuffer = result.Buffer;
var consumed = readableBuffer.Start;
var examined = readableBuffer.Start;
var buffer = result.Buffer;
// Call UpdateCompletedStreams() prior to frame processing in order to remove any streams that have exceded their drain timeouts.
UpdateCompletedStreams();
try
{
if (!readableBuffer.IsEmpty)
while (Http2FrameReader.TryReadFrame(ref buffer, _incomingFrame, _serverSettings.MaxFrameSize, out var framePayload))
{
if (Http2FrameReader.ReadFrame(readableBuffer, _incomingFrame, _serverSettings.MaxFrameSize, out var framePayload))
{
Log.Http2FrameReceived(ConnectionId, _incomingFrame);
consumed = examined = framePayload.End;
await ProcessFrameAsync(application, framePayload);
}
else
{
examined = readableBuffer.End;
}
Log.Http2FrameReceived(ConnectionId, _incomingFrame);
await ProcessFrameAsync(application, framePayload);
}
if (result.IsCompleted)
@ -242,7 +232,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
finally
{
Input.AdvanceTo(consumed, examined);
Input.AdvanceTo(buffer.Start, buffer.End);
UpdateConnectionState();
}

View File

@ -31,16 +31,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public const int SettingSize = 6; // 2 bytes for the id, 4 bytes for the value.
public static bool ReadFrame(in ReadOnlySequence<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;
if (readableBuffer.Length < HeaderLength)
if (buffer.Length < HeaderLength)
{
return false;
}
var headerSlice = readableBuffer.Slice(0, HeaderLength);
var headerSlice = buffer.Slice(0, HeaderLength);
var header = headerSlice.ToSpan();
var payloadLength = (int)Bitshifter.ReadUInt24BigEndian(header);
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// Make sure the whole frame is buffered
var frameLength = HeaderLength + payloadLength;
if (readableBuffer.Length < frameLength)
if (buffer.Length < frameLength)
{
return false;
}
@ -61,10 +61,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
frame.Flags = header[FlagsOffset];
frame.StreamId = (int)Bitshifter.ReadUInt31BigEndian(header.Slice(StreamIdOffset));
var extendedHeaderLength = ReadExtendedFields(frame, readableBuffer);
var extendedHeaderLength = ReadExtendedFields(frame, buffer);
// The remaining payload minus the extra fields
framePayload = readableBuffer.Slice(HeaderLength + extendedHeaderLength, payloadLength - extendedHeaderLength);
framePayload = buffer.Slice(HeaderLength + extendedHeaderLength, payloadLength - extendedHeaderLength);
buffer = buffer.Slice(framePayload.End);
return true;
}

View File

@ -59,20 +59,16 @@ namespace System.IO.Pipelines
AvailableMemory = arrayPoolBuffer;
}
public void SetUnownedMemory(Memory<byte> memory)
{
AvailableMemory = memory;
}
public void ResetMemory()
{
if (_memoryOwner is IMemoryOwner<byte> owner)
{
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

View File

@ -341,8 +341,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeW
}
else
{
// We can't use the pool so allocate an array
newSegment.SetUnownedMemory(new byte[sizeHint]);
// We can't use the recommended pool so use the ArrayPool
newSegment.SetOwnedMemory(ArrayPool<byte>.Shared.Rent(sizeHint));
}
_tailMemory = newSegment.AvailableMemory;

View File

@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
@ -755,6 +756,176 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
};
testContext.InitializeHeartbeat();
var dateHeaderValueManager = new DateHeaderValueManager();
dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue);
testContext.DateHeaderValueManager = dateHeaderValueManager;
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0));
async Task App(HttpContext context)
{
context.RequestAborted.Register(() =>
{
requestAborted = true;
});
for (var i = 0; i < chunkCount; i++)
{
await context.Response.BodyWriter.WriteAsync(new Memory<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));
@ -785,11 +956,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
"",
"");
var minTotalOutputSize = chunkCount * chunkSize;
await connection.Receive(
"HTTP/1.1 200 OK",
"Connection: close",
$"Date: {dateHeaderValueManager.GetDateHeaderValues().String}");
// Make sure consuming a single chunk exceeds the 2 second timeout.
var targetBytesPerSecond = chunkSize / 4;
await AssertStreamCompleted(connection.Stream, minTotalOutputSize, targetBytesPerSecond);
// expectedBytes was determined by manual testing. A constant Date header is used, so this shouldn't change unless
// the response header writing logic or response body chunking logic itself changes.
await AssertStreamCompletedAtTargetRate(connection.Stream, expectedBytes: 33_553_556, targetBytesPerSecond);
await appFuncCompleted.Task.DefaultTimeout();
}
await server.StopAsync();
@ -800,87 +977,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
Assert.False(requestAborted);
}
private bool ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeadersRetryPredicate(Exception e)
=> e is IOException && e.Message.Contains("Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request");
[Fact]
[Flaky("https://github.com/dotnet/corefx/issues/30691", FlakyOn.AzP.Windows)]
[CollectDump]
public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeaders()
{
var headerSize = 1024 * 1024; // 1 MB for each header value
var headerCount = 64; // 64 MB of headers per response
var requestCount = 4; // Minimum of 256 MB of total response headers
var headerValue = new string('a', headerSize);
var headerStringValues = new StringValues(Enumerable.Repeat(headerValue, headerCount).ToArray());
var requestAborted = false;
var mockKestrelTrace = new Mock<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)
{
var receiveBuffer = new byte[64 * 1024];
@ -908,7 +1004,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
Assert.True(totalReceived < totalBytes, $"{nameof(AssertStreamAborted)} Stream completed successfully.");
}
private async Task AssertStreamCompleted(Stream stream, long minimumBytes, int targetBytesPerSecond)
private async Task AssertBytesReceivedAtTargetRate(Stream stream, int expectedBytes, int targetBytesPerSecond)
{
var receiveBuffer = new byte[64 * 1024];
var totalReceived = 0;
var startTime = DateTimeOffset.UtcNow;
do
{
var received = await stream.ReadAsync(receiveBuffer, 0, Math.Min(receiveBuffer.Length, expectedBytes - totalReceived));
Assert.NotEqual(0, received);
totalReceived += received;
var expectedTimeElapsed = TimeSpan.FromSeconds(totalReceived / targetBytesPerSecond);
var timeElapsed = DateTimeOffset.UtcNow - startTime;
if (timeElapsed < expectedTimeElapsed)
{
await Task.Delay(expectedTimeElapsed - timeElapsed);
}
} while (totalReceived < expectedBytes);
}
private async Task AssertStreamCompletedAtTargetRate(Stream stream, long expectedBytes, int targetBytesPerSecond)
{
var receiveBuffer = new byte[64 * 1024];
var received = 0;
@ -928,7 +1047,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
}
} while (received > 0);
Assert.True(totalReceived >= minimumBytes, $"{nameof(AssertStreamCompleted)} Stream aborted prematurely.");
Assert.Equal(expectedBytes, totalReceived);
}
public static TheoryData<string, StringValues, string> NullHeaderData

View File

@ -1112,12 +1112,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var buffer = result.Buffer;
var consumed = buffer.Start;
var examined = buffer.Start;
var copyBuffer = buffer;
try
{
Assert.True(buffer.Length > 0);
if (Http2FrameReader.ReadFrame(buffer, frame, maxFrameSize, out var framePayload))
if (Http2FrameReader.TryReadFrame(ref buffer, frame, maxFrameSize, out var framePayload))
{
consumed = examined = framePayload.End;
frame.Payload = framePayload.ToArray();
@ -1135,7 +1136,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
finally
{
_bytesReceived += buffer.Slice(buffer.Start, consumed).Length;
_bytesReceived += copyBuffer.Slice(copyBuffer.Start, consumed).Length;
_pair.Application.Input.AdvanceTo(consumed, examined);
}
}

View File

@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.Http2
try
{
if (Http2FrameReader.ReadFrame(buffer, frame, 16_384, out var framePayload))
if (Http2FrameReader.TryReadFrame(ref buffer, frame, 16_384, out var framePayload))
{
consumed = examined = framePayload.End;
return frame;

View File

@ -24,6 +24,8 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
internal static class ErrorCodes
{
internal const uint ERROR_SUCCESS = 0;
internal const uint ERROR_ACCESS_DENIED = 5;
internal const uint ERROR_SHARING_VIOLATION = 32;
internal const uint ERROR_HANDLE_EOF = 38;
internal const uint ERROR_NOT_SUPPORTED = 50;
internal const uint ERROR_INVALID_PARAMETER = 87;

View File

@ -539,7 +539,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
/// </returns>
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 cancelableStream = AsyncEnumerableAdapters.MakeCancelableTypedAsyncEnumerable(stream, cts);
return cancelableStream;

View File

@ -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]
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
public async Task CanSendAndReceiveMessage(string protocolName, HttpTransportType transportType, string path)

View File

@ -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>("/negotiateProtocolVersion12", options =>
{
options.MinimumProtocolVersion = 12;
});
endpoints.MapHub<TestHub>("/negotiateProtocolVersionNegative", options =>
{
options.MinimumProtocolVersion = -1;
});
endpoints.MapGet("/generateJwtToken", context =>
{
return context.Response.WriteAsync(GenerateJwtToken());

View File

@ -359,7 +359,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
var httpHandler = new TestHttpMessageHandler();
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;
return ResponseUtils.CreateResponse(HttpStatusCode.Accepted);

View File

@ -36,6 +36,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
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]
public Task ConnectionCannotBeStartedIfNoCommonTransportsBetweenClientAndServer()
{
@ -50,12 +56,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
}
[Theory]
[InlineData("http://fakeuri.org/", "http://fakeuri.org/negotiate")]
[InlineData("http://fakeuri.org/?q=1/0", "http://fakeuri.org/negotiate?q=1/0")]
[InlineData("http://fakeuri.org?q=1/0", "http://fakeuri.org/negotiate?q=1/0")]
[InlineData("http://fakeuri.org/endpoint", "http://fakeuri.org/endpoint/negotiate")]
[InlineData("http://fakeuri.org/endpoint/", "http://fakeuri.org/endpoint/negotiate")]
[InlineData("http://fakeuri.org/endpoint?q=1/0", "http://fakeuri.org/endpoint/negotiate?q=1/0")]
[InlineData("http://fakeuri.org/", "http://fakeuri.org/negotiate?negotiateVersion=1")]
[InlineData("http://fakeuri.org/?q=1/0", "http://fakeuri.org/negotiate?q=1/0&negotiateVersion=1")]
[InlineData("http://fakeuri.org?q=1/0", "http://fakeuri.org/negotiate?q=1/0&negotiateVersion=1")]
[InlineData("http://fakeuri.org/endpoint", "http://fakeuri.org/endpoint/negotiate?negotiateVersion=1")]
[InlineData("http://fakeuri.org/endpoint/", "http://fakeuri.org/endpoint/negotiate?negotiateVersion=1")]
[InlineData("http://fakeuri.org/endpoint?q=1/0", "http://fakeuri.org/endpoint/negotiate?q=1/0&negotiateVersion=1")]
public async Task CorrectlyHandlesQueryStringWhenAppendingNegotiateToUrl(string requestedUrl, string expectedNegotiate)
{
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
@ -119,6 +125,124 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
}
[Fact]
public async Task NegotiateCanHaveNewFields()
{
string connectionId = null;
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
JsonConvert.SerializeObject(new
{
connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
availableTransports = new object[]
{
new
{
transport = "LongPolling",
transferFormats = new[] { "Text" }
},
},
newField = "ignore this",
})));
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
using (var noErrorScope = new VerifyNoErrorsScope())
{
await WithConnectionAsync(
CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
async (connection) =>
{
await connection.StartAsync().OrTimeout();
connectionId = connection.ConnectionId;
});
}
Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
}
[Fact]
public async Task ConnectionIdGetsSetWithNegotiateProtocolGreaterThanZero()
{
string connectionId = null;
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
JsonConvert.SerializeObject(new
{
connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
negotiateVersion = 1,
connectionToken = "different-id",
availableTransports = new object[]
{
new
{
transport = "LongPolling",
transferFormats = new[] { "Text" }
},
},
newField = "ignore this",
})));
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
using (var noErrorScope = new VerifyNoErrorsScope())
{
await WithConnectionAsync(
CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
async (connection) =>
{
await connection.StartAsync().OrTimeout();
connectionId = connection.ConnectionId;
});
}
Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
Assert.Equal("http://fakeuri.org/?negotiateVersion=1&id=different-id", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
}
[Fact]
public async Task ConnectionTokenFieldIsIgnoredForNegotiateIdLessThanOne()
{
string connectionId = null;
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
JsonConvert.SerializeObject(new
{
connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
connectionToken = "different-id",
availableTransports = new object[]
{
new
{
transport = "LongPolling",
transferFormats = new[] { "Text" }
},
},
newField = "ignore this",
})));
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
using (var noErrorScope = new VerifyNoErrorsScope())
{
await WithConnectionAsync(
CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
async (connection) =>
{
await connection.StartAsync().OrTimeout();
connectionId = connection.ConnectionId;
});
}
Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
Assert.Equal("http://fakeuri.org/?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
}
[Fact]
public async Task NegotiateThatReturnsUrlGetFollowed()
{
@ -172,10 +296,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
});
}
Assert.Equal("http://fakeuri.org/negotiate", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
Assert.Equal("https://another.domain.url/chat/negotiate", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
Assert.Equal("https://another.domain.url/chat/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
}
@ -278,10 +402,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
});
}
Assert.Equal("http://fakeuri.org/negotiate", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
Assert.Equal("https://another.domain.url/chat/negotiate", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
Assert.Equal("https://another.domain.url/chat/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
// Delete request
Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
}

View File

@ -62,7 +62,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
}
public static string CreateNegotiationContent(string connectionId = "00000000-0000-0000-0000-000000000000",
HttpTransportType? transportTypes = null)
HttpTransportType? transportTypes = null, string connectionToken = "connection-token", int negotiateVersion = 0)
{
var availableTransports = new List<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 });
}
}
}

View File

@ -1,3 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Net;
@ -117,7 +120,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
});
testHttpMessageHandler.OnRequest((request, next, cancellationToken) =>
{
if (request.Method.Equals(HttpMethod.Delete) && request.RequestUri.PathAndQuery.StartsWith("/?id="))
if (request.Method.Equals(HttpMethod.Delete) && request.RequestUri.PathAndQuery.Contains("&id="))
{
deleteCts.Cancel();
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.Accepted));

View File

@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
// Not configurable on purpose, high enough that if we reach here, it's likely
// a buggy server
private static readonly int _maxRedirects = 100;
private static readonly int _protocolVersionNumber = 1;
private static readonly Task<string> _noAccessToken = Task.FromResult<string>(null);
private static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(120);
@ -41,6 +42,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
private readonly HttpConnectionOptions _httpConnectionOptions;
private ITransport _transport;
private readonly ITransportFactory _transportFactory;
private string _connectionToken;
private string _connectionId;
private readonly ConnectionLogScope _logScope;
private readonly ILoggerFactory _loggerFactory;
@ -341,7 +343,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
}
// This should only need to happen once
var connectUrl = CreateConnectUrl(uri, negotiationResponse.ConnectionId);
var connectUrl = CreateConnectUrl(uri, _connectionToken);
// We're going to search for the transfer format as a string because we don't want to parse
// all the transfer formats in the negotiation response, and we want to allow transfer formats
@ -382,7 +384,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
if (negotiationResponse == null)
{
negotiationResponse = await GetNegotiationResponseAsync(uri, cancellationToken);
connectUrl = CreateConnectUrl(uri, negotiationResponse.ConnectionId);
connectUrl = CreateConnectUrl(uri, _connectionToken);
}
Log.StartingTransport(_logger, transportType, connectUrl);
@ -428,8 +430,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
urlBuilder.Path += "/";
}
urlBuilder.Path += "negotiate";
var uri = Utils.AppendQueryString(urlBuilder.Uri, $"negotiateVersion={_protocolVersionNumber}");
using (var request = new HttpRequestMessage(HttpMethod.Post, urlBuilder.Uri))
using (var request = new HttpRequestMessage(HttpMethod.Post, uri))
{
// Corefx changed the default version and High Sierra curlhandler tries to upgrade request
request.Version = new Version(1, 1);
@ -466,7 +469,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
throw new FormatException("Invalid connection id.");
}
return Utils.AppendQueryString(url, "id=" + connectionId);
return Utils.AppendQueryString(url, $"negotiateVersion={_protocolVersionNumber}&id=" + connectionId);
}
private async Task StartTransport(Uri connectUrl, HttpTransportType transportType, TransferFormat transferFormat, CancellationToken cancellationToken)
@ -627,7 +630,19 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
private async Task<NegotiationResponse> GetNegotiationResponseAsync(Uri uri, CancellationToken cancellationToken)
{
var negotiationResponse = await NegotiateAsync(uri, _httpClient, _logger, cancellationToken);
_connectionId = negotiationResponse.ConnectionId;
// If the negotiationVersion is greater than zero then we know that the negotiation response contains a
// connectionToken that will be required to conenct. Otherwise we just set the connectionId and the
// connectionToken on the client to the same value.
if (negotiationResponse.Version > 0)
{
_connectionId = negotiationResponse.ConnectionId;
_connectionToken = negotiationResponse.ConnectionToken;
}
else
{
_connectionToken = _connectionId = negotiationResponse.ConnectionId;
}
_logScope.ConnectionId = _connectionId;
return negotiationResponse;
}

View File

@ -17,7 +17,7 @@ public class UserAgentHelper {
// Parsing version numbers
String detailedVersion = Version.getDetailedVersion();
agentBuilder.append(getVersion(detailedVersion));
agentBuilder.append("; (");
agentBuilder.append(" (");
agentBuilder.append(detailedVersion);
agentBuilder.append("; ");

View File

@ -1116,7 +1116,7 @@ class HubConnectionTest {
hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
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();
assertEquals("{\"type\":1,\"invocationId\":\"1\",\"target\":\"fixedMessage\",\"arguments\":[null]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]);
assertFalse(done.get());

View File

@ -46,7 +46,7 @@ public class UserAgentTest {
String detailedVersion = Version.getDetailedVersion();
String handMadeUserAgent = "Microsoft SignalR/" + UserAgentHelper.getVersion(detailedVersion) +
"; (" + detailedVersion + "; " + UserAgentHelper.getOS() + "; Java; " +
" (" + detailedVersion + "; " + UserAgentHelper.getOS() + "; Java; " +
UserAgentHelper.getJavaVersion() + "; " + UserAgentHelper.getJavaVendor() + ")";
assertEquals(handMadeUserAgent, userAgent);

View File

@ -1,8 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { HttpTransportType, IHubProtocol, JsonHubProtocol } from "@microsoft/signalr";
import { HttpClient, HttpTransportType, IHubProtocol, JsonHubProtocol } from "@microsoft/signalr";
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
import { TestLogger } from "./TestLogger";
import { FetchHttpClient } from "@microsoft/signalr/dist/esm/FetchHttpClient";
import { NodeHttpClient } from "@microsoft/signalr/dist/esm/NodeHttpClient";
import { Platform } from "@microsoft/signalr/dist/esm/Utils";
import { XhrHttpClient } from "@microsoft/signalr/dist/esm/XhrHttpClient";
// On slower CI machines, these tests sometimes take longer than 5s
jasmine.DEFAULT_TIMEOUT_INTERVAL = 20 * 1000;
@ -97,6 +103,34 @@ export function eachTransportAndProtocol(action: (transport: HttpTransportType,
});
}
export function eachTransportAndProtocolAndHttpClient(action: (transport: HttpTransportType, protocol: IHubProtocol, httpClient: HttpClient) => void) {
eachTransportAndProtocol((transport, protocol) => {
getHttpClients().forEach((httpClient) => {
action(transport, protocol, httpClient);
});
});
}
export function getGlobalObject(): any {
return typeof window !== "undefined" ? window : global;
}
export function getHttpClients(): HttpClient[] {
const httpClients: HttpClient[] = [];
if (typeof XMLHttpRequest !== "undefined") {
httpClients.push(new XhrHttpClient(TestLogger.instance));
}
if (typeof fetch !== "undefined") {
httpClients.push(new FetchHttpClient(TestLogger.instance));
}
if (Platform.isNode) {
httpClients.push(new NodeHttpClient(TestLogger.instance));
}
return httpClients;
}
export function eachHttpClient(action: (transport: HttpClient) => void) {
return getHttpClients().forEach((t) => {
return action(t);
});
}

View File

@ -5,7 +5,7 @@
// tslint:disable:no-floating-promises
import { HttpTransportType, IHttpConnectionOptions, TransferFormat } from "@microsoft/signalr";
import { eachTransport, ECHOENDPOINT_URL } from "./Common";
import { eachHttpClient, eachTransport, ECHOENDPOINT_URL } from "./Common";
import { TestLogger } from "./TestLogger";
// We want to continue testing HttpConnection, but we don't export it anymore. So just pull it in directly from the source file.
@ -44,109 +44,114 @@ describe("connection", () => {
});
eachTransport((transportType) => {
describe(`over ${HttpTransportType[transportType]}`, () => {
it("can send and receive messages", (done) => {
const message = "Hello World!";
// the url should be resolved relative to the document.location.host
// and the leading '/' should be automatically added to the url
const connection = new HttpConnection(ECHOENDPOINT_URL, {
...commonOptions,
transport: transportType,
});
eachHttpClient((httpClient) => {
describe(`over ${HttpTransportType[transportType]} with ${(httpClient.constructor as any).name}`, () => {
it("can send and receive messages", (done) => {
const message = "Hello World!";
// the url should be resolved relative to the document.location.host
// and the leading '/' should be automatically added to the url
const connection = new HttpConnection(ECHOENDPOINT_URL, {
...commonOptions,
httpClient,
transport: transportType,
});
connection.onreceive = (data: any) => {
if (data === message) {
connection.stop();
}
};
connection.onclose = (error: any) => {
expect(error).toBeUndefined();
done();
};
connection.start(TransferFormat.Text).then(() => {
connection.send(message);
}).catch((e: any) => {
fail(e);
done();
});
});
it("does not log content of messages sent or received by default", (done) => {
TestLogger.saveLogsAndReset();
const message = "Hello World!";
// DON'T use commonOptions because we want to specifically test the scenario where logMessageContent is not set.
const connection = new HttpConnection(ECHOENDPOINT_URL, {
logger: TestLogger.instance,
transport: transportType,
});
connection.onreceive = (data: any) => {
if (data === message) {
connection.stop();
}
};
// @ts-ignore: We don't use the error parameter intentionally.
connection.onclose = (error) => {
// Search the logs for the message content
expect(TestLogger.instance.currentLog.messages.length).toBeGreaterThan(0);
// @ts-ignore: We don't use the _ or __ parameters intentionally.
for (const [_, __, logMessage] of TestLogger.instance.currentLog.messages) {
expect(logMessage).not.toContain(message);
}
done();
};
connection.start(TransferFormat.Text).then(() => {
connection.send(message);
}).catch((e) => {
fail(e);
done();
});
});
it("does log content of messages sent or received when enabled", (done) => {
TestLogger.saveLogsAndReset();
const message = "Hello World!";
// DON'T use commonOptions because we want to specifically test the scenario where logMessageContent is set to true (even if commonOptions changes).
const connection = new HttpConnection(ECHOENDPOINT_URL, {
logMessageContent: true,
logger: TestLogger.instance,
transport: transportType,
});
connection.onreceive = (data: any) => {
if (data === message) {
connection.stop();
}
};
// @ts-ignore: We don't use the error parameter intentionally.
connection.onclose = (error) => {
// Search the logs for the message content
let matches = 0;
expect(TestLogger.instance.currentLog.messages.length).toBeGreaterThan(0);
// @ts-ignore: We don't use the _ or __ parameters intentionally.
for (const [_, __, logMessage] of TestLogger.instance.currentLog.messages) {
if (logMessage.indexOf(message) !== -1) {
matches += 1;
connection.onreceive = (data: any) => {
if (data === message) {
connection.stop();
}
}
};
// One match for send, one for receive.
expect(matches).toEqual(2);
done();
};
connection.onclose = (error: any) => {
expect(error).toBeUndefined();
done();
};
connection.start(TransferFormat.Text).then(() => {
connection.send(message);
}).catch((e: any) => {
fail(e);
done();
connection.start(TransferFormat.Text).then(() => {
connection.send(message);
}).catch((e: any) => {
fail(e);
done();
});
});
it("does not log content of messages sent or received by default", (done) => {
TestLogger.saveLogsAndReset();
const message = "Hello World!";
// DON'T use commonOptions because we want to specifically test the scenario where logMessageContent is not set.
const connection = new HttpConnection(ECHOENDPOINT_URL, {
httpClient,
logger: TestLogger.instance,
transport: transportType,
});
connection.onreceive = (data: any) => {
if (data === message) {
connection.stop();
}
};
// @ts-ignore: We don't use the error parameter intentionally.
connection.onclose = (error) => {
// Search the logs for the message content
expect(TestLogger.instance.currentLog.messages.length).toBeGreaterThan(0);
// @ts-ignore: We don't use the _ or __ parameters intentionally.
for (const [_, __, logMessage] of TestLogger.instance.currentLog.messages) {
expect(logMessage).not.toContain(message);
}
done();
};
connection.start(TransferFormat.Text).then(() => {
connection.send(message);
}).catch((e) => {
fail(e);
done();
});
});
it("does log content of messages sent or received when enabled", (done) => {
TestLogger.saveLogsAndReset();
const message = "Hello World!";
// DON'T use commonOptions because we want to specifically test the scenario where logMessageContent is set to true (even if commonOptions changes).
const connection = new HttpConnection(ECHOENDPOINT_URL, {
httpClient,
logMessageContent: true,
logger: TestLogger.instance,
transport: transportType,
});
connection.onreceive = (data: any) => {
if (data === message) {
connection.stop();
}
};
// @ts-ignore: We don't use the error parameter intentionally.
connection.onclose = (error) => {
// Search the logs for the message content
let matches = 0;
expect(TestLogger.instance.currentLog.messages.length).toBeGreaterThan(0);
// @ts-ignore: We don't use the _ or __ parameters intentionally.
for (const [_, __, logMessage] of TestLogger.instance.currentLog.messages) {
if (logMessage.indexOf(message) !== -1) {
matches += 1;
}
}
// One match for send, one for receive.
expect(matches).toEqual(2);
done();
};
connection.start(TransferFormat.Text).then(() => {
connection.send(message);
}).catch((e: any) => {
fail(e);
done();
});
});
});
});

View File

@ -7,7 +7,7 @@
import { AbortError, DefaultHttpClient, HttpClient, HttpRequest, HttpResponse, HttpTransportType, HubConnectionBuilder, IHttpConnectionOptions, JsonHubProtocol, NullLogger } from "@microsoft/signalr";
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
import { eachTransport, eachTransportAndProtocol, ENDPOINT_BASE_HTTPS_URL, ENDPOINT_BASE_URL } from "./Common";
import { eachTransport, eachTransportAndProtocolAndHttpClient, ENDPOINT_BASE_HTTPS_URL, ENDPOINT_BASE_URL } from "./Common";
import "./LogBannerReporter";
import { TestLogger } from "./TestLogger";
@ -49,12 +49,12 @@ function getConnectionBuilder(transportType?: HttpTransportType, url?: string, o
}
describe("hubConnection", () => {
eachTransportAndProtocol((transportType, protocol) => {
eachTransportAndProtocolAndHttpClient((transportType, protocol, httpClient) => {
describe("using " + protocol.name + " over " + HttpTransportType[transportType] + " transport", () => {
it("can invoke server method and receive result", (done) => {
const message = "你好,世界!";
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -81,7 +81,7 @@ describe("hubConnection", () => {
it("using https, can invoke server method and receive result", (done) => {
const message = "你好,世界!";
const hubConnection = getConnectionBuilder(transportType, TESTHUBENDPOINT_HTTPS_URL)
const hubConnection = getConnectionBuilder(transportType, TESTHUBENDPOINT_HTTPS_URL, { httpClient })
.withHubProtocol(protocol)
.build();
@ -108,7 +108,7 @@ describe("hubConnection", () => {
it("can invoke server method non-blocking and not receive result", (done) => {
const message = "你好,世界!";
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -130,7 +130,7 @@ describe("hubConnection", () => {
});
it("can invoke server method structural object and receive structural result", (done) => {
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -154,7 +154,7 @@ describe("hubConnection", () => {
});
it("can stream server method and receive result", (done) => {
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -185,7 +185,7 @@ describe("hubConnection", () => {
});
it("can stream server method and cancel stream", (done) => {
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -219,7 +219,7 @@ describe("hubConnection", () => {
it("rethrows an exception from the server when invoking", (done) => {
const errorMessage = "An unexpected error occurred invoking 'ThrowException' on the server. InvalidOperationException: An error occurred.";
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -241,7 +241,7 @@ describe("hubConnection", () => {
});
it("throws an exception when invoking streaming method with invoke", (done) => {
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -263,7 +263,7 @@ describe("hubConnection", () => {
});
it("throws an exception when receiving a streaming result for method called with invoke", (done) => {
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -286,7 +286,7 @@ describe("hubConnection", () => {
it("rethrows an exception from the server when streaming", (done) => {
const errorMessage = "An unexpected error occurred invoking 'StreamThrowException' on the server. InvalidOperationException: An error occurred.";
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -313,7 +313,7 @@ describe("hubConnection", () => {
});
it("throws an exception when invoking hub method with stream", (done) => {
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -340,7 +340,7 @@ describe("hubConnection", () => {
});
it("can receive server calls", (done) => {
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -370,7 +370,7 @@ describe("hubConnection", () => {
});
it("can receive server calls without rebinding handler when restarted", (done) => {
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -425,7 +425,7 @@ describe("hubConnection", () => {
});
it("closed with error or start fails if hub cannot be created", async (done) => {
const hubConnection = getConnectionBuilder(transportType, ENDPOINT_BASE_URL + "/uncreatable")
const hubConnection = getConnectionBuilder(transportType, ENDPOINT_BASE_URL + "/uncreatable", { httpClient })
.withHubProtocol(protocol)
.build();
@ -446,7 +446,7 @@ describe("hubConnection", () => {
});
it("can handle different types", (done) => {
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -489,7 +489,7 @@ describe("hubConnection", () => {
});
it("can receive different types", (done) => {
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -534,7 +534,7 @@ describe("hubConnection", () => {
it("can be restarted", (done) => {
const message = "你好,世界!";
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -577,7 +577,7 @@ describe("hubConnection", () => {
});
it("can stream from client to server with rxjs", async (done) => {
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();
@ -594,7 +594,7 @@ describe("hubConnection", () => {
});
it("can stream from client to server and close with error with rxjs", async (done) => {
const hubConnection = getConnectionBuilder(transportType)
const hubConnection = getConnectionBuilder(transportType, undefined, { httpClient })
.withHubProtocol(protocol)
.build();

View File

@ -13,5 +13,9 @@
<ProjectReference Include="..\signalr\signalr.npmproj" />
</ItemGroup>
<ItemGroup>
<BuildOutputFiles Include="dist\browser\signalr-protocol-msgpack.js" />
</ItemGroup>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Directory.Build.targets))\Directory.Build.targets" />
</Project>

View File

@ -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
@ -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.
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
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.

View File

@ -8,5 +8,10 @@
<IsShippingPackage>true</IsShippingPackage>
</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" />
</Project>

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { AbortError } from "./Errors";
import { FetchHttpClient } from "./FetchHttpClient";
import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
import { ILogger } from "./ILogger";
import { NodeHttpClient } from "./NodeHttpClient";
@ -15,7 +16,9 @@ export class DefaultHttpClient extends HttpClient {
public constructor(logger: ILogger) {
super();
if (typeof XMLHttpRequest !== "undefined") {
if (typeof fetch !== "undefined") {
this.httpClient = new FetchHttpClient(logger);
} else if (typeof XMLHttpRequest !== "undefined") {
this.httpClient = new XhrHttpClient(logger);
} else {
this.httpClient = new NodeHttpClient(logger);

View File

@ -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;
}

View File

@ -57,6 +57,14 @@ export class HttpResponse {
* @param {ArrayBuffer} content The content of the response.
*/
constructor(statusCode: number, statusText: string, content: ArrayBuffer);
/** Constructs a new instance of {@link @microsoft/signalr.HttpResponse} with the specified status code, message and binary content.
*
* @param {number} statusCode The status code of the response.
* @param {string} statusText The status message of the response.
* @param {string | ArrayBuffer} content The content of the response.
*/
constructor(statusCode: number, statusText: string, content: string | ArrayBuffer);
constructor(
public readonly statusCode: number,
public readonly statusText?: string,

View File

@ -34,7 +34,9 @@ namespace Microsoft.AspNetCore.Http.Connections
public string AccessToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.Collections.Generic.IList<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 ConnectionToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Error { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Url { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public int Version { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
}

View File

@ -34,7 +34,9 @@ namespace Microsoft.AspNetCore.Http.Connections
public string AccessToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.Collections.Generic.IList<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 ConnectionToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Error { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Url { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public int Version { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
}

View File

@ -15,6 +15,8 @@ namespace Microsoft.AspNetCore.Http.Connections
{
private const string ConnectionIdPropertyName = "connectionId";
private static JsonEncodedText ConnectionIdPropertyNameBytes = JsonEncodedText.Encode(ConnectionIdPropertyName);
private const string ConnectionTokenPropertyName = "connectionToken";
private static JsonEncodedText ConnectionTokenPropertyNameBytes = JsonEncodedText.Encode(ConnectionTokenPropertyName);
private const string UrlPropertyName = "url";
private static JsonEncodedText UrlPropertyNameBytes = JsonEncodedText.Encode(UrlPropertyName);
private const string AccessTokenPropertyName = "accessToken";
@ -27,6 +29,8 @@ namespace Microsoft.AspNetCore.Http.Connections
private static JsonEncodedText TransferFormatsPropertyNameBytes = JsonEncodedText.Encode(TransferFormatsPropertyName);
private const string ErrorPropertyName = "error";
private static JsonEncodedText ErrorPropertyNameBytes = JsonEncodedText.Encode(ErrorPropertyName);
private const string NegotiateVersionPropertyName = "negotiateVersion";
private static JsonEncodedText NegotiateVersionPropertyNameBytes = JsonEncodedText.Encode(NegotiateVersionPropertyName);
// Use C#7.3's ReadOnlySpan<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
@ -41,6 +45,19 @@ namespace Microsoft.AspNetCore.Http.Connections
var writer = reusableWriter.GetJsonWriter();
writer.WriteStartObject();
// If we already have an error its due to a protocol version incompatibility.
// We can just write the error and complete the JSON object and return.
if (!string.IsNullOrEmpty(response.Error))
{
writer.WriteString(ErrorPropertyNameBytes, response.Error);
writer.WriteEndObject();
writer.Flush();
Debug.Assert(writer.CurrentDepth == 0);
return;
}
writer.WriteNumber(NegotiateVersionPropertyNameBytes, response.Version);
if (!string.IsNullOrEmpty(response.Url))
{
writer.WriteString(UrlPropertyNameBytes, response.Url);
@ -56,6 +73,11 @@ namespace Microsoft.AspNetCore.Http.Connections
writer.WriteString(ConnectionIdPropertyNameBytes, response.ConnectionId);
}
if (response.Version > 0 && !string.IsNullOrEmpty(response.ConnectionToken))
{
writer.WriteString(ConnectionTokenPropertyNameBytes, response.ConnectionToken);
}
writer.WriteStartArray(AvailableTransportsPropertyNameBytes);
if (response.AvailableTransports != null)
@ -112,10 +134,12 @@ namespace Microsoft.AspNetCore.Http.Connections
reader.EnsureObjectStart();
string connectionId = null;
string connectionToken = null;
string url = null;
string accessToken = null;
List<AvailableTransport> availableTransports = null;
string error = null;
int version = 0;
var completed = false;
while (!completed && reader.CheckRead())
@ -135,6 +159,14 @@ namespace Microsoft.AspNetCore.Http.Connections
{
connectionId = reader.ReadAsString(ConnectionIdPropertyName);
}
else if (reader.ValueTextEquals(ConnectionTokenPropertyNameBytes.EncodedUtf8Bytes))
{
connectionToken = reader.ReadAsString(ConnectionTokenPropertyName);
}
else if (reader.ValueTextEquals(NegotiateVersionPropertyNameBytes.EncodedUtf8Bytes))
{
version = reader.ReadAsInt32(NegotiateVersionPropertyName).GetValueOrDefault();
}
else if (reader.ValueTextEquals(AvailableTransportsPropertyNameBytes.EncodedUtf8Bytes))
{
reader.CheckRead();
@ -182,6 +214,14 @@ namespace Microsoft.AspNetCore.Http.Connections
throw new InvalidDataException($"Missing required property '{ConnectionIdPropertyName}'.");
}
if (version > 0)
{
if (connectionToken == null)
{
throw new InvalidDataException($"Missing required property '{ConnectionTokenPropertyName}'.");
}
}
if (availableTransports == null)
{
throw new InvalidDataException($"Missing required property '{AvailableTransportsPropertyName}'.");
@ -191,10 +231,12 @@ namespace Microsoft.AspNetCore.Http.Connections
return new NegotiationResponse
{
ConnectionId = connectionId,
ConnectionToken = connectionToken,
Url = url,
AccessToken = accessToken,
AvailableTransports = availableTransports,
Error = error,
Version = version
};
}
catch (Exception ex)

View File

@ -10,6 +10,8 @@ namespace Microsoft.AspNetCore.Http.Connections
public string Url { get; set; }
public string AccessToken { get; set; }
public string ConnectionId { get; set; }
public string ConnectionToken { get; set; }
public int Version { get; set; }
public IList<AvailableTransport> AvailableTransports { get; set; }
public string Error { get; set; }
}

View File

@ -53,6 +53,7 @@ namespace Microsoft.AspNetCore.Http.Connections
public long ApplicationMaxBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.Collections.Generic.IList<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 int MinimumProtocolVersion { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public long TransportMaxBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Http.Connections.HttpTransportType Transports { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Http.Connections.WebSocketOptions WebSockets { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }

View File

@ -57,5 +57,11 @@ namespace Microsoft.AspNetCore.Http.Connections
/// Gets or sets the maximum buffer size of the application writer.
/// </summary>
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;
}
}

View File

@ -48,11 +48,13 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
/// Creates the DefaultConnectionContext without Pipes to avoid upfront allocations.
/// The caller is expected to set the <see cref="Transport"/> and <see cref="Application"/> pipes manually.
/// </summary>
/// <param name="id"></param>
/// <param name="connectionId"></param>
/// <param name="connectionToken"></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;
// The default behavior is that both formats are supported.
@ -74,8 +76,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
Features.Set<IConnectionInherentKeepAliveFeature>(this);
}
public HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application, ILogger logger = null)
: this(id, logger)
internal HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application, ILogger logger = null)
: this(id, null, logger)
{
Transport = transport;
Application = application;
@ -113,6 +115,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
public override string ConnectionId { get; set; }
internal string ConnectionToken { get; set; }
public override IFeatureCollection Features { get; }
public ClaimsPrincipal User { get; set; }

View File

@ -52,6 +52,12 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
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.");
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)
{
_connectionDisposed(logger, connectionId, null);
@ -121,6 +127,16 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
{
_failedToReadHttpRequestBody(logger, connectionId, ex);
}
public static void NegotiateProtocolVersionMismatch(ILogger logger, int clientProtocolVersion)
{
_negotiateProtocolVersionMismatch(logger, clientProtocolVersion, null);
}
public static void InvalidNegotiateProtocolVersion(ILogger logger, string requestedProtocolVersion)
{
_invalidNegotiateProtocolVersion(logger, requestedProtocolVersion, null);
}
}
}
}

View File

@ -45,6 +45,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
private readonly HttpConnectionManager _manager;
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger _logger;
private static readonly int _protocolVersion = 1;
public HttpConnectionDispatcher(HttpConnectionManager manager, ILoggerFactory loggerFactory)
{
@ -58,7 +59,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
// Create the log scope and attempt to pass the Connection ID to it so as many logs as possible contain
// the Connection ID metadata. If this is the negotiate request then the Connection ID for the scope will
// be set a little later.
var logScope = new ConnectionLogScope(GetConnectionId(context));
HttpConnectionContext connectionContext = null;
var connectionToken = GetConnectionToken(context);
if (connectionToken != null)
{
_manager.TryGetConnection(GetConnectionToken(context), out connectionContext);
}
var logScope = new ConnectionLogScope(connectionContext?.ConnectionId);
using (_logger.BeginScope(logScope))
{
if (HttpMethods.IsPost(context.Request.Method))
@ -278,13 +287,29 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
private async Task ProcessNegotiate(HttpContext context, HttpConnectionDispatcherOptions options, ConnectionLogScope logScope)
{
context.Response.ContentType = "application/json";
string error = null;
int clientProtocolVersion = 0;
if (context.Request.Query.TryGetValue("NegotiateVersion", out var queryStringVersion))
{
// Set the negotiate response to the protocol we use.
var queryStringVersionValue = queryStringVersion.ToString();
if (!int.TryParse(queryStringVersionValue, out clientProtocolVersion))
{
error = $"The client requested an invalid protocol version '{queryStringVersionValue}'";
Log.InvalidNegotiateProtocolVersion(_logger, queryStringVersionValue);
}
}
// Establish the connection
var connection = CreateConnection(options);
HttpConnectionContext connection = null;
if (error == null)
{
connection = CreateConnection(options, clientProtocolVersion);
}
// Set the Connection ID on the logging scope so that logs from now on will have the
// Connection ID metadata set.
logScope.ConnectionId = connection.ConnectionId;
logScope.ConnectionId = connection?.ConnectionId;
// Don't use thread static instance here because writer is used with async
var writer = new MemoryBufferWriter();
@ -292,7 +317,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
try
{
// Get the bytes for the connection id
WriteNegotiatePayload(writer, connection.ConnectionId, context, options);
WriteNegotiatePayload(writer, connection?.ConnectionId, connection?.ConnectionToken, context, options, clientProtocolVersion, error);
Log.NegotiationRequest(_logger);
@ -306,10 +331,46 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
}
}
private static void WriteNegotiatePayload(IBufferWriter<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();
if (!string.IsNullOrEmpty(error))
{
response.Error = error;
NegotiateProtocol.WriteResponse(response, writer);
return;
}
if (clientProtocolVersion > 0)
{
if (clientProtocolVersion < options.MinimumProtocolVersion)
{
response.Error = $"The client requested version '{clientProtocolVersion}', but the server does not support this version.";
Log.NegotiateProtocolVersionMismatch(_logger, clientProtocolVersion);
NegotiateProtocol.WriteResponse(response, writer);
return;
}
else if (clientProtocolVersion > _protocolVersion)
{
response.Version = _protocolVersion;
}
else
{
response.Version = clientProtocolVersion;
}
}
else if (options.MinimumProtocolVersion > 0)
{
// NegotiateVersion wasn't parsed meaning the client requests version 0.
response.Error = $"The client requested version '0', but the server does not support this version.";
NegotiateProtocol.WriteResponse(response, writer);
return;
}
response.ConnectionId = connectionId;
response.ConnectionToken = connectionToken;
response.AvailableTransports = new List<AvailableTransport>();
if ((options.Transports & HttpTransportType.WebSockets) != 0 && ServerHasWebSockets(context.Features))
@ -335,7 +396,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
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)
{
@ -608,9 +669,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
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
context.Response.StatusCode = StatusCodes.Status400BadRequest;
@ -619,7 +680,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
return null;
}
if (!_manager.TryGetConnection(connectionId, out var connection))
if (!_manager.TryGetConnection(connectionToken, out var connection))
{
// No connection with that ID: Not Found
context.Response.StatusCode = StatusCodes.Status404NotFound;
@ -634,15 +695,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
// This is only used for WebSockets connections, which can connect directly without negotiating
private async Task<HttpConnectionContext> GetOrCreateConnectionAsync(HttpContext context, HttpConnectionDispatcherOptions options)
{
var connectionId = GetConnectionId(context);
var connectionToken = GetConnectionToken(context);
HttpConnectionContext connection;
// There's no connection id so this is a brand new connection
if (StringValues.IsNullOrEmpty(connectionId))
if (StringValues.IsNullOrEmpty(connectionToken))
{
connection = CreateConnection(options);
}
else if (!_manager.TryGetConnection(connectionId, out connection))
else if (!_manager.TryGetConnection(connectionToken, out connection))
{
// No connection with that ID: Not Found
context.Response.StatusCode = StatusCodes.Status404NotFound;
@ -653,12 +714,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
return connection;
}
private HttpConnectionContext CreateConnection(HttpConnectionDispatcherOptions options)
private HttpConnectionContext CreateConnection(HttpConnectionDispatcherOptions options, int clientProtocolVersion = 0)
{
var transportPipeOptions = new PipeOptions(pauseWriterThreshold: options.TransportMaxBufferSize, resumeWriterThreshold: options.TransportMaxBufferSize / 2, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false);
var appPipeOptions = new PipeOptions(pauseWriterThreshold: options.ApplicationMaxBufferSize, resumeWriterThreshold: options.ApplicationMaxBufferSize / 2, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false);
return _manager.CreateConnection(transportPipeOptions, appPipeOptions);
return _manager.CreateConnection(transportPipeOptions, appPipeOptions, clientProtocolVersion);
}
private class EmptyServiceProvider : IServiceProvider

View File

@ -78,18 +78,28 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
/// Creates a connection without Pipes setup to allow saving allocations until Pipes are needed.
/// </summary>
/// <returns></returns>
internal HttpConnectionContext CreateConnection(PipeOptions transportPipeOptions, PipeOptions appPipeOptions)
internal HttpConnectionContext CreateConnection(PipeOptions transportPipeOptions, PipeOptions appPipeOptions, int negotiateVersion = 0)
{
string connectionToken;
var id = MakeNewConnectionId();
if (negotiateVersion > 0)
{
connectionToken = MakeNewConnectionId();
}
else
{
connectionToken = id;
}
Log.CreatedNewConnection(_logger, id);
var connectionTimer = HttpConnectionsEventSource.Log.ConnectionStart(id);
var connection = new HttpConnectionContext(id, _connectionLogger);
var connection = new HttpConnectionContext(id, connectionToken, _connectionLogger);
var pair = DuplexPipe.CreateConnectionPair(transportPipeOptions, appPipeOptions);
connection.Transport = pair.Application;
connection.Application = pair.Transport;
_connections.TryAdd(id, (connection, connectionTimer));
_connections.TryAdd(connectionToken, (connection, connectionTimer));
return connection;
}
@ -205,7 +215,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
{
// Remove it from the list after disposal so that's it's easy to see
// connections that might be in a hung state via the connections list
RemoveConnection(connection.ConnectionId);
RemoveConnection(connection.ConnectionToken);
}
}
}

View File

@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
public class HttpConnectionDispatcherTests : VerifiableLoggedTest
{
[Fact]
public async Task NegotiateReservesConnectionIdAndReturnsIt()
public async Task NegotiateVersionZeroReservesConnectionIdAndReturnsIt()
{
using (StartVerifiableLog())
{
@ -54,8 +54,35 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions());
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
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.NotEqual(connectionId, connectionToken);
}
}
@ -74,12 +101,13 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
context.Response.Body = ms;
context.Request.QueryString = new QueryString("?negotiateVersion=1");
var options = new HttpConnectionDispatcherOptions { TransportMaxBufferSize = 4, ApplicationMaxBufferSize = 4 };
await dispatcher.ExecuteNegotiateAsync(context, options);
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
var connectionId = negotiateResponse.Value<string>("connectionId");
context.Request.QueryString = context.Request.QueryString.Add("id", connectionId);
Assert.True(manager.TryGetConnection(connectionId, out var connection));
var connectionToken = negotiateResponse.Value<string>("connectionToken");
context.Request.QueryString = context.Request.QueryString.Add("id", connectionToken);
Assert.True(manager.TryGetConnection(connectionToken, out var connection));
// Fake actual connection after negotiate to populate the pipes on the connection
await dispatcher.ExecuteAsync(context, options, c => Task.CompletedTask);
@ -95,6 +123,62 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
}
}
[Fact]
public async Task InvalidNegotiateProtocolVersionThrows()
{
using (StartVerifiableLog())
{
var manager = CreateConnectionManager(LoggerFactory);
var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory);
var context = new DefaultHttpContext();
var services = new ServiceCollection();
services.AddSingleton<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]
[InlineData(HttpTransportType.LongPolling)]
[InlineData(HttpTransportType.ServerSentEvents)]
@ -125,7 +209,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["id"] = connection.ConnectionToken;
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@ -166,6 +251,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
context.Response.Body = ms;
context.Request.QueryString = new QueryString("?negotiateVersion=1");
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions { Transports = transports });
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
@ -204,6 +290,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Method = "GET";
var values = new Dictionary<string, StringValues>();
values["id"] = "unknown";
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
SetTransport(context, transportType);
@ -240,6 +327,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Method = "POST";
var values = new Dictionary<string, StringValues>();
values["id"] = "unknown";
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@ -276,7 +364,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["id"] = connection.ConnectionToken;
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@ -315,6 +404,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Method = "POST";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@ -354,7 +444,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "GET";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["id"] = connection.ConnectionToken;
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@ -415,7 +506,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "GET";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["id"] = connection.ConnectionToken;
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@ -481,7 +573,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["id"] = connection.ConnectionToken;
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@ -544,6 +637,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Method = "POST";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@ -613,7 +707,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "GET";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["id"] = connection.ConnectionToken;
values["negotiateVersion"] = "1";
values["another"] = "value";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@ -661,8 +756,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
var connectionHttpContext = connection.GetHttpContext();
Assert.NotNull(connectionHttpContext);
Assert.Equal(2, connectionHttpContext.Request.Query.Count);
Assert.Equal(connection.ConnectionId, connectionHttpContext.Request.Query["id"]);
Assert.Equal(3, connectionHttpContext.Request.Query.Count);
Assert.Equal("value", connectionHttpContext.Request.Query["another"]);
Assert.Equal(3, connectionHttpContext.Request.Headers.Count);
@ -706,6 +800,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
services.AddSingleton<TestConnectionHandler>();
context.Request.Path = "/foo";
context.Request.Method = "GET";
context.Request.QueryString = new QueryString("?negotiateVersion=1");
SetTransport(context, transportType);
@ -748,7 +843,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["id"] = connection.ConnectionToken;
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@ -775,6 +871,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
services.AddSingleton<TestConnectionHandler>();
context.Request.Path = "/foo";
context.Request.Method = "POST";
context.Request.QueryString = new QueryString("?negotiateVersion=1");
var builder = new ConnectionBuilder(services.BuildServiceProvider());
builder.UseConnectionHandler<TestConnectionHandler>();
@ -846,6 +943,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory);
var context = MakeRequest("/foo", connection);
SetTransport(context, HttpTransportType.ServerSentEvents);
var services = new ServiceCollection();
@ -857,7 +955,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
var exists = manager.TryGetConnection(connection.ConnectionId, out _);
var exists = manager.TryGetConnection(connection.ConnectionToken, out _);
Assert.False(exists);
}
}
@ -1221,7 +1319,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
await task;
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
var exists = manager.TryGetConnection(connection.ConnectionId, out _);
var exists = manager.TryGetConnection(connection.ConnectionToken, out _);
Assert.False(exists);
}
}
@ -1262,7 +1360,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
await task;
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var exists = manager.TryGetConnection(connection.ConnectionId, out _);
var exists = manager.TryGetConnection(connection.ConnectionToken, out _);
Assert.False(exists);
}
}
@ -1364,10 +1462,10 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Method = "GET";
context.RequestServices = sp;
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["id"] = connection.ConnectionToken;
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
var builder = new ConnectionBuilder(sp);
builder.UseConnectionHandler<TestConnectionHandler>();
var app = builder.Build();
@ -1452,7 +1550,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
// Issue the delete request
var deleteContext = new DefaultHttpContext();
deleteContext.Request.Path = "/foo";
deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionId}");
deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionToken}");
deleteContext.Request.Method = "DELETE";
var ms = new MemoryStream();
deleteContext.Response.Body = ms;
@ -1495,7 +1593,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
// Issue the delete request and make sure the poll completes
var deleteContext = new DefaultHttpContext();
deleteContext.Request.Path = "/foo";
deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionId}");
deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionToken}");
deleteContext.Request.Method = "DELETE";
Assert.False(pollTask.IsCompleted);
@ -1513,7 +1611,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
Assert.Equal("text/plain", deleteContext.Response.ContentType);
// Verify the connection was removed from the manager
Assert.False(manager.TryGetConnection(connection.ConnectionId, out _));
Assert.False(manager.TryGetConnection(connection.ConnectionToken, out _));
}
}
@ -1543,7 +1641,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
// Issue the delete request and make sure the poll completes
var deleteContext = new DefaultHttpContext();
deleteContext.Request.Path = "/foo";
deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionId}");
deleteContext.Request.QueryString = new QueryString($"?id={connection.ConnectionToken}");
deleteContext.Request.Method = "DELETE";
await dispatcher.ExecuteAsync(deleteContext, options, app).OrTimeout();
@ -1561,7 +1659,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
await connection.DisposeAndRemoveTask.OrTimeout();
// Verify the connection was removed from the manager
Assert.False(manager.TryGetConnection(connection.ConnectionId, out _));
Assert.False(manager.TryGetConnection(connection.ConnectionToken, out _));
}
}
@ -1581,6 +1679,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
context.Response.Body = ms;
context.Request.QueryString = new QueryString("?negotiateVersion=1");
await dispatcher.ExecuteNegotiateAsync(context, new HttpConnectionDispatcherOptions { Transports = HttpTransportType.WebSockets });
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
@ -1637,7 +1736,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["id"] = connection.ConnectionToken;
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
var buffer = Encoding.UTF8.GetBytes("Hello, world");
@ -1693,7 +1793,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["id"] = connection.ConnectionToken;
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
var buffer = Encoding.UTF8.GetBytes("Hello, world");
@ -1746,7 +1847,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "POST";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["id"] = connection.ConnectionToken;
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
var buffer = Encoding.UTF8.GetBytes("Hello, world");
@ -1808,7 +1910,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
await pollTask.OrTimeout();
Assert.Equal(StatusCodes.Status500InternalServerError, pollContext.Response.StatusCode);
Assert.False(manager.TryGetConnection(connection.ConnectionId, out var _));
Assert.False(manager.TryGetConnection(connection.ConnectionToken, out var _));
}
}
@ -1831,7 +1933,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
context.Request.Path = "/foo";
context.Request.Method = "GET";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["id"] = connection.ConnectionToken;
values["negotiateVersion"] = "1";
var qs = new QueryCollection(values);
context.Request.Query = qs;
@ -1853,14 +1956,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
}
}
private static DefaultHttpContext MakeRequest(string path, ConnectionContext connection, string format = null)
private static DefaultHttpContext MakeRequest(string path, HttpConnectionContext connection, string format = null)
{
var context = new DefaultHttpContext();
context.Features.Set<IHttpResponseFeature>(new ResponseFeature());
context.Request.Path = path;
context.Request.Method = "GET";
var values = new Dictionary<string, StringValues>();
values["id"] = connection.ConnectionId;
values["id"] = connection.ConnectionToken;
values["negotiateVersion"] = "1";
if (format != null)
{
values["format"] = format;

View File

@ -131,7 +131,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
Assert.NotNull(connection.ConnectionId);
Assert.True(connectionManager.TryGetConnection(connection.ConnectionId, out var newConnection));
Assert.True(connectionManager.TryGetConnection(connection.ConnectionToken, out var newConnection));
Assert.Same(newConnection, connection);
}
}
@ -143,13 +143,13 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
{
var connectionManager = CreateConnectionManager(LoggerFactory);
var connection = connectionManager.CreateConnection(PipeOptions.Default, PipeOptions.Default);
var transport = connection.Transport;
Assert.NotNull(connection.ConnectionId);
Assert.NotNull(connection.ConnectionToken);
Assert.NotNull(transport);
Assert.True(connectionManager.TryGetConnection(connection.ConnectionId, out var newConnection));
Assert.True(connectionManager.TryGetConnection(connection.ConnectionToken, out var newConnection));
Assert.Same(newConnection, connection);
Assert.Same(transport, newConnection.Transport);
}
@ -168,12 +168,55 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
Assert.NotNull(connection.ConnectionId);
Assert.NotNull(transport);
Assert.True(connectionManager.TryGetConnection(connection.ConnectionId, out var newConnection));
Assert.True(connectionManager.TryGetConnection(connection.ConnectionToken, out var newConnection));
Assert.Same(newConnection, connection);
Assert.Same(transport, newConnection.Transport);
connectionManager.RemoveConnection(connection.ConnectionId);
Assert.False(connectionManager.TryGetConnection(connection.ConnectionId, out newConnection));
connectionManager.RemoveConnection(connection.ConnectionToken);
Assert.False(connectionManager.TryGetConnection(connection.ConnectionToken, out newConnection));
}
}
[Fact]
public void ConnectionIdAndConnectionTokenAreTheSameForNegotiateVersionZero()
{
using (StartVerifiableLog())
{
var connectionManager = CreateConnectionManager(LoggerFactory);
var connection = connectionManager.CreateConnection(PipeOptions.Default, PipeOptions.Default, negotiateVersion: 0);
var transport = connection.Transport;
Assert.NotNull(connection.ConnectionId);
Assert.NotNull(transport);
Assert.True(connectionManager.TryGetConnection(connection.ConnectionToken, out var newConnection));
Assert.Same(newConnection, connection);
Assert.Same(transport, newConnection.Transport);
Assert.Equal(connection.ConnectionId, connection.ConnectionToken);
}
}
[Fact]
public void ConnectionIdAndConnectionTokenAreDifferentForNegotiateVersionOne()
{
using (StartVerifiableLog())
{
var connectionManager = CreateConnectionManager(LoggerFactory);
var connection = connectionManager.CreateConnection(PipeOptions.Default, PipeOptions.Default, negotiateVersion: 1);
var transport = connection.Transport;
Assert.NotNull(connection.ConnectionId);
Assert.NotNull(transport);
Assert.True(connectionManager.TryGetConnection(connection.ConnectionToken, out var newConnection));
Assert.False(connectionManager.TryGetConnection(connection.ConnectionId, out var _));
Assert.Same(newConnection, connection);
Assert.Same(transport, newConnection.Transport);
Assert.NotEqual(connection.ConnectionId, connection.ConnectionToken);
}
}

View File

@ -13,13 +13,20 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
public class NegotiateProtocolTests
{
[Theory]
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null)]
[InlineData("{\"connectionId\":\"\",\"availableTransports\":[]}", "", new string[0], null, null)]
[InlineData("{\"url\": \"http://foo.com/chat\"}", null, null, "http://foo.com/chat", null)]
[InlineData("{\"url\": \"http://foo.com/chat\", \"accessToken\": \"token\"}", null, null, "http://foo.com/chat", "token")]
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"transport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null)]
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"\\u0074ransport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null)]
public void ParsingNegotiateResponseMessageSuccessForValid(string json, string connectionId, string[] availableTransports, string url, string accessToken)
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null, 0, null)]
[InlineData("{\"connectionId\":\"\",\"availableTransports\":[]}", "", new string[0], null, null, 0, null)]
[InlineData("{\"url\": \"http://foo.com/chat\"}", null, null, "http://foo.com/chat", null, 0, null)]
[InlineData("{\"url\": \"http://foo.com/chat\", \"accessToken\": \"token\"}", null, null, "http://foo.com/chat", "token", 0, null)]
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"transport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null, 0, null)]
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"\\u0074ransport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null, 0, null)]
[InlineData("{\"negotiateVersion\":123,\"connectionId\":\"123\",\"connectionToken\":\"789\",\"availableTransports\":[{\"\\u0074ransport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null, 123, "789")]
[InlineData("{\"negotiateVersion\":123,\"negotiateVersion\":321, \"connectionToken\":\"789\",\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null, 321, "789")]
[InlineData("{\"ignore\":123,\"negotiateVersion\":123, \"connectionToken\":\"789\",\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null, 123, "789")]
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[],\"negotiateVersion\":123, \"connectionToken\":\"789\"}", "123", new string[0], null, null, 123, "789")]
[InlineData("{\"connectionId\":\"123\",\"connectionToken\":\"789\",\"availableTransports\":[]}", "123", new string[0], null, null, 0, "789")]
[InlineData("{\"connectionToken\":\"789\",\"connectionId\":\"123\",\"availableTransports\":[],\"negotiateVersion\":123}", "123", new string[0], null, null, 123, "789")]
[InlineData("{\"connectionToken\":\"789\",\"connectionId\":\"123\",\"availableTransports\":[],\"negotiateVersion\":123, \"connectionToken\":\"987\"}", "123", new string[0], null, null, 123, "987")]
public void ParsingNegotiateResponseMessageSuccessForValid(string json, string connectionId, string[] availableTransports, string url, string accessToken, int version, string connectionToken)
{
var responseData = Encoding.UTF8.GetBytes(json);
var response = NegotiateProtocol.ParseResponse(responseData);
@ -28,6 +35,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
Assert.Equal(availableTransports?.Length, response.AvailableTransports?.Count);
Assert.Equal(url, response.Url);
Assert.Equal(accessToken, response.AccessToken);
Assert.Equal(version, response.Version);
Assert.Equal(connectionToken, response.ConnectionToken);
if (response.AvailableTransports != null)
{
@ -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\":[{\"transferFormats\":[]}]}", "Missing required property 'transport'.")]
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"transport\":\"test\"}]}", "Missing required property 'transferFormats'.")]
[InlineData("{\"connectionId\":\"123\",\"negotiateVersion\":123,\"availableTransports\":[]}", "Missing required property 'connectionToken'.")]
public void ParsingNegotiateResponseMessageThrowsForInvalid(string payload, string expectedMessage)
{
var responseData = Encoding.UTF8.GetBytes(payload);
@ -83,7 +93,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
string json = Encoding.UTF8.GetString(writer.ToArray());
Assert.Equal("{\"availableTransports\":[]}", json);
Assert.Equal("{\"negotiateVersion\":0,\"availableTransports\":[]}", json);
}
}
@ -102,7 +112,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
string json = Encoding.UTF8.GetString(writer.ToArray());
Assert.Equal("{\"availableTransports\":[{\"transport\":null,\"transferFormats\":[]}]}", json);
Assert.Equal("{\"negotiateVersion\":0,\"availableTransports\":[{\"transport\":null,\"transferFormats\":[]}]}", json);
}
}
}

View File

@ -4,6 +4,24 @@ Load testing for ASP.NET Core SignalR
## Commands
### server
The `server` command runs a web host exposing a single SignalR `Hub` endpoint on `/echo`. After the first client connection, the server will periodically write concurrent connection information to the console.
```
> dotnet run -- help server
Usage: server [options]
Options:
--log <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
The `local` command launches a set of local worker clients to establish connections to your SignalR server.
@ -31,13 +49,19 @@ Notes:
#### Examples
Attempt to make 10,000 connections to the `echo` hub using WebSockets and 10 workers:
Run the server:
```
dotnet run -- server
```
Attempt to make 10,000 connections to the server using WebSockets and 10 workers:
```
dotnet run -- local --target-url https://localhost:5001/echo --workers 10
```
Attempt to make 5,000 connections to the `echo` hub using Long Polling
Attempt to make 5,000 connections to the server using Long Polling
```
dotnet run -- local --target-url https://localhost:5001/echo --connections 5000 --transport LongPolling

View File

@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.SignalR
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;
SerializedHubMessage message = null;
@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.SignalR
// foreach over HubConnectionStore avoids allocating an enumerator
foreach (var connection in _connections)
{
if (include != null && !include(connection))
if (include != null && !include(connection, state))
{
continue;
}
@ -127,12 +127,12 @@ namespace Microsoft.AspNetCore.SignalR
// Tasks and message are passed by ref so they can be lazily created inside the method post-filtering,
// while still being re-usable when sending to multiple groups
private void SendToGroupConnections(string methodName, object[] args, ConcurrentDictionary<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 (var connection in connections)
{
if (include != null && !include(connection.Value))
if (include != null && !include(connection.Value, state))
{
continue;
}
@ -193,7 +193,7 @@ namespace Microsoft.AspNetCore.SignalR
// group might be modified inbetween checking and sending
List<Task> tasks = null;
SerializedHubMessage message = null;
SendToGroupConnections(methodName, args, group, null, ref tasks, ref message);
SendToGroupConnections(methodName, args, group, null, null, ref tasks, ref message);
if (tasks != null)
{
@ -221,7 +221,7 @@ namespace Microsoft.AspNetCore.SignalR
var group = _groups[groupName];
if (group != null)
{
SendToGroupConnections(methodName, args, group, null, ref tasks, ref message);
SendToGroupConnections(methodName, args, group, null, null, ref tasks, ref message);
}
}
@ -247,7 +247,7 @@ namespace Microsoft.AspNetCore.SignalR
List<Task> tasks = null;
SerializedHubMessage message = null;
SendToGroupConnections(methodName, args, group, connection => !excludedConnectionIds.Contains(connection.ConnectionId), ref tasks, ref message);
SendToGroupConnections(methodName, args, group, (connection, state) => !((IReadOnlyList<string>)state).Contains(connection.ConnectionId), excludedConnectionIds, ref tasks, ref message);
if (tasks != null)
{
@ -271,7 +271,7 @@ namespace Microsoft.AspNetCore.SignalR
/// <inheritdoc />
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 />
@ -292,19 +292,19 @@ namespace Microsoft.AspNetCore.SignalR
/// <inheritdoc />
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 />
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 />
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);
}
}
}

View File

@ -275,7 +275,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal
{
if (descriptor.OriginalParameterTypes[parameterPointer] == typeof(CancellationToken))
{
cts = CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted);
cts = CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted, default);
arguments[parameterPointer] = cts.Token;
}
else if (isStreamCall && ReflectionHelper.IsStreamingType(descriptor.OriginalParameterTypes[parameterPointer], mustBeDirectType: true))
@ -308,7 +308,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal
return;
}
cts = cts ?? CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted);
cts = cts ?? CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted, default);
connection.ActiveRequestCancellationSources.TryAdd(hubMethodInvocationMessage.InvocationId, cts);
var enumerable = descriptor.FromReturnedStream(result, cts.Token);

View File

@ -132,6 +132,14 @@ namespace Microsoft.AspNetCore.SignalR.Internal
methodBuilder.DefineGenericParameters(genericTypeNames);
}
// Check to see if the last parameter of the method is a CancellationToken
bool hasCancellationToken = paramTypes.LastOrDefault() == typeof(CancellationToken);
if (hasCancellationToken)
{
// remove CancellationToken from input paramTypes
paramTypes = paramTypes.Take(paramTypes.Length - 1).ToArray();
}
var generator = methodBuilder.GetILGenerator();
// Declare local variable to store the arguments to IClientProxy.SendCoreAsync
@ -145,7 +153,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal
generator.Emit(OpCodes.Ldstr, interfaceMethodInfo.Name);
// Create an new object array to hold all the parameters to this method
generator.Emit(OpCodes.Ldc_I4, parameters.Length); // Stack:
generator.Emit(OpCodes.Ldc_I4, paramTypes.Length); // Stack:
generator.Emit(OpCodes.Newarr, typeof(object)); // allocate object array
generator.Emit(OpCodes.Stloc_0);
@ -162,8 +170,16 @@ namespace Microsoft.AspNetCore.SignalR.Internal
// Load parameter array on to the stack.
generator.Emit(OpCodes.Ldloc_0);
// Get 'CancellationToken.None' and put it on the stack, since we don't support CancellationToken right now
generator.Emit(OpCodes.Call, CancellationTokenNoneProperty.GetMethod);
if (hasCancellationToken)
{
// Get CancellationToken from input argument and put it on the stack
generator.Emit(OpCodes.Ldarg, paramTypes.Length + 1);
}
else
{
// Get 'CancellationToken.None' and put it on the stack, for when method does not have CancellationToken
generator.Emit(OpCodes.Call, CancellationTokenNoneProperty.GetMethod);
}
// Send!
generator.Emit(OpCodes.Callvirt, invokeMethod);

View File

@ -75,6 +75,41 @@ namespace Microsoft.AspNetCore.SignalR.Tests.Internal
await task2.OrTimeout();
}
[Fact]
public async Task SupportsCancellationToken()
{
var clientProxy = new MockProxy();
var typedProxy = TypedClientBuilder<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]
public void ThrowsIfProvidedAClass()
{
@ -179,6 +214,12 @@ namespace Microsoft.AspNetCore.SignalR.Tests.Internal
Task SubMethod(string foo);
}
public interface ICancellationTokenMethod
{
Task Method(string foo, CancellationToken cancellationToken);
Task NoArgumentMethod(CancellationToken cancellationToken);
}
public interface IPropertiesClient
{
string Property { get; }