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 ContentStream.Close ();
68 bool allowAutoRedirect;
71 CookieContainer cookies;
73 Dictionary<IntPtr, StreamBucket> streamBuckets;
75 public CFNetworkHandler ()
77 allowAutoRedirect = true;
78 streamBuckets = new Dictionary<IntPtr, StreamBucket> ();
81 void EnsureModifiability ()
84 throw new InvalidOperationException (
85 "This instance has already started one or more requests. " +
86 "Properties can only be modified before sending the first request.");
89 public bool AllowAutoRedirect {
91 return allowAutoRedirect;
94 EnsureModifiability ();
95 allowAutoRedirect = value;
99 public CookieContainer CookieContainer {
104 EnsureModifiability ();
109 public bool UseSystemProxy {
111 return useSystemProxy;
114 EnsureModifiability ();
115 useSystemProxy = value;
119 // TODO: Add more properties
121 protected override void Dispose (bool disposing)
123 // TODO: CloseStream remaining stream buckets if there are any
125 base.Dispose (disposing);
128 CFHTTPMessage CreateWebRequestAsync (HttpRequestMessage request)
130 var req = CFHTTPMessage.CreateRequest (request.RequestUri, request.Method.Method, request.Version);
134 if (wr.ProtocolVersion == HttpVersion.Version10) {
135 wr.KeepAlive = request.Headers.ConnectionKeepAlive;
137 wr.KeepAlive = request.Headers.ConnectionClose != true;
140 if (useDefaultCredentials) {
141 wr.UseDefaultCredentials = true;
143 wr.Credentials = credentials;
150 if (cookies != null) {
151 string cookieHeader = cookies.GetCookieHeader (request.RequestUri);
152 if (cookieHeader != "")
153 req.SetHeaderFieldValue ("Cookie", cookieHeader);
156 foreach (var header in request.Headers) {
157 foreach (var value in header.Value) {
158 req.SetHeaderFieldValue (header.Key, value);
162 if (request.Content != null) {
163 foreach (var header in request.Content.Headers) {
164 foreach (var value in header.Value) {
165 req.SetHeaderFieldValue (header.Key, value);
173 protected internal override async Task<HttpResponseMessage> SendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
178 using (var message = CreateWebRequestAsync (request))
180 if (request.Content != null) {
181 var data = await request.Content.ReadAsByteArrayAsync ().ConfigureAwait (false);
182 message.SetBody (data);
185 stream = CFHTTPStream.CreateForHTTPRequest (message);
188 if (useSystemProxy) {
189 var proxies = CF.CFNetwork.GetSystemProxySettings ();
190 if (proxies.HTTPEnable) {
191 stream.SetProxy (proxies);
195 stream.ShouldAutoredirect = allowAutoRedirect;
196 stream.HasBytesAvailableEvent += HandleHasBytesAvailableEvent;
197 stream.ErrorEvent += HandleErrorEvent;
198 stream.ClosedEvent += HandleClosedEvent;
200 var response = new TaskCompletionSource<HttpResponseMessage> ();
202 if (cancellationToken.IsCancellationRequested) {
203 response.SetCanceled ();
204 return await response.Task;
207 var bucket = new StreamBucket () {
212 streamBuckets.Add (stream.Handle, bucket);
215 // Always schedule stream events handling on main-loop. Due to ConfigureAwait (false) we may end up
216 // on any thread-pool thread which may not have run-loop running
219 stream.EnableEvents (CF.CFRunLoop.Main, CF.CFRunLoop.ModeCommon);
221 stream.EnableEvents (CF.CFRunLoop.Main, CF.CFRunLoop.CFRunLoopCommonModes);
226 bucket.CancellationTokenRegistration = cancellationToken.Register (() => {
227 StreamBucket bucket2;
228 if (!streamBuckets.TryGetValue (stream.Handle, out bucket2))
231 bucket2.Response.TrySetCanceled ();
232 CloseStream (stream);
235 return await response.Task;
238 void HandleErrorEvent (object sender, CFStream.StreamEventArgs e)
240 var stream = (CFHTTPStream)sender;
243 if (!streamBuckets.TryGetValue (stream.Handle, out bucket))
246 bucket.Response.TrySetException (stream.GetError ());
247 CloseStream (stream);
250 void HandleClosedEvent (object sender, CFStream.StreamEventArgs e)
252 var stream = (CFHTTPStream)sender;
253 CloseStream (stream);
256 void CloseStream (CFHTTPStream stream)
259 if (streamBuckets.TryGetValue (stream.Handle, out bucket)) {
261 streamBuckets.Remove (stream.Handle);
267 void HandleHasBytesAvailableEvent (object sender, CFStream.StreamEventArgs e)
269 var stream = (CFHTTPStream) sender;
272 if (!streamBuckets.TryGetValue (stream.Handle, out bucket))
275 if (bucket.Response.Task.IsCompleted) {
276 bucket.ContentStream.ReadStreamData ();
280 var header = stream.GetResponseHeader ();
283 if (!header.IsHeaderComplete)
284 throw new NotImplementedException ();
286 bucket.ContentStream = new CFContentStream (stream);
288 var response_msg = new HttpResponseMessage (header.ResponseStatusCode);
289 response_msg.RequestMessage = bucket.Request;
290 response_msg.ReasonPhrase = header.ResponseStatusLine;
291 response_msg.Content = bucket.ContentStream;
293 var fields = header.GetAllHeaderFields ();
294 if (fields != null) {
295 foreach (var entry in fields) {
296 if (entry.Key == null)
299 var key = entry.Key.ToString ();
300 var value = entry.Value == null ? string.Empty : entry.Value.ToString ();
301 HttpHeaders item_headers;
302 if (HttpHeaders.GetKnownHeaderKind (key) == Headers.HttpHeaderKind.Content) {
303 item_headers = response_msg.Content.Headers;
305 item_headers = response_msg.Headers;
307 if (cookies != null && (key == "Set-Cookie" || key == "Set-Cookie2"))
308 AddCookie (value, bucket.Request.RequestUri, key);
311 item_headers.TryAddWithoutValidation (key, value);
315 bucket.Response.TrySetResult (response_msg);
317 bucket.ContentStream.ReadStreamData ();
320 void AddCookie (string value, Uri uri, string header)
322 CookieCollection cookies1 = null;
324 cookies1 = cookies.CookieCutter (uri, header, value, false);
328 if (cookies1 != null && cookies1.Count != 0)
329 cookies.Add (cookies1);