Merge pull request #3142 from henricm/fix-for-win-mono_string_to_utf8
[mono.git] / mcs / class / System.Net.Http / CFNetworkHandler.cs
1 //
2 // CFNetworkHandler.cs
3 //
4 // Authors:
5 //      Marek Safar  <marek.safar@gmail.com>
6 //
7 // Copyright (C) 2013 Xamarin Inc (http://www.xamarin.com)
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
16 //
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
19 //
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 //
28
29 using System.Threading;
30 using System.Net.Http.Headers;
31 using System.Threading.Tasks;
32 using System.IO;
33 using System.Collections.Generic;
34 using System.Net;
35
36 #if XAMCORE_4_0
37 using CFNetwork;
38 using CoreFoundation;
39 using CF=CoreFoundation;
40 #elif XAMCORE_2_0
41 using CoreServices;
42 using CoreFoundation;
43 using CF=CoreFoundation;
44 #else
45 using MonoTouch.CoreServices;
46 using MonoTouch.CoreFoundation;
47 using CF=MonoTouch.CoreFoundation;
48 #endif
49
50 namespace System.Net.Http
51 {
52         public class CFNetworkHandler : HttpMessageHandler
53         {
54                 class StreamBucket
55                 {
56                         public TaskCompletionSource<HttpResponseMessage> Response;
57                         public HttpRequestMessage Request;
58                         public CancellationTokenRegistration CancellationTokenRegistration;
59                         public CFContentStream ContentStream;
60
61                         public void Close ()
62                         {
63                                 CancellationTokenRegistration.Dispose ();
64                                 if (ContentStream != null) {
65                                     ContentStream.Close ();
66                                 }
67                         }
68                 }
69
70                 bool allowAutoRedirect;
71                 bool sentRequest;
72                 bool useSystemProxy;
73                 CookieContainer cookies;
74
75                 Dictionary<IntPtr, StreamBucket> streamBuckets;
76
77                 public CFNetworkHandler ()
78                 {
79                         allowAutoRedirect = true;
80                         streamBuckets = new Dictionary<IntPtr, StreamBucket> ();
81                 }
82
83                 void EnsureModifiability ()
84                 {
85                         if (sentRequest)
86                                 throw new InvalidOperationException (
87                                         "This instance has already started one or more requests. " +
88                                         "Properties can only be modified before sending the first request.");
89                 }
90
91                 public bool AllowAutoRedirect {
92                         get {
93                                 return allowAutoRedirect;
94                         }
95                         set {
96                                 EnsureModifiability ();
97                                 allowAutoRedirect = value;
98                         }
99                 }
100
101                 public CookieContainer CookieContainer {
102                         get {
103                                 return cookies;
104                         }
105                         set {
106                                 EnsureModifiability ();
107                                 cookies = value;
108                         }
109                 }
110
111                 public bool UseSystemProxy {
112                         get {
113                                 return useSystemProxy;
114                         }
115                         set {
116                                 EnsureModifiability ();
117                                 useSystemProxy = value;
118                         }
119                 }
120
121                 // TODO: Add more properties
122
123                 protected override void Dispose (bool disposing)
124                 {
125                         // TODO: CloseStream remaining stream buckets if there are any
126
127                         base.Dispose (disposing);
128                 }
129
130                 CFHTTPMessage CreateWebRequestAsync (HttpRequestMessage request)
131                 {
132                         var req = CFHTTPMessage.CreateRequest (request.RequestUri, request.Method.Method, request.Version);
133
134                         // TODO:
135 /*
136                         if (wr.ProtocolVersion == HttpVersion.Version10) {
137                                 wr.KeepAlive = request.Headers.ConnectionKeepAlive;
138                         } else {
139                                 wr.KeepAlive = request.Headers.ConnectionClose != true;
140                         }
141
142                         if (useDefaultCredentials) {
143                                 wr.UseDefaultCredentials = true;
144                         } else {
145                                 wr.Credentials = credentials;
146                         }
147
148                         if (useProxy) {
149                                 wr.Proxy = proxy;
150                         }
151 */
152                         if (cookies != null) {
153                                 string cookieHeader = cookies.GetCookieHeader (request.RequestUri);
154                                 if (cookieHeader != "")
155                                         req.SetHeaderFieldValue ("Cookie", cookieHeader);
156                         }
157
158                         foreach (var header in request.Headers) {
159                                 foreach (var value in header.Value) {
160                                         req.SetHeaderFieldValue (header.Key, value);
161                                 }
162                         }
163
164                         if (request.Content != null) {
165                                 foreach (var header in request.Content.Headers) {
166                                         foreach (var value in header.Value) {
167                                                 req.SetHeaderFieldValue (header.Key, value);
168                                         }
169                                 }
170                         }
171
172                         return req;
173                 }
174
175                 protected internal override async Task<HttpResponseMessage> SendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
176                 {
177                         sentRequest = true;
178
179                         CFHTTPStream stream;
180                         using (var message = CreateWebRequestAsync (request))
181                         {
182                                 if (request.Content != null) {
183                                         var data = await request.Content.ReadAsByteArrayAsync ().ConfigureAwait (false);
184                                         message.SetBody (data);
185                                 }
186
187                                 stream = CFHTTPStream.CreateForHTTPRequest (message);
188                         }
189
190                         if (useSystemProxy) {
191                                 var proxies = CF.CFNetwork.GetSystemProxySettings ();
192                                 if (proxies.HTTPEnable) {
193                                         stream.SetProxy (proxies);
194                                 }
195                         }
196
197                         stream.ShouldAutoredirect = allowAutoRedirect;
198                         stream.HasBytesAvailableEvent += HandleHasBytesAvailableEvent;
199                         stream.ErrorEvent += HandleErrorEvent;
200                         stream.ClosedEvent += HandleClosedEvent;
201
202                         var response = new TaskCompletionSource<HttpResponseMessage> ();
203
204                         if (cancellationToken.IsCancellationRequested) {
205                                 response.SetCanceled ();
206                                 return await response.Task;
207                         }
208
209                         var bucket = new StreamBucket () {
210                                 Request = request,
211                                 Response = response,
212                         };
213
214                         streamBuckets.Add (stream.Handle, bucket);
215
216                         //
217                         // Always schedule stream events handling on main-loop. Due to ConfigureAwait (false) we may end up
218                         // on any thread-pool thread which may not have run-loop running
219                         //
220 #if XAMCORE_2_0
221                         stream.EnableEvents (CF.CFRunLoop.Main, CF.CFRunLoop.ModeCommon);
222 #else
223                         stream.EnableEvents (CF.CFRunLoop.Main, CF.CFRunLoop.CFRunLoopCommonModes);
224 #endif
225
226                         stream.Open ();
227
228                         bucket.CancellationTokenRegistration = cancellationToken.Register (() => {
229                                 StreamBucket bucket2;
230                                 if (!streamBuckets.TryGetValue (stream.Handle, out bucket2))
231                                         return;
232
233                                 bucket2.Response.TrySetCanceled ();
234                                 CloseStream (stream);
235                         });
236
237                         return await response.Task;
238                 }
239
240                 void HandleErrorEvent (object sender, CFStream.StreamEventArgs e)
241                 {
242                         var stream = (CFHTTPStream)sender;
243
244                         StreamBucket bucket;
245                         if (!streamBuckets.TryGetValue (stream.Handle, out bucket))
246                                 return;
247
248                         bucket.Response.TrySetException (stream.GetError ());
249                         CloseStream (stream);
250                 }
251
252                 void HandleClosedEvent (object sender, CFStream.StreamEventArgs e)
253                 {
254                         var stream = (CFHTTPStream)sender;
255                         CloseStream (stream);
256                 }
257
258                 void CloseStream (CFHTTPStream stream)
259                 {
260                         StreamBucket bucket;
261                         if (streamBuckets.TryGetValue (stream.Handle, out bucket)) {
262                                 bucket.Close ();
263                                 streamBuckets.Remove (stream.Handle);
264                         }
265
266                         stream.Close ();
267                 }
268
269                 void HandleHasBytesAvailableEvent (object sender, CFStream.StreamEventArgs e)
270                 {
271                         var stream = (CFHTTPStream) sender;
272
273                         StreamBucket bucket;
274                         if (!streamBuckets.TryGetValue (stream.Handle, out bucket))
275                                 return;
276
277                         if (bucket.Response.Task.IsCompleted) {
278                                 bucket.ContentStream.ReadStreamData ();
279                                 return;
280                         }
281
282                         var header = stream.GetResponseHeader ();
283
284                         // Is this possible?
285                         if (!header.IsHeaderComplete)
286                                 throw new NotImplementedException ();
287
288                         bucket.ContentStream = new CFContentStream (stream);
289                                 
290                         var response_msg = new HttpResponseMessage (header.ResponseStatusCode);
291                         response_msg.RequestMessage = bucket.Request;
292                         response_msg.ReasonPhrase = header.ResponseStatusLine;
293                         response_msg.Content = bucket.ContentStream;
294
295                         var fields = header.GetAllHeaderFields ();
296                         if (fields != null) {
297                                 foreach (var entry in fields) {
298                                         if (entry.Key == null)
299                                                 continue;
300
301                                         var key = entry.Key.ToString ();
302                                         var value = entry.Value == null ? string.Empty : entry.Value.ToString ();
303                                         HttpHeaders item_headers;
304                                         if (HttpHeaders.GetKnownHeaderKind (key) == Headers.HttpHeaderKind.Content) {
305                                                 item_headers = response_msg.Content.Headers;
306                                         } else {
307                                                 item_headers = response_msg.Headers;
308
309                                                 if (cookies != null && (key == "Set-Cookie" || key == "Set-Cookie2"))
310                                                         AddCookie (value, bucket.Request.RequestUri, key);
311                                         }
312
313                                         item_headers.TryAddWithoutValidation (key, value);
314                                 }
315                         }
316
317                         bucket.Response.TrySetResult (response_msg);
318
319                         bucket.ContentStream.ReadStreamData ();
320                 }
321
322                 void AddCookie (string value, Uri uri, string header)
323                 {
324                         CookieCollection cookies1 = null;
325                         try {
326                                 cookies1 = cookies.CookieCutter (uri, header, value, false);
327                         } catch {
328                         }
329
330                         if (cookies1 != null && cookies1.Count != 0) 
331                                 cookies.Add (cookies1);
332                 }
333         }
334 }