Merge remote-tracking branch 'origin/master' into dotnet-maestro-bot-merge/release/3.1-to-master
This commit is contained in:
commit
772283163e
|
|
@ -487,6 +487,26 @@ jobs:
|
|||
path: artifacts/TestResults/
|
||||
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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -56,34 +56,5 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const int BasePort = 5001;
|
||||
private const int MaxPort = 8000;
|
||||
private static int NextPort = BasePort;
|
||||
|
||||
// GetNextPort doesn't check for HttpSys urlacls.
|
||||
public static int GetNextHttpSysPort(string scheme)
|
||||
{
|
||||
while (NextPort < MaxPort)
|
||||
{
|
||||
var port = NextPort++;
|
||||
|
||||
using (var server = new HttpListener())
|
||||
{
|
||||
server.Prefixes.Add($"{scheme}://localhost:{port}/");
|
||||
try
|
||||
{
|
||||
server.Start();
|
||||
server.Stop();
|
||||
return port;
|
||||
}
|
||||
catch (HttpListenerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
NextPort = BasePort;
|
||||
throw new Exception("Failed to locate a free port.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}.");
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ namespace Microsoft.AspNetCore.WebUtilities
|
|||
{
|
||||
if (Disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(FileBufferingReadStream));
|
||||
throw new ObjectDisposedException(nameof(FileBufferingWriteStream));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
|
|||
return InvokeCoreAwaited(context, policyTask);
|
||||
}
|
||||
|
||||
corsPolicy = policyTask.GetAwaiter().GetResult();
|
||||
corsPolicy = policyTask.Result;
|
||||
}
|
||||
|
||||
return EvaluateAndApplyPolicy(context, corsPolicy);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
|
@ -49,8 +49,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
|
|||
{
|
||||
return HealthCheckResult.Healthy();
|
||||
}
|
||||
|
||||
return HealthCheckResult.Unhealthy();
|
||||
|
||||
return new HealthCheckResult(context.Registration.FailureStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
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)));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
<TargetFramework>${DefaultNetCoreTargetFramework}</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
BROWSER=none
|
||||
|
|
@ -0,0 +1 @@
|
|||
BROWSER=none
|
||||
|
|
@ -2,16 +2,18 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
@ECHO OFF
|
||||
|
||||
%~dp0..\..\..\startvs.cmd %~dp0HttpSysServer.sln
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("; ");
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
import { AbortError, HttpError, TimeoutError } from "./Errors";
|
||||
import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
|
||||
import { ILogger, LogLevel } from "./ILogger";
|
||||
|
||||
export class FetchHttpClient extends HttpClient {
|
||||
private readonly logger: ILogger;
|
||||
|
||||
public constructor(logger: ILogger) {
|
||||
super();
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public async send(request: HttpRequest): Promise<HttpResponse> {
|
||||
// Check that abort was not signaled before calling send
|
||||
if (request.abortSignal && request.abortSignal.aborted) {
|
||||
throw new AbortError();
|
||||
}
|
||||
|
||||
if (!request.method) {
|
||||
throw new Error("No method defined.");
|
||||
}
|
||||
if (!request.url) {
|
||||
throw new Error("No url defined.");
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
|
||||
let error: any;
|
||||
// Hook our abortSignal into the abort controller
|
||||
if (request.abortSignal) {
|
||||
request.abortSignal.onabort = () => {
|
||||
abortController.abort();
|
||||
error = new AbortError();
|
||||
};
|
||||
}
|
||||
|
||||
// If a timeout has been passed in, setup a timeout to call abort
|
||||
// Type needs to be any to fit window.setTimeout and NodeJS.setTimeout
|
||||
let timeoutId: any = null;
|
||||
if (request.timeout) {
|
||||
const msTimeout = request.timeout!;
|
||||
timeoutId = setTimeout(() => {
|
||||
abortController.abort();
|
||||
this.logger.log(LogLevel.Warning, `Timeout from HTTP request.`);
|
||||
error = new TimeoutError();
|
||||
}, msTimeout);
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(request.url!, {
|
||||
body: request.content!,
|
||||
cache: "no-cache",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "text/plain;charset=UTF-8",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
...request.headers,
|
||||
},
|
||||
method: request.method!,
|
||||
mode: "cors",
|
||||
redirect: "manual",
|
||||
signal: abortController.signal,
|
||||
});
|
||||
} catch (e) {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
this.logger.log(
|
||||
LogLevel.Warning,
|
||||
`Error from HTTP request. ${e}.`,
|
||||
);
|
||||
throw e;
|
||||
} finally {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
if (request.abortSignal) {
|
||||
request.abortSignal.onabort = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new HttpError(response.statusText, response.status);
|
||||
}
|
||||
|
||||
const content = deserializeContent(response, request.responseType);
|
||||
const payload = await content;
|
||||
|
||||
return new HttpResponse(
|
||||
response.status,
|
||||
response.statusText,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function deserializeContent(response: Response, responseType?: XMLHttpRequestResponseType): Promise<string | ArrayBuffer> {
|
||||
let content;
|
||||
switch (responseType) {
|
||||
case "arraybuffer":
|
||||
content = response.arrayBuffer();
|
||||
break;
|
||||
case "text":
|
||||
content = response.text();
|
||||
break;
|
||||
case "blob":
|
||||
case "document":
|
||||
case "json":
|
||||
throw new Error(`${responseType} is not supported.`);
|
||||
default:
|
||||
content = response.text();
|
||||
break;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
|
@ -57,6 +57,14 @@ export class HttpResponse {
|
|||
* @param {ArrayBuffer} content The content of the response.
|
||||
*/
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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 { } }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { } }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; } }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Reference in New Issue