5 // Marek Safar <marek.safar@gmail.com>
7 // Copyright (C) 2013 Xamarin Inc (http://www.xamarin.com)
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:
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
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.
29 using System.Threading;
30 using System.Net.Http.Headers;
31 using System.Threading.Tasks;
33 using System.Collections.Generic;
39 using CF=CoreFoundation;
43 using CF=CoreFoundation;
45 using MonoTouch.CoreServices;
46 using MonoTouch.CoreFoundation;
47 using CF=MonoTouch.CoreFoundation;
50 namespace System.Net.Http
52 public class CFNetworkHandler : HttpMessageHandler
56 public TaskCompletionSource<HttpResponseMessage> Response;
57 public HttpRequestMessage Request;
58 public CancellationTokenRegistration CancellationTokenRegistration;
59 public CFContentStream ContentStream;
63 CancellationTokenRegistration.Dispose ();
64 if (ContentStream != null) {
65 ContentStream.Close ();
70 bool allowAutoRedirect;
73 CookieContainer cookies;
75 Dictionary<IntPtr, StreamBucket> streamBuckets;
77 public CFNetworkHandler ()
79 allowAutoRedirect = true;
80 streamBuckets = new Dictionary<IntPtr, StreamBucket> ();
83 void EnsureModifiability ()
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.");
91 public bool AllowAutoRedirect {
93 return allowAutoRedirect;
96 EnsureModifiability ();
97 allowAutoRedirect = value;
101 public CookieContainer CookieContainer {
106 EnsureModifiability ();
111 public bool UseSystemProxy {
113 return useSystemProxy;
116 EnsureModifiability ();
117 useSystemProxy = value;
121 // TODO: Add more properties
123 protected override void Dispose (bool disposing)
125 // TODO: CloseStream remaining stream buckets if there are any
127 base.Dispose (disposing);
130 CFHTTPMessage CreateWebRequestAsync (HttpRequestMessage request)
132 var req = CFHTTPMessage.CreateRequest (request.RequestUri, request.Method.Method, request.Version);
136 if (wr.ProtocolVersion == HttpVersion.Version10) {
137 wr.KeepAlive = request.Headers.ConnectionKeepAlive;
139 wr.KeepAlive = request.Headers.ConnectionClose != true;
142 if (useDefaultCredentials) {
143 wr.UseDefaultCredentials = true;
145 wr.Credentials = credentials;
152 if (cookies != null) {
153 string cookieHeader = cookies.GetCookieHeader (request.RequestUri);
154 if (cookieHeader != "")
155 req.SetHeaderFieldValue ("Cookie", cookieHeader);
158 foreach (var header in request.Headers) {
159 foreach (var value in header.Value) {
160 req.SetHeaderFieldValue (header.Key, value);
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);
175 protected internal override async Task<HttpResponseMessage> SendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
180 using (var message = CreateWebRequestAsync (request))
182 if (request.Content != null) {
183 var data = await request.Content.ReadAsByteArrayAsync ().ConfigureAwait (false);
184 message.SetBody (data);
187 stream = CFHTTPStream.CreateForHTTPRequest (message);
190 if (useSystemProxy) {
191 var proxies = CF.CFNetwork.GetSystemProxySettings ();
192 if (proxies.HTTPEnable) {
193 stream.SetProxy (proxies);
197 stream.ShouldAutoredirect = allowAutoRedirect;
198 stream.HasBytesAvailableEvent += HandleHasBytesAvailableEvent;
199 stream.ErrorEvent += HandleErrorEvent;
200 stream.ClosedEvent += HandleClosedEvent;
202 var response = new TaskCompletionSource<HttpResponseMessage> ();
204 if (cancellationToken.IsCancellationRequested) {
205 response.SetCanceled ();
206 return await response.Task;
209 var bucket = new StreamBucket () {
214 streamBuckets.Add (stream.Handle, bucket);
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
221 stream.EnableEvents (CF.CFRunLoop.Main, CF.CFRunLoop.ModeCommon);
223 stream.EnableEvents (CF.CFRunLoop.Main, CF.CFRunLoop.CFRunLoopCommonModes);
228 bucket.CancellationTokenRegistration = cancellationToken.Register (() => {
229 StreamBucket bucket2;
230 if (!streamBuckets.TryGetValue (stream.Handle, out bucket2))
233 bucket2.Response.TrySetCanceled ();
234 CloseStream (stream);
237 return await response.Task;
240 void HandleErrorEvent (object sender, CFStream.StreamEventArgs e)
242 var stream = (CFHTTPStream)sender;
245 if (!streamBuckets.TryGetValue (stream.Handle, out bucket))
248 bucket.Response.TrySetException (stream.GetError ());
249 CloseStream (stream);
252 void HandleClosedEvent (object sender, CFStream.StreamEventArgs e)
254 var stream = (CFHTTPStream)sender;
255 CloseStream (stream);
258 void CloseStream (CFHTTPStream stream)
261 if (streamBuckets.TryGetValue (stream.Handle, out bucket)) {
263 streamBuckets.Remove (stream.Handle);
269 void HandleHasBytesAvailableEvent (object sender, CFStream.StreamEventArgs e)
271 var stream = (CFHTTPStream) sender;
274 if (!streamBuckets.TryGetValue (stream.Handle, out bucket))
277 if (bucket.Response.Task.IsCompleted) {
278 bucket.ContentStream.ReadStreamData ();
282 var header = stream.GetResponseHeader ();
285 if (!header.IsHeaderComplete)
286 throw new NotImplementedException ();
288 bucket.ContentStream = new CFContentStream (stream);
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;
295 var fields = header.GetAllHeaderFields ();
296 if (fields != null) {
297 foreach (var entry in fields) {
298 if (entry.Key == null)
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;
307 item_headers = response_msg.Headers;
309 if (cookies != null && (key == "Set-Cookie" || key == "Set-Cookie2"))
310 AddCookie (value, bucket.Request.RequestUri, key);
313 item_headers.TryAddWithoutValidation (key, value);
317 bucket.Response.TrySetResult (response_msg);
319 bucket.ContentStream.ReadStreamData ();
322 void AddCookie (string value, Uri uri, string header)
324 CookieCollection cookies1 = null;
326 cookies1 = cookies.CookieCutter (uri, header, value, false);
330 if (cookies1 != null && cookies1.Count != 0)
331 cookies.Add (cookies1);