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..7de85e7ea3d 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,111 @@ 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; + try + { + while (true) + { + try + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Via) { Version = HttpVersion.Version11 }; + + // 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 +232,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) { - clientWebSocket.Options.SetRequestHeader(WebSocketTransportSettings.BinaryEncoderTransferModeHeader, _channelFactory.TransferMode.ToString()); + 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 + }; + } + } + //Fix for issue #5729: Removed the httpsChannelFactory.RequireClientCertificate condition from the following if statement. + if (httpsChannelFactory.WebSocketCertificateCallback != null) + { + handler.SslOptions.RemoteCertificateValidationCallback = httpsChannelFactory.WebSocketCertificateCallback; } } + //configure handler.Proxy (NetworkCredential credential, TokenImpersonationLevel impersonationLevel, AuthenticationLevel authenticationLevel) = 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 +363,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"; } - clientWebSocket.Options.KeepAliveInterval = _channelFactory.WebSocketSettings.KeepAliveInterval; + 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 */); + + // Return the security key + the base64 encoded hashed bytes + return new KeyValuePair( + secKey, + Convert.ToBase64String(bytes.Slice(0, bytesWritten))); } protected override void OnCleanup() @@ -288,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 new file mode 100644 index 00000000000..90829439203 --- /dev/null +++ b/src/System.ServiceModel.Http/src/System/ServiceModel/Channels/HttpKnownHeaderNames.cs @@ -0,0 +1,92 @@ +// 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 +{ + 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"; + } +}