/mcs/class/referencesource/System/net/System/Net/WebSockets/ClientWebSocket.cs

http://github.com/mono/mono · C# · 622 lines · 505 code · 78 blank · 39 comment · 74 complexity · aaf7a4795ff2e825104e6e6d3ae1b401 MD5 · raw file

  1. //------------------------------------------------------------------------------
  2. // <copyright file="ClientWebSocket.cs" company="Microsoft">
  3. // Copyright (c) Microsoft Corporation. All rights reserved.
  4. // </copyright>
  5. //------------------------------------------------------------------------------
  6. namespace System.Net.WebSockets
  7. {
  8. using System;
  9. using System.Collections.Generic;
  10. using System.Diagnostics.CodeAnalysis;
  11. using System.Diagnostics.Contracts;
  12. using System.Net;
  13. using System.Security.Cryptography.X509Certificates;
  14. using System.Threading;
  15. using System.Threading.Tasks;
  16. public sealed class ClientWebSocket : WebSocket
  17. {
  18. private readonly ClientWebSocketOptions options;
  19. private WebSocket innerWebSocket;
  20. private readonly CancellationTokenSource cts;
  21. // Stages of this class. Interlocked doesn't support enums.
  22. private int state;
  23. private const int created = 0;
  24. private const int connecting = 1;
  25. private const int connected = 2;
  26. private const int disposed = 3;
  27. static ClientWebSocket()
  28. {
  29. // Register ws: and wss: with WebRequest.Register so that WebRequest.Create returns a
  30. // WebSocket capable HttpWebRequest instance.
  31. WebSocket.RegisterPrefixes();
  32. }
  33. public ClientWebSocket()
  34. {
  35. if (Logging.On) Logging.Enter(Logging.WebSockets, this, ".ctor", null);
  36. if (!WebSocketProtocolComponent.IsSupported)
  37. {
  38. WebSocketHelpers.ThrowPlatformNotSupportedException_WSPC();
  39. }
  40. state = created;
  41. options = new ClientWebSocketOptions();
  42. cts = new CancellationTokenSource();
  43. if (Logging.On) Logging.Exit(Logging.WebSockets, this, ".ctor", null);
  44. }
  45. #region Properties
  46. public ClientWebSocketOptions Options { get { return options; } }
  47. public override WebSocketCloseStatus? CloseStatus
  48. {
  49. get
  50. {
  51. if (innerWebSocket != null)
  52. {
  53. return innerWebSocket.CloseStatus;
  54. }
  55. return null;
  56. }
  57. }
  58. public override string CloseStatusDescription
  59. {
  60. get
  61. {
  62. if (innerWebSocket != null)
  63. {
  64. return innerWebSocket.CloseStatusDescription;
  65. }
  66. return null;
  67. }
  68. }
  69. public override string SubProtocol
  70. {
  71. get
  72. {
  73. if (innerWebSocket != null)
  74. {
  75. return innerWebSocket.SubProtocol;
  76. }
  77. return null;
  78. }
  79. }
  80. public override WebSocketState State
  81. {
  82. get
  83. {
  84. // state == Connected or Disposed
  85. if (innerWebSocket != null)
  86. {
  87. return innerWebSocket.State;
  88. }
  89. switch (state)
  90. {
  91. case created:
  92. return WebSocketState.None;
  93. case connecting:
  94. return WebSocketState.Connecting;
  95. case disposed: // We only get here if disposed before connecting
  96. return WebSocketState.Closed;
  97. default:
  98. Contract.Assert(false, "NotImplemented: " + state);
  99. return WebSocketState.Closed;
  100. }
  101. }
  102. }
  103. #endregion Properties
  104. public Task ConnectAsync(Uri uri, CancellationToken cancellationToken)
  105. {
  106. if (uri == null)
  107. {
  108. throw new ArgumentNullException("uri");
  109. }
  110. if (!uri.IsAbsoluteUri)
  111. {
  112. throw new ArgumentException(SR.GetString(SR.net_uri_NotAbsolute), "uri");
  113. }
  114. if (uri.Scheme != Uri.UriSchemeWs && uri.Scheme != Uri.UriSchemeWss)
  115. {
  116. throw new ArgumentException(SR.GetString(SR.net_WebSockets_Scheme), "uri");
  117. }
  118. // Check that we have not started already
  119. int priorState = Interlocked.CompareExchange(ref state, connecting, created);
  120. if (priorState == disposed)
  121. {
  122. throw new ObjectDisposedException(GetType().FullName);
  123. }
  124. else if (priorState != created)
  125. {
  126. throw new InvalidOperationException(SR.GetString(SR.net_WebSockets_AlreadyStarted));
  127. }
  128. options.SetToReadOnly();
  129. return ConnectAsyncCore(uri, cancellationToken);
  130. }
  131. private async Task ConnectAsyncCore(Uri uri, CancellationToken cancellationToken)
  132. {
  133. HttpWebResponse response = null;
  134. CancellationTokenRegistration connectCancellation = new CancellationTokenRegistration();
  135. // Any errors from here on out are fatal and this instance will be disposed.
  136. try
  137. {
  138. HttpWebRequest request = CreateAndConfigureRequest(uri);
  139. if (Logging.On) Logging.Associate(Logging.WebSockets, this, request);
  140. connectCancellation = cancellationToken.Register(AbortRequest, request, false);
  141. response = await request.GetResponseAsync().SuppressContextFlow() as HttpWebResponse;
  142. Contract.Assert(response != null, "Not an HttpWebResponse");
  143. if (Logging.On) Logging.Associate(Logging.WebSockets, this, response);
  144. string subprotocol = ValidateResponse(request, response);
  145. innerWebSocket = WebSocket.CreateClientWebSocket(response.GetResponseStream(), subprotocol,
  146. options.ReceiveBufferSize, options.SendBufferSize, options.KeepAliveInterval, false,
  147. options.GetOrCreateBuffer());
  148. if (Logging.On) Logging.Associate(Logging.WebSockets, this, innerWebSocket);
  149. // Change internal state to 'connected' to enable the other methods
  150. if (Interlocked.CompareExchange(ref state, connected, connecting) != connecting)
  151. {
  152. // Aborted/Disposed during connect.
  153. throw new ObjectDisposedException(GetType().FullName);
  154. }
  155. }
  156. catch (WebException ex)
  157. {
  158. ConnectExceptionCleanup(response);
  159. WebSocketException wex = new WebSocketException(SR.GetString(SR.net_webstatus_ConnectFailure), ex);
  160. if (Logging.On) Logging.Exception(Logging.WebSockets, this, "ConnectAsync", wex);
  161. throw wex;
  162. }
  163. catch (Exception ex)
  164. {
  165. ConnectExceptionCleanup(response);
  166. if (Logging.On) Logging.Exception(Logging.WebSockets, this, "ConnectAsync", ex);
  167. throw;
  168. }
  169. finally
  170. {
  171. // We successfully connected (or failed trying), disengage from this token.
  172. // Otherwise any timeout/cancellation would apply to the full session.
  173. // In the failure case we need to release the reference to HWR.
  174. connectCancellation.Dispose();
  175. }
  176. }
  177. private void ConnectExceptionCleanup(HttpWebResponse response)
  178. {
  179. Dispose();
  180. if (response != null)
  181. {
  182. response.Dispose();
  183. }
  184. }
  185. private HttpWebRequest CreateAndConfigureRequest(Uri uri)
  186. {
  187. HttpWebRequest request = WebRequest.Create(uri) as HttpWebRequest;
  188. if (request == null)
  189. {
  190. throw new InvalidOperationException(SR.GetString(SR.net_WebSockets_InvalidRegistration));
  191. }
  192. // Request Headers
  193. foreach (string key in options.RequestHeaders.Keys)
  194. {
  195. request.Headers.Add(key, options.RequestHeaders[key]);
  196. }
  197. // SubProtocols
  198. if (options.RequestedSubProtocols.Count > 0)
  199. {
  200. request.Headers.Add(HttpKnownHeaderNames.SecWebSocketProtocol,
  201. string.Join(", ", options.RequestedSubProtocols));
  202. }
  203. // Creds
  204. if (options.UseDefaultCredentials)
  205. {
  206. request.UseDefaultCredentials = true;
  207. }
  208. else if (options.Credentials != null)
  209. {
  210. request.Credentials = options.Credentials;
  211. }
  212. // Certs
  213. if (options.InternalClientCertificates != null)
  214. {
  215. request.ClientCertificates = options.InternalClientCertificates;
  216. }
  217. request.Proxy = options.Proxy;
  218. request.CookieContainer = options.Cookies;
  219. // For Abort/Dispose. Calling Abort on the request at any point will close the connection.
  220. cts.Token.Register(AbortRequest, request, false);
  221. return request;
  222. }
  223. // Validate the response headers and return the sub-protocol.
  224. private string ValidateResponse(HttpWebRequest request, HttpWebResponse response)
  225. {
  226. // 101
  227. if (response.StatusCode != HttpStatusCode.SwitchingProtocols)
  228. {
  229. throw new WebSocketException(SR.GetString(SR.net_WebSockets_Connect101Expected,
  230. (int)response.StatusCode));
  231. }
  232. // Upgrade: websocket
  233. string upgradeHeader = response.Headers[HttpKnownHeaderNames.Upgrade];
  234. if (!string.Equals(upgradeHeader, WebSocketHelpers.WebSocketUpgradeToken,
  235. StringComparison.OrdinalIgnoreCase))
  236. {
  237. throw new WebSocketException(SR.GetString(SR.net_WebSockets_InvalidResponseHeader,
  238. HttpKnownHeaderNames.Upgrade, upgradeHeader));
  239. }
  240. // Connection: Upgrade
  241. string connectionHeader = response.Headers[HttpKnownHeaderNames.Connection];
  242. if (!string.Equals(connectionHeader, HttpKnownHeaderNames.Upgrade,
  243. StringComparison.OrdinalIgnoreCase))
  244. {
  245. throw new WebSocketException(SR.GetString(SR.net_WebSockets_InvalidResponseHeader,
  246. HttpKnownHeaderNames.Connection, connectionHeader));
  247. }
  248. // Sec-WebSocket-Accept derived from request Sec-WebSocket-Key
  249. string websocketAcceptHeader = response.Headers[HttpKnownHeaderNames.SecWebSocketAccept];
  250. string expectedAcceptHeader = WebSocketHelpers.GetSecWebSocketAcceptString(
  251. request.Headers[HttpKnownHeaderNames.SecWebSocketKey]);
  252. if (!string.Equals(websocketAcceptHeader, expectedAcceptHeader, StringComparison.OrdinalIgnoreCase))
  253. {
  254. throw new WebSocketException(SR.GetString(SR.net_WebSockets_InvalidResponseHeader,
  255. HttpKnownHeaderNames.SecWebSocketAccept, websocketAcceptHeader));
  256. }
  257. // Sec-WebSocket-Protocol matches one from request
  258. // A missing header is ok. It's also ok if the client didn't specify any.
  259. string subProtocol = response.Headers[HttpKnownHeaderNames.SecWebSocketProtocol];
  260. if (!string.IsNullOrWhiteSpace(subProtocol) && options.RequestedSubProtocols.Count > 0)
  261. {
  262. bool foundMatch = false;
  263. foreach (string requestedSubProtocol in options.RequestedSubProtocols)
  264. {
  265. if (string.Equals(requestedSubProtocol, subProtocol, StringComparison.OrdinalIgnoreCase))
  266. {
  267. foundMatch = true;
  268. break;
  269. }
  270. }
  271. if (!foundMatch)
  272. {
  273. throw new WebSocketException(SR.GetString(SR.net_WebSockets_AcceptUnsupportedProtocol,
  274. string.Join(", ", options.RequestedSubProtocols), subProtocol));
  275. }
  276. }
  277. return string.IsNullOrWhiteSpace(subProtocol) ? null : subProtocol; // May be null or valid.
  278. }
  279. public override Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage,
  280. CancellationToken cancellationToken)
  281. {
  282. ThrowIfNotConnected();
  283. return innerWebSocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken);
  284. }
  285. public override Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer,
  286. CancellationToken cancellationToken)
  287. {
  288. ThrowIfNotConnected();
  289. return innerWebSocket.ReceiveAsync(buffer, cancellationToken);
  290. }
  291. public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription,
  292. CancellationToken cancellationToken)
  293. {
  294. ThrowIfNotConnected();
  295. return innerWebSocket.CloseAsync(closeStatus, statusDescription, cancellationToken);
  296. }
  297. public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription,
  298. CancellationToken cancellationToken)
  299. {
  300. ThrowIfNotConnected();
  301. return innerWebSocket.CloseOutputAsync(closeStatus, statusDescription, cancellationToken);
  302. }
  303. public override void Abort()
  304. {
  305. if (state == disposed)
  306. {
  307. return;
  308. }
  309. if (innerWebSocket != null)
  310. {
  311. innerWebSocket.Abort();
  312. }
  313. Dispose();
  314. }
  315. private void AbortRequest(object obj)
  316. {
  317. HttpWebRequest request = (HttpWebRequest)obj;
  318. request.Abort();
  319. }
  320. public override void Dispose()
  321. {
  322. int priorState = Interlocked.Exchange(ref state, disposed);
  323. if (priorState == disposed)
  324. {
  325. // No cleanup required.
  326. return;
  327. }
  328. cts.Cancel(false);
  329. cts.Dispose();
  330. if (innerWebSocket != null)
  331. {
  332. innerWebSocket.Dispose();
  333. }
  334. }
  335. private void ThrowIfNotConnected()
  336. {
  337. if (state == disposed)
  338. {
  339. throw new ObjectDisposedException(GetType().FullName);
  340. }
  341. else if (state != connected)
  342. {
  343. throw new InvalidOperationException(SR.GetString(SR.net_WebSockets_NotConnected));
  344. }
  345. }
  346. }
  347. public sealed class ClientWebSocketOptions
  348. {
  349. private bool isReadOnly; // After ConnectAsync is called the options cannot be modified.
  350. private readonly IList<string> requestedSubProtocols;
  351. private readonly WebHeaderCollection requestHeaders;
  352. private TimeSpan keepAliveInterval;
  353. private int receiveBufferSize;
  354. private int sendBufferSize;
  355. private ArraySegment<byte>? buffer;
  356. private bool useDefaultCredentials;
  357. private ICredentials credentials;
  358. private IWebProxy proxy;
  359. private X509CertificateCollection clientCertificates;
  360. private CookieContainer cookies;
  361. internal ClientWebSocketOptions()
  362. {
  363. requestedSubProtocols = new List<string>();
  364. requestHeaders = new WebHeaderCollection(WebHeaderCollectionType.HttpWebRequest);
  365. Proxy = WebRequest.DefaultWebProxy;
  366. receiveBufferSize = WebSocketHelpers.DefaultReceiveBufferSize;
  367. sendBufferSize = WebSocketHelpers.DefaultClientSendBufferSize;
  368. keepAliveInterval = WebSocket.DefaultKeepAliveInterval;
  369. }
  370. #region HTTP Settings
  371. // Note that some headers are restricted like Host.
  372. public void SetRequestHeader(string headerName, string headerValue)
  373. {
  374. ThrowIfReadOnly();
  375. // WebHeadersColection performs the validation
  376. requestHeaders.Set(headerName, headerValue);
  377. }
  378. internal WebHeaderCollection RequestHeaders { get { return requestHeaders; } }
  379. public bool UseDefaultCredentials
  380. {
  381. get
  382. {
  383. return useDefaultCredentials;
  384. }
  385. set
  386. {
  387. ThrowIfReadOnly();
  388. useDefaultCredentials = value;
  389. }
  390. }
  391. public ICredentials Credentials
  392. {
  393. get
  394. {
  395. return credentials;
  396. }
  397. set
  398. {
  399. ThrowIfReadOnly();
  400. credentials = value;
  401. }
  402. }
  403. public IWebProxy Proxy
  404. {
  405. get
  406. {
  407. return proxy;
  408. }
  409. set
  410. {
  411. ThrowIfReadOnly();
  412. proxy = value;
  413. }
  414. }
  415. [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly",
  416. Justification = "This collectin will be handed off directly to HttpWebRequest.")]
  417. public X509CertificateCollection ClientCertificates
  418. {
  419. get
  420. {
  421. if (clientCertificates == null)
  422. {
  423. clientCertificates = new X509CertificateCollection();
  424. }
  425. return clientCertificates;
  426. }
  427. set
  428. {
  429. ThrowIfReadOnly();
  430. if (value == null)
  431. {
  432. throw new ArgumentNullException("value");
  433. }
  434. clientCertificates = value;
  435. }
  436. }
  437. internal X509CertificateCollection InternalClientCertificates { get { return clientCertificates; } }
  438. public CookieContainer Cookies
  439. {
  440. get
  441. {
  442. return cookies;
  443. }
  444. set
  445. {
  446. ThrowIfReadOnly();
  447. cookies = value;
  448. }
  449. }
  450. #endregion HTTP Settings
  451. #region WebSocket Settings
  452. public void SetBuffer(int receiveBufferSize, int sendBufferSize)
  453. {
  454. ThrowIfReadOnly();
  455. WebSocketHelpers.ValidateBufferSizes(receiveBufferSize, sendBufferSize);
  456. this.buffer = null;
  457. this.receiveBufferSize = receiveBufferSize;
  458. this.sendBufferSize = sendBufferSize;
  459. }
  460. public void SetBuffer(int receiveBufferSize, int sendBufferSize, ArraySegment<byte> buffer)
  461. {
  462. ThrowIfReadOnly();
  463. WebSocketHelpers.ValidateBufferSizes(receiveBufferSize, sendBufferSize);
  464. WebSocketHelpers.ValidateArraySegment(buffer, "buffer");
  465. WebSocketBuffer.Validate(buffer.Count, receiveBufferSize, sendBufferSize, false);
  466. this.receiveBufferSize = receiveBufferSize;
  467. this.sendBufferSize = sendBufferSize;
  468. // Only full-trust applications can specify their own buffer to be used as the
  469. // internal buffer for the WebSocket object. This is because the contents of the
  470. // buffer are used internally by the WebSocket as it marshals data with embedded
  471. // pointers to native code. A malicious application could use this to corrupt
  472. // native memory.
  473. if (AppDomain.CurrentDomain.IsFullyTrusted)
  474. {
  475. this.buffer = buffer;
  476. }
  477. else
  478. {
  479. // We silently ignore the passed in buffer and will create an internal
  480. // buffer later.
  481. this.buffer = null;
  482. }
  483. }
  484. internal int ReceiveBufferSize { get { return receiveBufferSize; } }
  485. internal int SendBufferSize { get { return sendBufferSize; } }
  486. internal ArraySegment<byte> GetOrCreateBuffer()
  487. {
  488. if (!buffer.HasValue)
  489. {
  490. buffer = WebSocket.CreateClientBuffer(receiveBufferSize, sendBufferSize);
  491. }
  492. return buffer.Value;
  493. }
  494. public void AddSubProtocol(string subProtocol)
  495. {
  496. ThrowIfReadOnly();
  497. WebSocketHelpers.ValidateSubprotocol(subProtocol);
  498. // Duplicates not allowed.
  499. foreach (string item in requestedSubProtocols)
  500. {
  501. if (string.Equals(item, subProtocol, StringComparison.OrdinalIgnoreCase))
  502. {
  503. throw new ArgumentException(SR.GetString(SR.net_WebSockets_NoDuplicateProtocol, subProtocol),
  504. "subProtocol");
  505. }
  506. }
  507. requestedSubProtocols.Add(subProtocol);
  508. }
  509. internal IList<string> RequestedSubProtocols { get { return requestedSubProtocols; } }
  510. public TimeSpan KeepAliveInterval
  511. {
  512. get
  513. {
  514. return keepAliveInterval;
  515. }
  516. set
  517. {
  518. ThrowIfReadOnly();
  519. if (value < Timeout.InfiniteTimeSpan)
  520. {
  521. throw new ArgumentOutOfRangeException("value", value,
  522. SR.GetString(SR.net_WebSockets_ArgumentOutOfRange_TooSmall,
  523. Timeout.InfiniteTimeSpan.ToString()));
  524. }
  525. keepAliveInterval = value;
  526. }
  527. }
  528. #endregion WebSocket settings
  529. #region Helpers
  530. internal void SetToReadOnly()
  531. {
  532. Contract.Assert(!isReadOnly, "Already set");
  533. isReadOnly = true;
  534. }
  535. private void ThrowIfReadOnly()
  536. {
  537. if (isReadOnly)
  538. {
  539. throw new InvalidOperationException(SR.GetString(SR.net_WebSockets_AlreadyStarted));
  540. }
  541. }
  542. #endregion Helpers
  543. }
  544. }