2
// MonoMac.CFNetwork.MessageHandler
5
// Martin Baulig (martin.baulig@gmail.com)
7
// Copyright 2012 Xamarin Inc. (http://www.xamarin.com)
10
// Permission is hereby granted, free of charge, to any person obtaining
11
// a copy of this software and associated documentation files (the
12
// "Software"), to deal in the Software without restriction, including
13
// without limitation the rights to use, copy, modify, merge, publish,
14
// distribute, sublicense, and/or sell copies of the Software, and to
15
// permit persons to whom the Software is furnished to do so, subject to
16
// the following conditions:
18
// The above copyright notice and this permission notice shall be
19
// included in all copies or substantial portions of the Software.
21
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32
using System.Text.RegularExpressions;
33
using System.Collections.Generic;
34
using System.Threading;
35
using System.Threading.Tasks;
36
using System.Net.Http;
37
using MonoMac.CoreFoundation;
38
using MonoMac.CoreServices;
39
using MonoMac.Foundation;
41
namespace MonoMac.CFNetwork {
43
public class MessageHandler : HttpClientHandler {
44
public MessageHandler ()
48
public MessageHandler (WorkerThread worker)
50
WorkerThread = worker;
53
public WorkerThread WorkerThread {
59
* CFNetwork supports two ways of authentication:
61
* a) You send a normal request to the server and when it responds with
62
* a 401 or 407, then you call CFHTTPMessageAddAuthentication on the
63
* returned response CFHTTPMessage. When the call succeeds, you resend
66
* b) You do the same thing for the first request, but call
67
* CFHTTPAuthenticationCreateFromResponse on the returned response to
68
* get a CFHTTPAuthentication object which can persist multiple requests.
70
* On subsequent requests, you can then resue that object by calling
71
* CFHTTPAuthenticationAppliesToRequest, CFHTTPAuthenticationIsValid and
72
* (if both succeed) CFHTTPMessageApplyCredentials prior to sending the
76
CFHTTPAuthentication auth;
78
#region implemented abstract members of HttpMessageHandler
79
protected override async Task<HttpResponseMessage> SendAsync (HttpRequestMessage request,
80
CancellationToken cancellationToken)
82
if (!request.RequestUri.IsAbsoluteUri)
83
throw new InvalidOperationException ();
85
using (var message = CreateRequest (request)) {
86
var body = await CreateBody (request, message, cancellationToken);
87
return await ProcessRequest (request, message, body, true, cancellationToken);
92
CFHTTPMessage CreateRequest (HttpRequestMessage request)
94
var message = CFHTTPMessage.CreateRequest (
95
request.RequestUri, request.Method.Method, request.Version);
97
SetupRequest (request, message);
99
if ((auth == null) || (Credentials == null) || !PreAuthenticate)
102
if (!auth.AppliesToRequest (message))
105
var method = auth.GetMethod ();
106
var credential = Credentials.GetCredential (request.RequestUri, method);
107
if (credential == null)
110
message.ApplyCredentials (auth, credential);
114
void SetupRequest (HttpRequestMessage request, CFHTTPMessage message)
116
string accept_encoding = null;
117
if ((AutomaticDecompression & DecompressionMethods.GZip) != 0)
118
accept_encoding = "gzip";
119
if ((AutomaticDecompression & DecompressionMethods.Deflate) != 0)
120
accept_encoding = accept_encoding != null ? "gzip, deflate" : "deflate";
121
if (accept_encoding != null)
122
message.SetHeaderFieldValue ("Accept-Encoding", accept_encoding);
124
if (request.Content != null) {
125
foreach (var header in request.Content.Headers) {
126
var value = string.Join (",", header.Value);
127
message.SetHeaderFieldValue (header.Key, value);
131
foreach (var header in request.Headers) {
132
if ((accept_encoding != null) && header.Key.Equals ("Accept-Encoding"))
134
var value = string.Join (",", header.Value);
135
message.SetHeaderFieldValue (header.Key, value);
138
if (UseCookies && (CookieContainer != null)) {
139
string cookieHeader = CookieContainer.GetCookieHeader (request.RequestUri);
140
if (cookieHeader != "")
141
message.SetHeaderFieldValue ("Cookie", cookieHeader);
145
async Task<WebRequestStream> CreateBody (HttpRequestMessage request, CFHTTPMessage message,
146
CancellationToken cancellationToken)
148
if (request.Content == null)
152
* There are two ways of sending the body:
154
* - CFHTTPMessageSetBody() sets the full body contents
155
* We use this by default.
157
* - CFReadStreamCreateForStreamedHTTPRequest() should be used
158
* if the body is too large to fit in memory. It also uses
159
* chunked transfer encoding.
161
* We use this if the user either gave us a StreamContent, or we
162
* don't have any Content-Length, so we'll have to use chunked
166
var length = request.Content.Headers.ContentLength;
167
if ((request.Content is StreamContent) || (length == null)) {
168
var stream = await request.Content.ReadAsStreamAsync ().ConfigureAwait (false);
169
return new WebRequestStream (stream, cancellationToken);
172
var text = await request.Content.ReadAsByteArrayAsync ().ConfigureAwait (false);
173
message.SetBody (text);
177
bool GetKeepAlive (HttpRequestMessage request)
179
if (request.Version != HttpVersion.Version10)
180
return request.Headers.ConnectionClose != true;
182
foreach (var header in request.Headers.Connection) {
183
if (string.Equals (header, "Keep-Alive", StringComparison.OrdinalIgnoreCase))
187
return request.Headers.Contains ("Keep-Alive");
190
async Task<HttpResponseMessage> ProcessRequest (HttpRequestMessage request,
191
CFHTTPMessage message,
192
WebRequestStream body,
193
bool retryWithCredentials,
194
CancellationToken cancellationToken)
196
cancellationToken.ThrowIfCancellationRequested ();
198
WebResponseStream stream;
200
stream = WebResponseStream.Create (message, body);
202
stream = WebResponseStream.Create (message);
204
throw new HttpRequestException (string.Format (
205
"Failed to create web request for '{0}'.",
209
stream.Stream.ShouldAutoredirect = AllowAutoRedirect;
210
stream.Stream.AttemptPersistentConnection = GetKeepAlive (request);
212
var response = await stream.Open (
213
WorkerThread, cancellationToken).ConfigureAwait (false);
215
var status = (HttpStatusCode)response.ResponseStatusCode;
217
if (retryWithCredentials && (body == null) &&
218
(status == HttpStatusCode.Unauthorized) ||
219
(status == HttpStatusCode.ProxyAuthenticationRequired)) {
220
if (HandleAuthentication (request.RequestUri, message, response)) {
222
return await ProcessRequest (
223
request, message, null, false, cancellationToken);
227
// The Content object takes ownership of the stream, so we don't
230
var retval = new HttpResponseMessage ();
231
retval.StatusCode = response.ResponseStatusCode;
232
retval.ReasonPhrase = GetReasonPhrase (response);
233
retval.Version = response.Version;
235
var content = new Content (stream);
236
retval.Content = content;
238
DecodeHeaders (response, retval, content);
242
string GetReasonPhrase (CFHTTPMessage response)
244
var line = response.ResponseStatusLine;
245
var match = Regex.Match (line, "HTTP/1.(0|1) (\\d+) (.*)");
249
return match.Groups [3].Value;
252
bool HandleAuthentication (Uri uri, CFHTTPMessage request, CFHTTPMessage response)
254
if (Credentials == null)
257
if (PreAuthenticate) {
258
FindAuthenticationObject (response);
259
return HandlePreAuthentication (uri, request);
262
var basic = Credentials.GetCredential (uri, "Basic");
263
var digest = Credentials.GetCredential (uri, "Digest");
266
if ((basic != null) && (digest == null))
267
ok = HandleAuthentication (
268
request, response, CFHTTPMessage.AuthenticationScheme.Basic, basic);
269
if ((digest != null) && (basic == null))
270
ok = HandleAuthentication (
271
request, response, CFHTTPMessage.AuthenticationScheme.Digest, digest);
275
FindAuthenticationObject (response);
276
return HandlePreAuthentication (uri, request);
279
bool HandlePreAuthentication (Uri uri, CFHTTPMessage message)
281
var method = auth.GetMethod ();
282
var credential = Credentials.GetCredential (uri, method);
283
if (credential == null)
286
message.ApplyCredentials (auth, credential);
290
bool HandleAuthentication (CFHTTPMessage request, CFHTTPMessage response,
291
CFHTTPMessage.AuthenticationScheme scheme,
292
NetworkCredential credential)
294
bool forProxy = response.ResponseStatusCode == HttpStatusCode.ProxyAuthenticationRequired;
296
return request.AddAuthentication (
297
response, (NSString)credential.UserName, (NSString)credential.Password,
301
void FindAuthenticationObject (CFHTTPMessage response)
311
auth = CFHTTPAuthentication.CreateFromResponse (response);
313
throw new HttpRequestException ("Failed to create CFHTTPAuthentication");
317
throw new HttpRequestException ("Failed to validate CFHTTPAuthentication");
320
void DecodeHeaders (CFHTTPMessage message, HttpResponseMessage response, Content content)
322
using (var dict = message.GetAllHeaderFields ()) {
323
foreach (var entry in dict) {
324
DecodeHeader (response, content, entry);
329
void DecodeHeader (HttpResponseMessage response, Content content,
330
KeyValuePair<NSObject,NSObject> entry)
332
string key = (NSString)entry.Key;
333
string value = (NSString)entry.Value;
336
if (content.DecodeHeader (key, value))
339
response.Headers.Add (key, value);
346
* FIXME: .NET automatically fixes an invalid date header
347
* by setting it to the current time. Mono does not.
349
if (key.Equals ("Date"))
350
response.Headers.Date = DateTime.Now;
353
protected override void Dispose (bool disposing)
362
base.Dispose (disposing);