Merge pull request #924 from marcusva/master
[mono.git] / mcs / class / System / System.Net.WebSockets / ClientWebSocket.cs
1 //
2 // ClientWebSocket.cs
3 //
4 // Authors:
5 //        Jérémie Laval <jeremie dot laval at xamarin dot com>
6 //
7 // Copyright 2013 Xamarin Inc (http://www.xamarin.com).
8 //
9 // Lightly inspired from WebSocket4Net distributed under the Apache License 2.0
10 //
11 // Permission is hereby granted, free of charge, to any person obtaining a copy
12 // of this software and associated documentation files (the "Software"), to deal
13 // in the Software without restriction, including without limitation the rights
14 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 // copies of the Software, and to permit persons to whom the Software is
16 // furnished to do so, subject to the following conditions:
17 //
18 // The above copyright notice and this permission notice shall be included in
19 // all copies or substantial portions of the Software.
20 //
21 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27 // THE SOFTWARE.
28
29 #if NET_4_5
30
31 using System;
32 using System.Net;
33 using System.Net.Sockets;
34 using System.Security.Principal;
35 using System.Security.Cryptography.X509Certificates;
36 using System.Runtime.CompilerServices;
37 using System.Collections.Generic;
38 using System.Threading;
39 using System.Threading.Tasks;
40 using System.Globalization;
41 using System.Text;
42 using System.Security.Cryptography;
43
44 namespace System.Net.WebSockets
45 {
46         public class ClientWebSocket : WebSocket, IDisposable
47         {
48                 const string VersionTag = "13";
49
50                 ClientWebSocketOptions options;
51                 WebSocketState state;
52                 string subProtocol;
53
54                 HttpWebRequest req;
55                 WebConnection connection;
56                 StreamWebSocket internalWebSocket;
57
58                 public ClientWebSocket ()
59                 {
60                         options = new ClientWebSocketOptions ();
61                         state = WebSocketState.None;
62                 }
63
64                 public override void Dispose ()
65                 {
66                         if (internalWebSocket != null)
67                                 internalWebSocket.Dispose ();
68                 }
69
70                 public override void Abort ()
71                 {
72                         if (internalWebSocket != null)
73                                 internalWebSocket.Abort ();
74                 }
75
76                 public ClientWebSocketOptions Options {
77                         get {
78                                 return options;
79                         }
80                 }
81
82                 public override WebSocketState State {
83                         get {
84                                 if (internalWebSocket != null)
85                                         return internalWebSocket.State;
86                                 return state;
87                         }
88                 }
89
90                 public override WebSocketCloseStatus? CloseStatus {
91                         get {
92                                 if (internalWebSocket != null)
93                                         return internalWebSocket.CloseStatus;
94                                 if (state != WebSocketState.Closed)
95                                         return (WebSocketCloseStatus?)null;
96                                 return WebSocketCloseStatus.Empty;
97                         }
98                 }
99
100                 [MonoTODO]
101                 public override string CloseStatusDescription {
102                         get { return null; }
103                 }
104
105                 public override string SubProtocol {
106                         get { return subProtocol; }
107                 }
108
109                 public async Task ConnectAsync (Uri uri, CancellationToken cancellationToken)
110                 {
111                         state = WebSocketState.Connecting;
112                         var httpUri = new UriBuilder (uri);
113                         if (uri.Scheme == "wss")
114                                 httpUri.Scheme = "https";
115                         else
116                                 httpUri.Scheme = "http";
117                         req = (HttpWebRequest)WebRequest.Create (httpUri.Uri);
118                         req.ReuseConnection = true;
119                         if (options.Cookies != null)
120                                 req.CookieContainer = options.Cookies;
121
122                         if (options.CustomRequestHeaders.Count > 0) {
123                                 foreach (var header in options.CustomRequestHeaders)
124                                         req.Headers[header.Key] = header.Value;
125                         }
126
127                         var secKey = Convert.ToBase64String (Encoding.ASCII.GetBytes (Guid.NewGuid ().ToString ().Substring (0, 16)));
128                         string expectedAccept = StreamWebSocket.CreateAcceptKey (secKey);
129
130                         req.Headers["Upgrade"] = "WebSocket";
131                         req.Headers["Sec-WebSocket-Version"] = VersionTag;
132                         req.Headers["Sec-WebSocket-Key"] = secKey;
133                         req.Headers["Sec-WebSocket-Origin"] = uri.Host;
134                         if (options.SubProtocols.Count > 0)
135                                 req.Headers["Sec-WebSocket-Protocol"] = string.Join (",", options.SubProtocols);
136
137                         if (options.Credentials != null)
138                                 req.Credentials = options.Credentials;
139                         if (options.ClientCertificates != null)
140                                 req.ClientCertificates = options.ClientCertificates;
141                         if (options.Proxy != null)
142                                 req.Proxy = options.Proxy;
143                         req.UseDefaultCredentials = options.UseDefaultCredentials;
144                         req.Connection = "Upgrade";
145
146                         HttpWebResponse resp = null;
147                         try {
148                                 resp = (HttpWebResponse)(await req.GetResponseAsync ().ConfigureAwait (false));
149                         } catch (Exception e) {
150                                 throw new WebSocketException (WebSocketError.Success, e);
151                         }
152
153                         if (resp.StatusCode != HttpStatusCode.SwitchingProtocols)
154                                 throw new WebSocketException ("The server returned status code '" + (int)resp.StatusCode + "' when status code '101' was expected");
155                         if (!string.Equals (resp.Headers["Upgrade"], "WebSocket", StringComparison.OrdinalIgnoreCase)
156                                 || !string.Equals (resp.Headers["Connection"], "Upgrade", StringComparison.OrdinalIgnoreCase)
157                                 || !string.Equals (resp.Headers["Sec-WebSocket-Accept"], expectedAccept))
158                                 throw new WebSocketException ("HTTP header error during handshake");
159                         if (resp.Headers["Sec-WebSocket-Protocol"] != null) {
160                                 if (!options.SubProtocols.Contains (resp.Headers["Sec-WebSocket-Protocol"]))
161                                         throw new WebSocketException (WebSocketError.UnsupportedProtocol);
162                                 subProtocol = resp.Headers["Sec-WebSocket-Protocol"];
163                         }
164                         state = WebSocketState.Open;
165                         connection = req.StoredConnection;
166                         internalWebSocket = new StreamWebSocket(connection.nstream, connection.nstream, connection.socket, subProtocol, true, new ArraySegment<byte>(new byte[0]));
167                 }
168
169                 public override Task SendAsync (ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken)
170                 {
171                         return internalWebSocket.SendAsync (buffer, messageType, endOfMessage, cancellationToken);
172                 }
173
174                 public override Task<WebSocketReceiveResult> ReceiveAsync (ArraySegment<byte> buffer, CancellationToken cancellationToken)
175                 {
176                         return internalWebSocket.ReceiveAsync (buffer, cancellationToken);
177                 }
178
179                 // The damn difference between those two methods is that CloseAsync will wait for server acknowledgement before completing
180                 // while CloseOutputAsync will send the close packet and simply complete.
181
182                 public override Task CloseAsync (WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)
183                 {
184                         return internalWebSocket.CloseAsync (closeStatus, statusDescription, cancellationToken);
185                 }
186
187                 public override Task CloseOutputAsync (WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)
188                 {
189                         return internalWebSocket.CloseOutputAsync (closeStatus, statusDescription, cancellationToken);
190                 }
191         }
192 }
193
194 #endif