未验证 提交 1846c072 编写于 作者: K Katya Sokolova 提交者: GitHub

Provide upgrade response details (#71757)

* Provide Upgrade response details

* fixing tests

* Address review feedback

* Save HttpStatusCode without CollectHttpResponseDetails

* Remove unnesessary skip on test

* Disable ConnectAsync_Failed on browser since CollectHttpResponseDetails is not supported

* Update src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs
Co-authored-by: NNatalia Kondratyeva <knatalia@microsoft.com>

* Address review feedback

* Revert "Save HttpStatusCode without CollectHttpResponseDetails"

This reverts commit 0713bd8e292b6a76b0b9f297d95e466f11feff3b.

* renove using from ref

* Update test

* Update src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/ClientWebSocket.cs
Co-authored-by: NNatalia Kondratyeva <knatalia@microsoft.com>

* fixing Values and Enumerator for HttpResponseHeaders

* fixing Values and Enumerator for HttpResponseHeaders

* Apply suggestions from code review
Co-authored-by: NMiha Zupan <mihazupan.zupan1@gmail.com>

* Update src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Managed.cs
Co-authored-by: NStephen Toub <stoub@microsoft.com>

* Apply suggestions from code review
Co-authored-by: NMiha Zupan <mihazupan.zupan1@gmail.com>
Co-authored-by: NStephen Toub <stoub@microsoft.com>

* Check CollectHttpResponseDetails setter

* disable CA1822 // Mark members as static
Co-authored-by: NNatalia Kondratyeva <knatalia@microsoft.com>
Co-authored-by: NMiha Zupan <mihazupan.zupan1@gmail.com>
Co-authored-by: NStephen Toub <stoub@microsoft.com>
上级 ca1a4d89
......@@ -3,6 +3,7 @@
// ------------------------------------------------------------------------------
// Changes to this file must follow the https://aka.ms/api-review process.
// ------------------------------------------------------------------------------
namespace System.Net.WebSockets
{
public sealed partial class ClientWebSocket : System.Net.WebSockets.WebSocket
......@@ -10,6 +11,8 @@ public sealed partial class ClientWebSocket : System.Net.WebSockets.WebSocket
public ClientWebSocket() { }
public override System.Net.WebSockets.WebSocketCloseStatus? CloseStatus { get { throw null; } }
public override string? CloseStatusDescription { get { throw null; } }
public System.Net.HttpStatusCode HttpStatusCode { get { throw null; } }
public System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>>? HttpResponseHeaders { get { throw null; } set { } }
public System.Net.WebSockets.ClientWebSocketOptions Options { get { throw null; } }
public override System.Net.WebSockets.WebSocketState State { get { throw null; } }
public override string? SubProtocol { get { throw null; } }
......@@ -32,6 +35,8 @@ public sealed partial class ClientWebSocketOptions
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public System.Net.CookieContainer? Cookies { get { throw null; } set { } }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public bool CollectHttpResponseDetails { get { throw null; } set { } }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public System.Net.ICredentials? Credentials { get { throw null; } set { } }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public System.TimeSpan KeepAliveInterval { get { throw null; } set { } }
......
......@@ -13,6 +13,7 @@
<Compile Include="System\Net\WebSockets\ClientWebSocketOptions.cs" Condition="'$(TargetPlatformIdentifier)' != 'Browser'" />
<Compile Include="$(CommonPath)System\Net\UriScheme.cs" Link="Common\System\Net\UriScheme.cs" />
<Compile Include="$(CommonPath)System\Net\WebSockets\WebSocketValidate.cs" Link="Common\System\Net\WebSockets\WebSocketValidate.cs" />
<Compile Include="System\Net\WebSockets\HttpResponseHeadersReadOnlyCollection.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetPlatformIdentifier)' != 'Browser'">
<Compile Include="System\Net\WebSockets\WebSocketHandle.Managed.cs" />
......
......@@ -82,6 +82,13 @@ public System.Net.CookieContainer Cookies
set => throw new PlatformNotSupportedException();
}
[UnsupportedOSPlatform("browser")]
public bool CollectHttpResponseDetails
{
get => throw new PlatformNotSupportedException();
set => throw new PlatformNotSupportedException();
}
#endregion HTTP Settings
#region WebSocket Settings
......
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
......@@ -51,6 +52,21 @@ public override WebSocketState State
}
}
public System.Net.HttpStatusCode HttpStatusCode => _innerWebSocket?.HttpStatusCode ?? 0;
// setter to clean up when not needed anymore
public IReadOnlyDictionary<string, IEnumerable<string>>? HttpResponseHeaders
{
get => _innerWebSocket?.HttpResponseHeaders;
set
{
if (_innerWebSocket != null)
{
_innerWebSocket.HttpResponseHeaders = value;
}
}
}
public Task ConnectAsync(Uri uri, CancellationToken cancellationToken)
{
return ConnectAsync(uri, null, cancellationToken);
......
......@@ -28,6 +28,7 @@ public sealed class ClientWebSocketOptions
internal List<string>? _requestedSubProtocols;
private Version _version = Net.HttpVersion.Version11;
private HttpVersionPolicy _versionPolicy = HttpVersionPolicy.RequestVersionOrLower;
private bool _collectHttpResponseDetails;
internal ClientWebSocketOptions() { } // prevent external instantiation
......@@ -232,6 +233,17 @@ public void SetBuffer(int receiveBufferSize, int sendBufferSize, ArraySegment<by
_buffer = buffer;
}
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public bool CollectHttpResponseDetails
{
get => _collectHttpResponseDetails;
set
{
ThrowIfReadOnly();
_collectHttpResponseDetails = value;
}
}
#endregion WebSocket settings
#region Helpers
......
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
namespace System.Net.WebSockets
{
internal sealed class HttpResponseHeadersReadOnlyCollection : IReadOnlyDictionary<string, IEnumerable<string>>
{
private readonly HttpHeadersNonValidated _headers;
public HttpResponseHeadersReadOnlyCollection(HttpResponseHeaders headers) => _headers = headers.NonValidated;
public IEnumerable<string> this[string key] => _headers[key];
public IEnumerable<string> Keys
{
get
{
foreach (KeyValuePair<string, HeaderStringValues> header in _headers)
{
yield return header.Key;
}
}
}
public IEnumerable<IEnumerable<string>> Values
{
get
{
foreach (KeyValuePair<string, HeaderStringValues> header in _headers)
{
yield return header.Value;
}
}
}
public int Count => _headers.Count;
public bool ContainsKey(string key) => _headers.Contains(key);
public IEnumerator<KeyValuePair<string, IEnumerable<string>>> GetEnumerator()
{
foreach (KeyValuePair<string, HeaderStringValues> header in _headers)
{
yield return new KeyValuePair<string, IEnumerable<string>>(header.Key, header.Value);
}
}
public bool TryGetValue(string key, [MaybeNullWhen(false)] out IEnumerable<string> value)
{
if (_headers.TryGetValues(key, out HeaderStringValues values))
{
value = values;
return true;
}
value = null;
return false;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
......@@ -10,6 +11,11 @@ namespace System.Net.WebSockets
internal sealed class WebSocketHandle
{
private WebSocketState _state = WebSocketState.Connecting;
#pragma warning disable CA1822 // Mark members as static
public HttpStatusCode HttpStatusCode => (HttpStatusCode)0;
#pragma warning restore CA1822 // Mark members as static
public IReadOnlyDictionary<string, IEnumerable<string>>? HttpResponseHeaders { get; set; }
public WebSocket? WebSocket { get; private set; }
public WebSocketState State => WebSocket?.State ?? _state;
......
......@@ -27,6 +27,9 @@ internal sealed class WebSocketHandle
public WebSocket? WebSocket { get; private set; }
public WebSocketState State => WebSocket?.State ?? _state;
public HttpStatusCode HttpStatusCode { get; private set; }
public IReadOnlyDictionary<string, IEnumerable<string>>? HttpResponseHeaders { get; set; }
public static ClientWebSocketOptions CreateDefaultOptions() => new ClientWebSocketOptions() { Proxy = DefaultWebProxy.Instance };
......@@ -48,6 +51,8 @@ public async Task ConnectAsync(Uri uri, HttpMessageInvoker? invoker, Cancellatio
invoker ??= new HttpMessageInvoker(SetupHandler(options, out disposeHandler));
HttpResponseMessage? response = null;
bool disposeResponse = false;
bool tryDowngrade = false;
try
{
......@@ -187,7 +192,7 @@ public async Task ConnectAsync(Uri uri, HttpMessageInvoker? invoker, Cancellatio
}
Abort();
response?.Dispose();
disposeResponse = true;
if (exc is WebSocketException ||
(exc is OperationCanceledException && cancellationToken.IsCancellationRequested))
......@@ -199,6 +204,20 @@ public async Task ConnectAsync(Uri uri, HttpMessageInvoker? invoker, Cancellatio
}
finally
{
if (response is not null)
{
if (options.CollectHttpResponseDetails)
{
HttpStatusCode = response.StatusCode;
HttpResponseHeaders = new HttpResponseHeadersReadOnlyCollection(response.Headers);
}
if (disposeResponse)
{
response.Dispose();
}
}
// Disposing the handler will not affect any active stream wrapped in the WebSocket.
if (disposeHandler)
{
......
......@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Test.Common;
using System.Threading;
using System.Threading.Tasks;
......@@ -313,5 +314,71 @@ public async Task ConnectAsync_CancellationRequestedAfterConnect_ThrowsOperation
catch (IOException) { }
}, new LoopbackServer.Options { WebSocketEndpoint = true });
}
[ConditionalFact(nameof(WebSocketsSupported))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/34690", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
[SkipOnPlatform(TestPlatforms.Browser, "CollectHttpResponseDetails not supported on Browser")]
public async Task ConnectAsync_HttpResponseDetailsCollectedOnFailure()
{
await LoopbackServer.CreateClientAndServerAsync(async uri =>
{
using (var clientWebSocket = new ClientWebSocket())
using (var cts = new CancellationTokenSource(TimeOutMilliseconds))
{
clientWebSocket.Options.CollectHttpResponseDetails = true;
Task t = clientWebSocket.ConnectAsync(uri, cts.Token);
await Assert.ThrowsAnyAsync<WebSocketException>(() => t);
Assert.Equal(HttpStatusCode.Unauthorized, clientWebSocket.HttpStatusCode);
Assert.NotEmpty(clientWebSocket.HttpResponseHeaders);
}
}, server => server.AcceptConnectionSendResponseAndCloseAsync(HttpStatusCode.Unauthorized), new LoopbackServer.Options { WebSocketEndpoint = true });
}
[ConditionalFact(nameof(WebSocketsSupported))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/34690", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
[SkipOnPlatform(TestPlatforms.Browser, "CollectHttpResponseDetails not supported on Browser")]
public async Task ConnectAsync_HttpResponseDetailsCollectedOnFailure_CustomHeader()
{
await LoopbackServer.CreateClientAndServerAsync(async uri =>
{
using (var clientWebSocket = new ClientWebSocket())
using (var cts = new CancellationTokenSource(TimeOutMilliseconds))
{
clientWebSocket.Options.CollectHttpResponseDetails = true;
Task t = clientWebSocket.ConnectAsync(uri, cts.Token);
await Assert.ThrowsAnyAsync<WebSocketException>(() => t);
Assert.Equal(HttpStatusCode.Unauthorized, clientWebSocket.HttpStatusCode);
Assert.NotEmpty(clientWebSocket.HttpResponseHeaders);
Assert.Contains("X-CustomHeader1", clientWebSocket.HttpResponseHeaders);
Assert.Contains("X-CustomHeader2", clientWebSocket.HttpResponseHeaders);
Assert.NotNull(clientWebSocket.HttpResponseHeaders.Values);
}
}, server => server.AcceptConnectionSendResponseAndCloseAsync(HttpStatusCode.Unauthorized, "X-CustomHeader1: Value1\r\nX-CustomHeader2: Value2\r\n"), new LoopbackServer.Options { WebSocketEndpoint = true });
}
[ConditionalFact(nameof(WebSocketsSupported))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/34690", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
[SkipOnPlatform(TestPlatforms.Browser, "CollectHttpResponseDetails not supported on Browser")]
public async Task ConnectAsync_HttpResponseDetailsCollectedOnSuccess_Extentions()
{
await LoopbackServer.CreateClientAndServerAsync(async uri =>
{
using (var clientWebSocket = new ClientWebSocket())
using (var cts = new CancellationTokenSource(TimeOutMilliseconds))
{
clientWebSocket.Options.CollectHttpResponseDetails = true;
await clientWebSocket.ConnectAsync(uri, cts.Token);
Assert.Equal(HttpStatusCode.SwitchingProtocols, clientWebSocket.HttpStatusCode);
Assert.NotEmpty(clientWebSocket.HttpResponseHeaders);
Assert.Contains("Sec-WebSocket-Extensions", clientWebSocket.HttpResponseHeaders);
}
}, server => server.AcceptConnectionAsync(async connection =>
{
Dictionary<string, string> headers = await LoopbackHelper.WebSocketHandshakeAsync(connection, "X-CustomHeader1");
}), new LoopbackServer.Options { WebSocketEndpoint = true });
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册