From c75604e41a0ebb37b22e46bfff315da227967e13 Mon Sep 17 00:00:00 2001 From: Carol Wang Date: Tue, 11 Feb 2025 03:07:36 +0000 Subject: [PATCH 1/2] Change WebSocket handshake implementation to align with the .NET approach. --- .../src/Resources/Strings.resx | 15 + .../src/Resources/xlf/Strings.cs.xlf | 25 ++ .../src/Resources/xlf/Strings.de.xlf | 25 ++ .../src/Resources/xlf/Strings.es.xlf | 25 ++ .../src/Resources/xlf/Strings.fr.xlf | 25 ++ .../src/Resources/xlf/Strings.it.xlf | 25 ++ .../src/Resources/xlf/Strings.ja.xlf | 25 ++ .../src/Resources/xlf/Strings.ko.xlf | 25 ++ .../src/Resources/xlf/Strings.pl.xlf | 25 ++ .../src/Resources/xlf/Strings.pt-BR.xlf | 25 ++ .../src/Resources/xlf/Strings.ru.xlf | 25 ++ .../src/Resources/xlf/Strings.tr.xlf | 25 ++ .../src/Resources/xlf/Strings.zh-Hans.xlf | 25 ++ .../src/Resources/xlf/Strings.zh-Hant.xlf | 25 ++ ...tWebSocketTransportDuplexSessionChannel.cs | 332 ++++++++++++++---- .../Channels/HttpKnownHeaderNames.cs | 93 +++++ 16 files changed, 706 insertions(+), 59 deletions(-) create mode 100644 src/System.ServiceModel.Http/src/System/ServiceModel/Channels/HttpKnownHeaderNames.cs diff --git a/src/System.ServiceModel.Http/src/Resources/Strings.resx b/src/System.ServiceModel.Http/src/Resources/Strings.resx index 6ac5023c405..1b46e7b8497 100644 --- a/src/System.ServiceModel.Http/src/Resources/Strings.resx +++ b/src/System.ServiceModel.Http/src/Resources/Strings.resx @@ -363,4 +363,19 @@ The subprotocol '{0}' was not requested by the client. The client requested the following subprotocol(s): '{1}'. + + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + + + Unable to connect to the remote server + + + The server returned status code '{0}' when status code '{1}' was expected. + + + The server's response was missing the required header '{0}'. + + + The '{0}' header value '{1}' is invalid. + \ No newline at end of file diff --git a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.cs.xlf b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.cs.xlf index 3e5ed772ca5..af25a85bbca 100644 --- a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.cs.xlf +++ b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.cs.xlf @@ -412,6 +412,31 @@ Server nepřijal žádost o připojení. Verze protokolu WebSocket na straně klienta se pravděpodobně neshoduje s nastavením na straně serveru ({0}). + + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + + + + The server returned status code '{0}' when status code '{1}' was expected. + The server returned status code '{0}' when status code '{1}' was expected. + + + + The '{0}' header value '{1}' is invalid. + The '{0}' header value '{1}' is invalid. + + + + The server's response was missing the required header '{0}'. + The server's response was missing the required header '{0}'. + + + + Unable to connect to the remote server + Unable to connect to the remote server + + \ No newline at end of file diff --git a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.de.xlf b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.de.xlf index 96ae0eef208..3be24dd6f31 100644 --- a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.de.xlf +++ b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.de.xlf @@ -412,6 +412,31 @@ Der Server hat die Verbindungsanforderung nicht akzeptiert. Möglicherweise stimmt die Version des WebSocket-Protokolls auf dem Client nicht mit der auf dem Server ("{0}") überein. + + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + + + + The server returned status code '{0}' when status code '{1}' was expected. + The server returned status code '{0}' when status code '{1}' was expected. + + + + The '{0}' header value '{1}' is invalid. + The '{0}' header value '{1}' is invalid. + + + + The server's response was missing the required header '{0}'. + The server's response was missing the required header '{0}'. + + + + Unable to connect to the remote server + Unable to connect to the remote server + + \ No newline at end of file diff --git a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.es.xlf b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.es.xlf index 3728c8dd124..2a9b99a7526 100644 --- a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.es.xlf +++ b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.es.xlf @@ -412,6 +412,31 @@ El servidor no aceptó la solicitud de conexión. Es posible que la versión del subprotocolo WebSocket de su cliente no coincida con el del servidor ("{0}"). + + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + + + + The server returned status code '{0}' when status code '{1}' was expected. + The server returned status code '{0}' when status code '{1}' was expected. + + + + The '{0}' header value '{1}' is invalid. + The '{0}' header value '{1}' is invalid. + + + + The server's response was missing the required header '{0}'. + The server's response was missing the required header '{0}'. + + + + Unable to connect to the remote server + Unable to connect to the remote server + + \ No newline at end of file diff --git a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.fr.xlf b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.fr.xlf index 5f48d297549..4155c7c47e5 100644 --- a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.fr.xlf +++ b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.fr.xlf @@ -412,6 +412,31 @@ Le serveur a refusé la demande de connexion. Il est possible que la version du protocole WebSocket sur votre client ne corresponde pas à la version située sur le serveur ('{0}'). + + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + + + + The server returned status code '{0}' when status code '{1}' was expected. + The server returned status code '{0}' when status code '{1}' was expected. + + + + The '{0}' header value '{1}' is invalid. + The '{0}' header value '{1}' is invalid. + + + + The server's response was missing the required header '{0}'. + The server's response was missing the required header '{0}'. + + + + Unable to connect to the remote server + Unable to connect to the remote server + + \ No newline at end of file diff --git a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.it.xlf b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.it.xlf index 49076ebb27d..9fd70dcb544 100644 --- a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.it.xlf +++ b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.it.xlf @@ -412,6 +412,31 @@ Il server non ha accettato la richiesta di connessione. È possibile che la versione del protocollo WebSocket nel client non corrisponda a quella nel server ('{0}'). + + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + + + + The server returned status code '{0}' when status code '{1}' was expected. + The server returned status code '{0}' when status code '{1}' was expected. + + + + The '{0}' header value '{1}' is invalid. + The '{0}' header value '{1}' is invalid. + + + + The server's response was missing the required header '{0}'. + The server's response was missing the required header '{0}'. + + + + Unable to connect to the remote server + Unable to connect to the remote server + + \ No newline at end of file diff --git a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.ja.xlf b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.ja.xlf index 56b1a2f0d70..25dd0fe8a2b 100644 --- a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.ja.xlf +++ b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.ja.xlf @@ -412,6 +412,31 @@ サーバーが接続要求を受け入れませんでした。クライアントの WebSocket プロトコルのバージョンが、サーバー ('{0}') のものと一致していない可能性があります。 + + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + + + + The server returned status code '{0}' when status code '{1}' was expected. + The server returned status code '{0}' when status code '{1}' was expected. + + + + The '{0}' header value '{1}' is invalid. + The '{0}' header value '{1}' is invalid. + + + + The server's response was missing the required header '{0}'. + The server's response was missing the required header '{0}'. + + + + Unable to connect to the remote server + Unable to connect to the remote server + + \ No newline at end of file diff --git a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.ko.xlf b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.ko.xlf index 2c4bb87e9d6..e02c540f62e 100644 --- a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.ko.xlf +++ b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.ko.xlf @@ -412,6 +412,31 @@ 서버에서 연결 요청을 수락하지 않았습니다. 클라이언트의 WebSocket 프로토콜 버전이 서버의 프로토콜 버전('{0}')과 일치하지 않을 수 있습니다. + + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + + + + The server returned status code '{0}' when status code '{1}' was expected. + The server returned status code '{0}' when status code '{1}' was expected. + + + + The '{0}' header value '{1}' is invalid. + The '{0}' header value '{1}' is invalid. + + + + The server's response was missing the required header '{0}'. + The server's response was missing the required header '{0}'. + + + + Unable to connect to the remote server + Unable to connect to the remote server + + \ No newline at end of file diff --git a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.pl.xlf b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.pl.xlf index 96297b597ba..021a695d8fd 100644 --- a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.pl.xlf +++ b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.pl.xlf @@ -412,6 +412,31 @@ Serwer nie zaakceptował żądania połączenia. Być może wersja protokołu WebSocket używana przez klienta jest niezgodna z wersją używaną przez serwer („{0}”). + + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + + + + The server returned status code '{0}' when status code '{1}' was expected. + The server returned status code '{0}' when status code '{1}' was expected. + + + + The '{0}' header value '{1}' is invalid. + The '{0}' header value '{1}' is invalid. + + + + The server's response was missing the required header '{0}'. + The server's response was missing the required header '{0}'. + + + + Unable to connect to the remote server + Unable to connect to the remote server + + \ No newline at end of file diff --git a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.pt-BR.xlf b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.pt-BR.xlf index 9d046cd6329..d75f2648162 100644 --- a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.pt-BR.xlf +++ b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.pt-BR.xlf @@ -412,6 +412,31 @@ O servidor não aceitou a solicitação de conexão. É possível que a versão do protocolo WebSocket no cliente não corresponda à versão no servidor ('{0}'). + + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + + + + The server returned status code '{0}' when status code '{1}' was expected. + The server returned status code '{0}' when status code '{1}' was expected. + + + + The '{0}' header value '{1}' is invalid. + The '{0}' header value '{1}' is invalid. + + + + The server's response was missing the required header '{0}'. + The server's response was missing the required header '{0}'. + + + + Unable to connect to the remote server + Unable to connect to the remote server + + \ No newline at end of file diff --git a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.ru.xlf b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.ru.xlf index 66ddad72aca..3ac453779c8 100644 --- a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.ru.xlf +++ b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.ru.xlf @@ -412,6 +412,31 @@ Сервер не принял запрос на подключение. Возможно, версия протокола WebSocket на клиенте не согласуется с таковой на сервере ("{0}"). + + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + + + + The server returned status code '{0}' when status code '{1}' was expected. + The server returned status code '{0}' when status code '{1}' was expected. + + + + The '{0}' header value '{1}' is invalid. + The '{0}' header value '{1}' is invalid. + + + + The server's response was missing the required header '{0}'. + The server's response was missing the required header '{0}'. + + + + Unable to connect to the remote server + Unable to connect to the remote server + + \ No newline at end of file diff --git a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.tr.xlf b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.tr.xlf index fcb5d5a876f..38246880824 100644 --- a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.tr.xlf +++ b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.tr.xlf @@ -412,6 +412,31 @@ Sunucu, bağlantı isteğini kabul etmedi. İstemciniz üzerindeki WebSocket protokol sürümü, sunucudaki ('{0}') ile eşleşmiyor olabilir. + + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + + + + The server returned status code '{0}' when status code '{1}' was expected. + The server returned status code '{0}' when status code '{1}' was expected. + + + + The '{0}' header value '{1}' is invalid. + The '{0}' header value '{1}' is invalid. + + + + The server's response was missing the required header '{0}'. + The server's response was missing the required header '{0}'. + + + + Unable to connect to the remote server + Unable to connect to the remote server + + \ No newline at end of file diff --git a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.zh-Hans.xlf b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.zh-Hans.xlf index 3cf60175219..70282e66671 100644 --- a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.zh-Hans.xlf @@ -412,6 +412,31 @@ 服务器不接受连接请求。有可能是因为客户端上 WebSocket 协议的版本与服务器上该协议的版本({0})不匹配。 + + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + + + + The server returned status code '{0}' when status code '{1}' was expected. + The server returned status code '{0}' when status code '{1}' was expected. + + + + The '{0}' header value '{1}' is invalid. + The '{0}' header value '{1}' is invalid. + + + + The server's response was missing the required header '{0}'. + The server's response was missing the required header '{0}'. + + + + Unable to connect to the remote server + Unable to connect to the remote server + + \ No newline at end of file diff --git a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.zh-Hant.xlf b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.zh-Hant.xlf index 71206117018..be5d423c14a 100644 --- a/src/System.ServiceModel.Http/src/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/System.ServiceModel.Http/src/Resources/xlf/Strings.zh-Hant.xlf @@ -412,6 +412,31 @@ 伺服器未接受連線要求。可能是用戶端的 WebSocket 通訊協定版本與伺服器的版本 ('{0}') 不符。 + + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). + + + + The server returned status code '{0}' when status code '{1}' was expected. + The server returned status code '{0}' when status code '{1}' was expected. + + + + The '{0}' header value '{1}' is invalid. + The '{0}' header value '{1}' is invalid. + + + + The server's response was missing the required header '{0}'. + The server's response was missing the required header '{0}'. + + + + Unable to connect to the remote server + Unable to connect to the remote server + + \ No newline at end of file diff --git a/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/ClientWebSocketTransportDuplexSessionChannel.cs b/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/ClientWebSocketTransportDuplexSessionChannel.cs index 90ff387bb59..6a10a20c666 100644 --- a/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/ClientWebSocketTransportDuplexSessionChannel.cs +++ b/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/ClientWebSocketTransportDuplexSessionChannel.cs @@ -4,17 +4,22 @@ using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.IdentityModel.Selectors; using System.IdentityModel.Tokens; +using System.IO; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Net.Security; using System.Net.WebSockets; using System.Runtime; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using System.ServiceModel.Security.Tokens; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -57,7 +62,10 @@ protected override void OnOpen(TimeSpan timeout) protected internal override async Task OnOpenAsync(TimeSpan timeout) { TimeoutHelper helper = new TimeoutHelper(timeout); - + bool disposeInvoker = false; + (HttpMessageInvoker invoker, disposeInvoker) = await SetupInvoker(helper.RemainingTime()); + HttpResponseMessage response = null; + bool disposeResponse = false; bool success = false; try { @@ -70,11 +78,134 @@ protected internal override async Task OnOpenAsync(TimeSpan timeout) try { - var clientWebSocket = new ClientWebSocket(); - await ConfigureClientWebSocketAsync(clientWebSocket, helper.RemainingTime()); - await clientWebSocket.ConnectAsync(Via, await helper.GetCancellationTokenAsync()); - ValidateWebSocketConnection(clientWebSocket); - WebSocket = clientWebSocket; + //old implementation: + //var clientWebSocket = new ClientWebSocket(); + //await ConfigureClientWebSocketAsync(clientWebSocket, helper.RemainingTime()); + //await clientWebSocket.ConnectAsync(Via, await helper.GetCancellationTokenAsync()); + //ValidateWebSocketConnection(clientWebSocket); + //WebSocket = clientWebSocket; + + /* new implementation: + 1. Create a SocketsHttpHandler, set certificate, certificate validation callback, and credentials. Wrap in an HttpMessageInvoker. + 2. Create an HttpRequestMessage. Set the required WebSocket headers, including any sub protocols, set the service url etc. + 3. Send the request using the invoker + 4. Validate the response headers, including upgrade status code, websocket specific headers, sub protocol. + 5. Fetch the content stream from the response content + 6. Wrap the content stream in a websocket + */ + + try + { + while (true) + { + try + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Via) { Version = HttpVersion.Version11 }; + //if (options._requestHeaders?.Count > 0) // use field to avoid lazily initializing the collection + //{ + // foreach (string key in options.RequestHeaders) + // { + // request.Headers.TryAddWithoutValidation(key, options.RequestHeaders[key]); + // } + //} + + // These headers were added for WCF specific handshake to avoid encoder or transfermode mismatch between client and server. + // For BinaryMessageEncoder, since we are using a sessionful channel for websocket, the encoder is actually different when + // we are using Buffered or Stramed transfermode. So we need an extra header to identify the transfermode we are using, just + // to make people a little bit easier to diagnose these mismatch issues. + if (_channelFactory.MessageVersion != MessageVersion.None) + { + request.Headers.TryAddWithoutValidation(WebSocketTransportSettings.SoapContentTypeHeader, _channelFactory.WebSocketSoapContentType); + + if (_channelFactory.MessageEncoderFactory is BinaryMessageEncoderFactory) + { + request.Headers.TryAddWithoutValidation(WebSocketTransportSettings.BinaryEncoderTransferModeHeader, _channelFactory.TransferMode.ToString()); + } + } + + string secValue = AddWebSocketHeaders(request); + + Task sendTask = invoker is HttpClient client + ? client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) + : invoker.SendAsync(request, CancellationToken.None); + response = await sendTask.ConfigureAwait(false); + + ValidateResponse(response, secValue); + break; + } + catch (HttpRequestException ex) when (ex.HttpRequestError == HttpRequestError.ExtendedConnectNotSupported || ex.Data.Contains("HTTP2_ENABLED")) + { + } + } + + // The SecWebSocketProtocol header is optional. We should only get it with a non-empty value if we requested subprotocols, + // and then it must only be one of the ones we requested. If we got a subprotocol other than one we requested (or if we + // already got one in a previous header), fail. Otherwise, track which one we got. + string subprotocol = null; + if (response.Headers.TryGetValues(HttpKnownHeaderNames.SecWebSocketProtocol, out IEnumerable subprotocolEnumerableValues)) + { + Debug.Assert(subprotocolEnumerableValues is string[]); + string[] subprotocolArray = (string[])subprotocolEnumerableValues; + if (subprotocolArray.Length > 0 && !string.IsNullOrEmpty(subprotocolArray[0])) + { + if (WebSocketSettings.SubProtocol is not null) + { + if (WebSocketSettings.SubProtocol.Equals(subprotocolArray[0], StringComparison.OrdinalIgnoreCase)) + { + subprotocol = WebSocketSettings.SubProtocol; + } + } + + if (subprotocol == null) + { + throw new WebSocketException( + WebSocketError.UnsupportedProtocol, + SR.Format(SR.net_WebSockets_AcceptUnsupportedProtocol, WebSocketSettings.SubProtocol, string.Join(", ", subprotocolArray))); + } + } + } + + // Get the response stream and wrap it in a web socket. + Stream connectedStream = response.Content.ReadAsStream(); + Debug.Assert(connectedStream.CanWrite); + Debug.Assert(connectedStream.CanRead); + WebSocket = WebSocket.CreateFromStream(connectedStream, new WebSocketCreationOptions + { + IsServer = false, + SubProtocol = subprotocol, + KeepAliveInterval = this.WebSocketSettings.KeepAliveInterval, + //???KeepAliveTimeout = options.KeepAliveTimeout + //???DangerousDeflateOptions = negotiatedDeflateOptions + }); + } + catch (Exception exc) + { + Abort(); + disposeResponse = true; + + if (exc is WebSocketException || exc is OperationCanceledException) + { + throw; + } + + throw new WebSocketException(WebSocketError.Faulted, SR.net_webstatus_ConnectFailure, exc); + } + finally + { + if (response is not null) + { + if (disposeResponse) + { + response.Dispose(); + } + } + + // Disposing the invoker will not affect any active stream wrapped in the WebSocket. + if (disposeInvoker) + { + invoker?.Dispose(); + } + } } finally { @@ -124,95 +255,122 @@ protected internal override async Task OnOpenAsync(TimeSpan timeout) } } - private void ValidateWebSocketConnection(ClientWebSocket clientWebSocket) + private void ValidateResponse(HttpResponseMessage response, string secValue) { - string requested = WebSocketSettings.SubProtocol; - string obtained = clientWebSocket.SubProtocol; - if (!(requested == null ? string.IsNullOrWhiteSpace(obtained) : requested.Equals(obtained, StringComparison.OrdinalIgnoreCase))) + Debug.Assert(response.Version == HttpVersion.Version11 || response.Version == HttpVersion.Version20); + + if (response.Version == HttpVersion.Version11) { - clientWebSocket.Dispose(); - throw FxTrace.Exception.AsError(new InvalidOperationException(SR.Format(SR.WebSocketInvalidProtocolNotInClientList, obtained, requested))); + if (response.StatusCode != HttpStatusCode.SwitchingProtocols) + { + throw new WebSocketException(WebSocketError.NotAWebSocket, SR.Format(SR.net_WebSockets_ConnectStatusExpected, (int)response.StatusCode, (int)HttpStatusCode.SwitchingProtocols)); + } + + Debug.Assert(secValue != null); + + // The Connection, Upgrade, and SecWebSocketAccept headers are required and with specific values. + ValidateHeader(response.Headers, HttpKnownHeaderNames.Connection, "Upgrade"); + ValidateHeader(response.Headers, HttpKnownHeaderNames.Upgrade, "websocket"); + ValidateHeader(response.Headers, HttpKnownHeaderNames.SecWebSocketAccept, secValue); } - } - private async Task ConfigureClientWebSocketAsync(ClientWebSocket clientWebSocket, TimeSpan timeout) - { - TimeoutHelper helper = new TimeoutHelper(timeout); - ChannelParameterCollection channelParameterCollection = new ChannelParameterCollection(); - if (HttpChannelFactory.MapIdentity(RemoteAddress, _channelFactory.AuthenticationScheme)) + if (response.Content is null) { - clientWebSocket.Options.SetRequestHeader("Host", HttpTransportSecurityHelpers.GetIdentityHostHeader(RemoteAddress)); + throw new WebSocketException(WebSocketError.ConnectionClosedPrematurely); } + } - (_webRequestTokenProvider, _webRequestProxyTokenProvider) = - await _channelFactory.CreateAndOpenTokenProvidersAsync( - RemoteAddress, - Via, - channelParameterCollection, - helper.RemainingTime()); - - SecurityTokenContainer clientCertificateToken = null; - if (_channelFactory is HttpsChannelFactory httpsChannelFactory && httpsChannelFactory.RequireClientCertificate) + private static void ValidateHeader(HttpHeaders headers, string name, string expectedValue) + { + if (headers.NonValidated.TryGetValues(name, out HeaderStringValues hsv)) { - SecurityTokenProvider certificateProvider = await httpsChannelFactory.CreateAndOpenCertificateTokenProviderAsync(RemoteAddress, Via, channelParameterCollection, helper.RemainingTime()); - clientCertificateToken = await httpsChannelFactory.GetCertificateSecurityTokenAsync(certificateProvider, RemoteAddress, Via, channelParameterCollection, helper); - if (clientCertificateToken != null) + if (hsv.Count == 1) { - X509SecurityToken x509Token = (X509SecurityToken)clientCertificateToken.Token; - clientWebSocket.Options.ClientCertificates.Add(x509Token.Certificate); + foreach (string value in hsv) + { + if (string.Equals(value, expectedValue, StringComparison.OrdinalIgnoreCase)) + { + return; + } + break; + } } - if (httpsChannelFactory.WebSocketCertificateCallback != null) - { - clientWebSocket.Options.RemoteCertificateValidationCallback = httpsChannelFactory.WebSocketCertificateCallback; - } + throw new WebSocketException(WebSocketError.HeaderError, SR.Format(SR.net_WebSockets_InvalidResponseHeader, name, hsv)); } - if (WebSocketSettings.SubProtocol != null) + throw new WebSocketException(WebSocketError.Faulted, SR.Format(SR.net_WebSockets_MissingResponseHeader, name)); + } + + private async Task<(HttpMessageInvoker invoker, bool disposeInvoker)> SetupInvoker(TimeSpan timeout) + { + TimeoutHelper helper = new TimeoutHelper(timeout); + ChannelParameterCollection channelParameterCollection = new ChannelParameterCollection(); + bool disposeInvoker = true; + var handler = new SocketsHttpHandler(); + handler.PooledConnectionLifetime = TimeSpan.Zero; + if (_channelFactory.AllowCookies) { - clientWebSocket.Options.AddSubProtocol(WebSocketSettings.SubProtocol); + var cookieContainerManager = _channelFactory.GetHttpCookieContainerManager(); + handler.CookieContainer = cookieContainerManager.CookieContainer; + handler.UseCookies = cookieContainerManager.CookieContainer != null; } - // These headers were added for WCF specific handshake to avoid encoder or transfermode mismatch between client and server. - // For BinaryMessageEncoder, since we are using a sessionful channel for websocket, the encoder is actually different when - // we are using Buffered or Stramed transfermode. So we need an extra header to identify the transfermode we are using, just - // to make people a little bit easier to diagnose these mismatch issues. - if (_channelFactory.MessageVersion != MessageVersion.None) + //configure handler.SslOptions + SecurityTokenContainer clientCertificateToken = null; + if (_channelFactory is HttpsChannelFactory httpsChannelFactory) { - clientWebSocket.Options.SetRequestHeader(WebSocketTransportSettings.SoapContentTypeHeader, _channelFactory.WebSocketSoapContentType); - - if (_channelFactory.MessageEncoderFactory is BinaryMessageEncoderFactory) + if(httpsChannelFactory.RequireClientCertificate) + { + SecurityTokenProvider certificateProvider = await httpsChannelFactory.CreateAndOpenCertificateTokenProviderAsync(RemoteAddress, Via, channelParameterCollection, helper.RemainingTime()); + clientCertificateToken = await httpsChannelFactory.GetCertificateSecurityTokenAsync(certificateProvider, RemoteAddress, Via, channelParameterCollection, helper); + if (clientCertificateToken != null) + { + X509SecurityToken x509Token = (X509SecurityToken)clientCertificateToken.Token; + Debug.Assert(handler.SslOptions.ClientCertificates == null); + handler.SslOptions.ClientCertificates = new X509Certificate2Collection + { + x509Token.Certificate + }; + } + } + + if (httpsChannelFactory.WebSocketCertificateCallback != null) { - clientWebSocket.Options.SetRequestHeader(WebSocketTransportSettings.BinaryEncoderTransferModeHeader, _channelFactory.TransferMode.ToString()); + handler.SslOptions.RemoteCertificateValidationCallback = httpsChannelFactory.WebSocketCertificateCallback; } } + //configure handler.Proxy (NetworkCredential credential, TokenImpersonationLevel impersonationLevel, AuthenticationLevel authenticationLevel) = - await HttpChannelUtilities.GetCredentialAsync(_channelFactory.AuthenticationScheme, _webRequestTokenProvider, timeout); - + await HttpChannelUtilities.GetCredentialAsync(_channelFactory.AuthenticationScheme, _webRequestTokenProvider, timeout); if (_channelFactory.Proxy != null) { - clientWebSocket.Options.Proxy = _channelFactory.Proxy; + handler.Proxy = _channelFactory.Proxy; } else if (_channelFactory.ProxyFactory != null) { - clientWebSocket.Options.Proxy = await _channelFactory.ProxyFactory.CreateWebProxyAsync( + handler.Proxy = await _channelFactory.ProxyFactory.CreateWebProxyAsync( authenticationLevel, impersonationLevel, _webRequestProxyTokenProvider, helper.RemainingTime()); } + else + { + handler.UseProxy = false; + } + //configure handler.Credentials if (credential == CredentialCache.DefaultCredentials || credential == null) { if (_channelFactory.AuthenticationScheme != AuthenticationSchemes.Anonymous) { - clientWebSocket.Options.UseDefaultCredentials = true; + handler.Credentials = CredentialCache.DefaultCredentials; } } else { - clientWebSocket.Options.UseDefaultCredentials = false; CredentialCache credentials = new CredentialCache(); Uri credentialCacheUriPrefix = _channelFactory.GetCredentialCacheUriPrefix(Via); if (_channelFactory.AuthenticationScheme == AuthenticationSchemes.IntegratedWindowsAuthentication) @@ -228,16 +386,72 @@ await _channelFactory.CreateAndOpenTokenProvidersAsync( credential); } - clientWebSocket.Options.Credentials = credentials; + handler.Credentials = credentials; } - if (_channelFactory.AllowCookies) + return (new HttpMessageInvoker(handler), disposeInvoker); + } + + private string AddWebSocketHeaders(HttpRequestMessage request) + { + // always exact because we handle downgrade here + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + string secValue = null; + + if (request.Version == HttpVersion.Version11) { - var cookieContainerManager = _channelFactory.GetHttpCookieContainerManager(); - clientWebSocket.Options.Cookies = cookieContainerManager.CookieContainer; + // Create the security key and expected response, then build all of the request headers + KeyValuePair secKeyAndSecWebSocketAccept = CreateSecKeyAndSecWebSocketAccept(); + secValue = secKeyAndSecWebSocketAccept.Value; + request.Headers.TryAddWithoutValidation(HttpKnownHeaderNames.Connection, HttpKnownHeaderNames.Upgrade); + request.Headers.TryAddWithoutValidation(HttpKnownHeaderNames.Upgrade, "websocket"); + request.Headers.TryAddWithoutValidation(HttpKnownHeaderNames.SecWebSocketKey, secKeyAndSecWebSocketAccept.Key); + } + else if (request.Version == HttpVersion.Version20) + { + request.Headers.Protocol = "websocket"; + } + + request.Headers.TryAddWithoutValidation(HttpKnownHeaderNames.SecWebSocketVersion, "13"); + + if (WebSocketSettings.SubProtocol != null) + { + request.Headers.TryAddWithoutValidation(HttpKnownHeaderNames.SecWebSocketProtocol, WebSocketSettings.SubProtocol); } + + return secValue; + } + + /// + /// Creates a pair of a security key for sending in the Sec-WebSocket-Key header and + /// the associated response we expect to receive as the Sec-WebSocket-Accept header value. + /// + /// A key-value pair of the request header security key and expected response header value. + [SuppressMessage("Microsoft.Security", "CA5350", Justification = "Required by RFC6455")] + private static KeyValuePair CreateSecKeyAndSecWebSocketAccept() + { + // GUID appended by the server as part of the security key response. Defined in the RFC. + ReadOnlySpan wsServerGuidBytes = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"u8; + + Span bytes = stackalloc byte[24 /* Base64 guid length */ + wsServerGuidBytes.Length]; + + // Base64-encode a new Guid's bytes to get the security key + bool success = Guid.NewGuid().TryWriteBytes(bytes); + Debug.Assert(success); + string secKey = Convert.ToBase64String(bytes.Slice(0, 16 /*sizeof(Guid)*/)); + + // Get the corresponding ASCII bytes for seckey+wsServerGuidBytes + int encodedSecKeyLength = Encoding.ASCII.GetBytes(secKey, bytes); + wsServerGuidBytes.CopyTo(bytes.Slice(encodedSecKeyLength)); + + // Hash the seckey+wsServerGuidBytes bytes + System.Security.Cryptography.SHA1.TryHashData(bytes, bytes, out int bytesWritten); + Debug.Assert(bytesWritten == 20 /* SHA1 hash length */); - clientWebSocket.Options.KeepAliveInterval = _channelFactory.WebSocketSettings.KeepAliveInterval; + // Return the security key + the base64 encoded hashed bytes + return new KeyValuePair( + secKey, + Convert.ToBase64String(bytes.Slice(0, bytesWritten))); } protected override void OnCleanup() diff --git a/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/HttpKnownHeaderNames.cs b/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/HttpKnownHeaderNames.cs new file mode 100644 index 00000000000..804035ec16b --- /dev/null +++ b/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/HttpKnownHeaderNames.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.ServiceModel.Channels +{ + // from https://source.dot.net/#System.Net.WebSockets.Client/src/libraries/Common/src/System/Net/HttpKnownHeaderNames.cs + internal static partial class HttpKnownHeaderNames + { + public const string Accept = "Accept"; + public const string AcceptCharset = "Accept-Charset"; + public const string AcceptEncoding = "Accept-Encoding"; + public const string AcceptLanguage = "Accept-Language"; + public const string AcceptPatch = "Accept-Patch"; + public const string AcceptRanges = "Accept-Ranges"; + public const string AccessControlAllowCredentials = "Access-Control-Allow-Credentials"; + public const string AccessControlAllowHeaders = "Access-Control-Allow-Headers"; + public const string AccessControlAllowMethods = "Access-Control-Allow-Methods"; + public const string AccessControlAllowOrigin = "Access-Control-Allow-Origin"; + public const string AccessControlExposeHeaders = "Access-Control-Expose-Headers"; + public const string AccessControlMaxAge = "Access-Control-Max-Age"; + public const string Age = "Age"; + public const string Allow = "Allow"; + public const string AltSvc = "Alt-Svc"; + public const string Authorization = "Authorization"; + public const string CacheControl = "Cache-Control"; + public const string Connection = "Connection"; + public const string ContentDisposition = "Content-Disposition"; + public const string ContentEncoding = "Content-Encoding"; + public const string ContentLanguage = "Content-Language"; + public const string ContentLength = "Content-Length"; + public const string ContentLocation = "Content-Location"; + public const string ContentMD5 = "Content-MD5"; + public const string ContentRange = "Content-Range"; + public const string ContentSecurityPolicy = "Content-Security-Policy"; + public const string ContentType = "Content-Type"; + public const string Cookie = "Cookie"; + public const string Cookie2 = "Cookie2"; + public const string Date = "Date"; + public const string ETag = "ETag"; + public const string Expect = "Expect"; + public const string Expires = "Expires"; + public const string From = "From"; + public const string Host = "Host"; + public const string IfMatch = "If-Match"; + public const string IfModifiedSince = "If-Modified-Since"; + public const string IfNoneMatch = "If-None-Match"; + public const string IfRange = "If-Range"; + public const string IfUnmodifiedSince = "If-Unmodified-Since"; + public const string KeepAlive = "Keep-Alive"; + public const string LastModified = "Last-Modified"; + public const string Link = "Link"; + public const string Location = "Location"; + public const string MaxForwards = "Max-Forwards"; + public const string Origin = "Origin"; + public const string P3P = "P3P"; + public const string Pragma = "Pragma"; + public const string ProxyAuthenticate = "Proxy-Authenticate"; + public const string ProxyAuthorization = "Proxy-Authorization"; + public const string ProxyConnection = "Proxy-Connection"; + public const string PublicKeyPins = "Public-Key-Pins"; + public const string Range = "Range"; + public const string Referer = "Referer"; // NB: The spelling-mistake "Referer" for "Referrer" must be matched. + public const string RetryAfter = "Retry-After"; + public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; + public const string SecWebSocketExtensions = "Sec-WebSocket-Extensions"; + public const string SecWebSocketKey = "Sec-WebSocket-Key"; + public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; + public const string SecWebSocketVersion = "Sec-WebSocket-Version"; + public const string Server = "Server"; + public const string SetCookie = "Set-Cookie"; + public const string SetCookie2 = "Set-Cookie2"; + public const string StrictTransportSecurity = "Strict-Transport-Security"; + public const string TE = "TE"; + public const string TSV = "TSV"; + public const string Trailer = "Trailer"; + public const string TransferEncoding = "Transfer-Encoding"; + public const string Upgrade = "Upgrade"; + public const string UpgradeInsecureRequests = "Upgrade-Insecure-Requests"; + public const string UserAgent = "User-Agent"; + public const string Vary = "Vary"; + public const string Via = "Via"; + public const string WWWAuthenticate = "WWW-Authenticate"; + public const string Warning = "Warning"; + public const string XAspNetVersion = "X-AspNet-Version"; + public const string XContentDuration = "X-Content-Duration"; + public const string XContentTypeOptions = "X-Content-Type-Options"; + public const string XFrameOptions = "X-Frame-Options"; + public const string XMSEdgeRef = "X-MSEdge-Ref"; + public const string XPoweredBy = "X-Powered-By"; + public const string XRequestID = "X-Request-ID"; + public const string XUACompatible = "X-UA-Compatible"; + } +} From 17d0c2a8c50e341c0077fbd5cac235d2832e73f2 Mon Sep 17 00:00:00 2001 From: Carol Wang Date: Thu, 27 Feb 2025 02:04:13 +0000 Subject: [PATCH 2/2] Clean up. --- ...tWebSocketTransportDuplexSessionChannel.cs | 32 +++---------------- .../Channels/HttpKnownHeaderNames.cs | 1 - 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/ClientWebSocketTransportDuplexSessionChannel.cs b/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/ClientWebSocketTransportDuplexSessionChannel.cs index 6a10a20c666..7de85e7ea3d 100644 --- a/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/ClientWebSocketTransportDuplexSessionChannel.cs +++ b/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/ClientWebSocketTransportDuplexSessionChannel.cs @@ -78,22 +78,6 @@ protected internal override async Task OnOpenAsync(TimeSpan timeout) try { - //old implementation: - //var clientWebSocket = new ClientWebSocket(); - //await ConfigureClientWebSocketAsync(clientWebSocket, helper.RemainingTime()); - //await clientWebSocket.ConnectAsync(Via, await helper.GetCancellationTokenAsync()); - //ValidateWebSocketConnection(clientWebSocket); - //WebSocket = clientWebSocket; - - /* new implementation: - 1. Create a SocketsHttpHandler, set certificate, certificate validation callback, and credentials. Wrap in an HttpMessageInvoker. - 2. Create an HttpRequestMessage. Set the required WebSocket headers, including any sub protocols, set the service url etc. - 3. Send the request using the invoker - 4. Validate the response headers, including upgrade status code, websocket specific headers, sub protocol. - 5. Fetch the content stream from the response content - 6. Wrap the content stream in a websocket - */ - try { while (true) @@ -101,13 +85,6 @@ 6. Wrap the content stream in a websocket try { HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Via) { Version = HttpVersion.Version11 }; - //if (options._requestHeaders?.Count > 0) // use field to avoid lazily initializing the collection - //{ - // foreach (string key in options.RequestHeaders) - // { - // request.Headers.TryAddWithoutValidation(key, options.RequestHeaders[key]); - // } - //} // These headers were added for WCF specific handshake to avoid encoder or transfermode mismatch between client and server. // For BinaryMessageEncoder, since we are using a sessionful channel for websocket, the encoder is actually different when @@ -320,7 +297,7 @@ private static void ValidateHeader(HttpHeaders headers, string name, string expe SecurityTokenContainer clientCertificateToken = null; if (_channelFactory is HttpsChannelFactory httpsChannelFactory) { - if(httpsChannelFactory.RequireClientCertificate) + if (httpsChannelFactory.RequireClientCertificate) { SecurityTokenProvider certificateProvider = await httpsChannelFactory.CreateAndOpenCertificateTokenProviderAsync(RemoteAddress, Via, channelParameterCollection, helper.RemainingTime()); clientCertificateToken = await httpsChannelFactory.GetCertificateSecurityTokenAsync(certificateProvider, RemoteAddress, Via, channelParameterCollection, helper); @@ -334,7 +311,7 @@ private static void ValidateHeader(HttpHeaders headers, string name, string expe }; } } - + //Fix for issue #5729: Removed the httpsChannelFactory.RequireClientCertificate condition from the following if statement. if (httpsChannelFactory.WebSocketCertificateCallback != null) { handler.SslOptions.RemoteCertificateValidationCallback = httpsChannelFactory.WebSocketCertificateCallback; @@ -343,7 +320,7 @@ private static void ValidateHeader(HttpHeaders headers, string name, string expe //configure handler.Proxy (NetworkCredential credential, TokenImpersonationLevel impersonationLevel, AuthenticationLevel authenticationLevel) = - await HttpChannelUtilities.GetCredentialAsync(_channelFactory.AuthenticationScheme, _webRequestTokenProvider, timeout); + await HttpChannelUtilities.GetCredentialAsync(_channelFactory.AuthenticationScheme, _webRequestTokenProvider, timeout); if (_channelFactory.Proxy != null) { handler.Proxy = _channelFactory.Proxy; @@ -418,7 +395,7 @@ private string AddWebSocketHeaders(HttpRequestMessage request) { request.Headers.TryAddWithoutValidation(HttpKnownHeaderNames.SecWebSocketProtocol, WebSocketSettings.SubProtocol); } - + return secValue; } @@ -502,4 +479,3 @@ private void CleanupTokenProviders() } } } - diff --git a/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/HttpKnownHeaderNames.cs b/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/HttpKnownHeaderNames.cs index 804035ec16b..90829439203 100644 --- a/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/HttpKnownHeaderNames.cs +++ b/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/HttpKnownHeaderNames.cs @@ -3,7 +3,6 @@ namespace System.ServiceModel.Channels { - // from https://source.dot.net/#System.Net.WebSockets.Client/src/libraries/Common/src/System/Net/HttpKnownHeaderNames.cs internal static partial class HttpKnownHeaderNames { public const string Accept = "Accept";