2 * Copyright (c) 2009-2010 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
25 * asynchttp.c - asynchronous http get/post engine.
28 #include "asynchttp.h"
30 #include <CoreFoundation/CFNumber.h>
31 #include <CoreFoundation/CFStream.h>
32 #include <CFNetwork/CFProxySupport.h>
33 #include <Security/SecInternal.h>
34 #include "SecBase64.h"
35 #include <AssertMacros.h>
36 #include <utilities/debugging.h>
37 #include <utilities/SecDispatchRelease.h>
46 #define PRIstatus "ld"
49 #define ocspdErrorLog(args...) asl_log(NULL, NULL, ASL_LEVEL_ERR, ## args)
51 /* POST method has Content-Type header line equal to
52 "application/ocsp-request" */
53 static CFStringRef kContentType
= CFSTR("Content-Type");
54 static CFStringRef kAppOcspRequest
= CFSTR("application/ocsp-request");
56 /* SPI to specify timeout on CFReadStream */
57 #define _kCFStreamPropertyReadTimeout CFSTR("_kCFStreamPropertyReadTimeout")
58 #define _kCFStreamPropertyWriteTimeout CFSTR("_kCFStreamPropertyWriteTimeout")
60 /* The timeout we set - 7 seconds */
61 #define STREAM_TIMEOUT (7 * NSEC_PER_SEC)
63 #define POST_BUFSIZE 2048
65 /* There has got to be an easier way to do this. For now we based this code
66 on CFNetwork/Connection/URLResponse.cpp. */
67 static CFStringRef
copyParseMaxAge(CFStringRef cacheControlHeader
) {
68 /* The format of the cache control header is a comma-separated list, but
69 each list element could be a key-value pair, with the value quoted and
70 possibly containing a comma. */
71 CFStringInlineBuffer inlineBuf
;
72 CFRange componentRange
;
73 CFIndex length
= CFStringGetLength(cacheControlHeader
);
75 CFCharacterSetRef whitespaceSet
= CFCharacterSetGetPredefined(kCFCharacterSetWhitespace
);
76 CFStringRef maxAgeValue
= NULL
;
78 CFStringInitInlineBuffer(cacheControlHeader
, &inlineBuf
, CFRangeMake(0, length
));
79 componentRange
.location
= 0;
82 bool inQuotes
= false;
83 bool foundComponentStart
= false;
84 CFIndex charIndex
= componentRange
.location
;
85 CFIndex componentEnd
= -1;
87 componentRange
.length
= 0;
89 while (charIndex
< length
) {
90 UniChar ch
= CFStringGetCharacterFromInlineBuffer(&inlineBuf
, charIndex
);
91 if (!inQuotes
&& ch
== ',') {
92 componentRange
.length
= charIndex
- componentRange
.location
;
95 if (!CFCharacterSetIsCharacterMember(whitespaceSet
, ch
)) {
96 if (!foundComponentStart
) {
97 foundComponentStart
= true;
98 componentRange
.location
= charIndex
;
100 componentEnd
= charIndex
;
103 inQuotes
= (inQuotes
== false);
109 if (componentEnd
== -1) {
110 componentRange
.length
= charIndex
- componentRange
.location
;
112 componentRange
.length
= componentEnd
- componentRange
.location
+ 1;
115 if (charIndex
== length
) {
116 /* Fell off the end; this is the last component. */
120 /* componentRange should now contain the range of the current
121 component; trimmed of any whitespace. */
123 /* We want to look for a max-age value. */
124 if (!maxAgeValue
&& CFStringFindWithOptions(cacheControlHeader
, CFSTR("max-age"), componentRange
, kCFCompareCaseInsensitive
| kCFCompareAnchored
, &maxAgeRg
)) {
126 CFIndex maxCompRg
= componentRange
.location
+ componentRange
.length
;
127 for (equalIdx
= maxAgeRg
.location
+ maxAgeRg
.length
; equalIdx
< maxCompRg
; equalIdx
++) {
128 UniChar equalCh
= CFStringGetCharacterFromInlineBuffer(&inlineBuf
, equalIdx
);
129 if (equalCh
== '=') {
130 // Parse out max-age value
132 while (equalIdx
< maxCompRg
&& CFCharacterSetIsCharacterMember(whitespaceSet
, CFStringGetCharacterAtIndex(cacheControlHeader
, equalIdx
))) {
135 if (equalIdx
< maxCompRg
) {
136 maxAgeValue
= CFStringCreateWithSubstring(kCFAllocatorDefault
, cacheControlHeader
, CFRangeMake(equalIdx
, maxCompRg
-equalIdx
));
138 } else if (!CFCharacterSetIsCharacterMember(whitespaceSet
, equalCh
)) {
139 // Not a valid max-age header; break out doing nothing
145 if (!done
&& maxAgeValue
) {
149 /* Advance to the next component; + 1 to get past the comma. */
150 componentRange
.location
= charIndex
+ 1;
157 static void asynchttp_complete(asynchttp_t
*http
) {
158 secdebug("http", "http: %p", http
);
159 /* Shutdown streams and timer, we're about to invoke our client callback. */
161 CFReadStreamSetClient(http
->stream
, kCFStreamEventNone
, NULL
, NULL
);
162 CFReadStreamSetDispatchQueue(http
->stream
, NULL
);
163 CFReadStreamClose(http
->stream
);
164 CFReleaseNull(http
->stream
);
167 dispatch_source_cancel(http
->timer
);
168 dispatch_release_null(http
->timer
);
171 if (http
->completed
) {
172 /* This should probably move to our clients. */
173 CFTimeInterval maxAge
= NULL_TIME
;
174 if (http
->response
) {
175 CFStringRef cacheControl
= CFHTTPMessageCopyHeaderFieldValue(
176 http
->response
, CFSTR("cache-control"));
178 CFStringRef maxAgeValue
= copyParseMaxAge(cacheControl
);
179 CFRelease(cacheControl
);
181 secdebug("http", "http header max-age: %@", maxAgeValue
);
182 maxAge
= CFStringGetDoubleValue(maxAgeValue
);
183 CFRelease(maxAgeValue
);
187 http
->completed(http
, maxAge
);
191 static void handle_server_response(CFReadStreamRef stream
,
192 CFStreamEventType type
, void *info
) {
193 asynchttp_t
*http
= (asynchttp_t
*)info
;
195 secerror("Avoiding crash due to CFReadStream invoking us after we called CFReadStreamSetDispatchQueue(stream, NULL) on a different block on our serial queue");
200 case kCFStreamEventHasBytesAvailable
:
202 UInt8 buffer
[POST_BUFSIZE
];
206 length
= CFReadStreamRead(stream
, buffer
, sizeof(buffer
));
208 const UInt8
*buffer
= CFReadStreamGetBuffer(stream
, -1, &length
);
211 "stream: %@ kCFStreamEventHasBytesAvailable read: %lu bytes",
214 /* Negative length == error */
215 asynchttp_complete(http
);
217 } else if (length
> 0) {
218 //CFHTTPMessageAppendBytes(http->response, buffer, length);
219 CFDataAppendBytes(http
->data
, buffer
, length
);
221 /* Read 0 bytes, are we are done or do we wait for
222 kCFStreamEventEndEncountered? */
223 asynchttp_complete(http
);
226 } while (CFReadStreamHasBytesAvailable(stream
));
229 case kCFStreamEventErrorOccurred
:
231 CFStreamError error
= CFReadStreamGetError(stream
);
234 "stream: %@ kCFStreamEventErrorOccurred domain: %ld error: %ld",
235 stream
, error
.domain
, (long) error
.error
);
237 if (error
.domain
== kCFStreamErrorDomainPOSIX
) {
238 ocspdErrorLog("CFReadStream posix: %s", strerror(error
.error
));
239 } else if (error
.domain
== kCFStreamErrorDomainMacOSStatus
) {
240 ocspdErrorLog("CFReadStream osstatus: %"PRIstatus
, error
.error
);
242 ocspdErrorLog("CFReadStream domain: %ld error: %"PRIstatus
,
243 error
.domain
, error
.error
);
245 asynchttp_complete(http
);
248 case kCFStreamEventEndEncountered
:
250 http
->response
= (CFHTTPMessageRef
)CFReadStreamCopyProperty(
251 stream
, kCFStreamPropertyHTTPResponseHeader
);
252 secdebug("http", "stream: %@ kCFStreamEventEndEncountered hdr: %@",
253 stream
, http
->response
);
254 CFHTTPMessageSetBody(http
->response
, http
->data
);
255 asynchttp_complete(http
);
259 ocspdErrorLog("handle_server_response unexpected event type: %lu",
265 /* Create a URI suitable for use in an http GET request, will return NULL if
266 the length would exceed 255 bytes. */
267 static CFURLRef
createGetURL(CFURLRef responder
, CFDataRef request
) {
268 CFURLRef getURL
= NULL
;
269 CFMutableDataRef base64Request
= NULL
;
270 CFStringRef base64RequestString
= NULL
;
271 CFStringRef peRequest
= NULL
;
274 base64Len
= SecBase64Encode(NULL
, CFDataGetLength(request
), NULL
, 0);
275 /* Don't bother doing all the work below if we know the end result will
276 exceed 255 bytes (minus one for the '/' separator makes 254). */
277 if (base64Len
+ CFURLGetBytes(responder
, NULL
, 0) > 254)
280 require(base64Request
= CFDataCreateMutable(kCFAllocatorDefault
,
282 CFDataSetLength(base64Request
, base64Len
);
283 SecBase64Encode(CFDataGetBytePtr(request
), CFDataGetLength(request
),
284 (char *)CFDataGetMutableBytePtr(base64Request
), base64Len
);
285 require(base64RequestString
= CFStringCreateWithBytes(kCFAllocatorDefault
,
286 CFDataGetBytePtr(base64Request
), base64Len
, kCFStringEncodingUTF8
,
288 require(peRequest
= CFURLCreateStringByAddingPercentEscapes(
289 kCFAllocatorDefault
, base64RequestString
, NULL
, CFSTR("+/="),
290 kCFStringEncodingUTF8
), errOut
);
292 CFStringRef urlString
= CFURLGetString(responder
);
294 if (CFStringHasSuffix(urlString
, CFSTR("/"))) {
295 fullURL
= CFStringCreateWithFormat(kCFAllocatorDefault
, NULL
,
296 CFSTR("%@%@"), urlString
, peRequest
);
298 fullURL
= CFStringCreateWithFormat(kCFAllocatorDefault
, NULL
,
299 CFSTR("%@/%@"), urlString
, peRequest
);
301 getURL
= CFURLCreateWithString(kCFAllocatorDefault
, fullURL
, NULL
);
304 getURL
= CFURLCreateWithString(kCFAllocatorDefault
, peRequest
, responder
);
308 CFReleaseSafe(base64Request
);
309 CFReleaseSafe(base64RequestString
);
310 CFReleaseSafe(peRequest
);
315 bool asyncHttpPost(CFURLRef responder
, CFDataRef requestData
/* , bool force_nocache */ ,
317 bool result
= true; /* True, we didn't schedule any work. */
318 /* resources to release on exit */
319 CFURLRef getURL
= NULL
;
321 /* Interesting tidbit from rfc5019
322 When sending requests that are less than or equal to 255 bytes in
323 total (after encoding) including the scheme and delimiters (http://),
324 server name and base64-encoded OCSPRequest structure, clients MUST
325 use the GET method (to enable OCSP response caching). OCSP requests
326 larger than 255 bytes SHOULD be submitted using the POST method.
328 Interesting tidbit from rfc2616:
329 Note: Servers ought to be cautious about depending on URI lengths
330 above 255 bytes, because some older client or proxy
331 implementations might not properly support these lengths.
333 Given the second note I'm assuming that the note in rfc5019 is about the
334 length of the URI, not the length of the entire HTTP request.
336 If we need to consider the entire request we need to have 17 bytes less, or
337 17 + 25 = 42 if we are appending a "Cache-Control: no-cache CRLF" header
340 The 17 and 42 above are based on the request encoding from rfc2616
341 Method SP Request-URI SP HTTP-Version CRLF (header CRLF)* CRLF
343 GET SP URI SP HTTP/1.1 CRLF CRLF
346 GET SP URI SP HTTP/1.1 CRLF Cache-Control: SP no-cache CRLF CRLF
350 /* First let's try creating a GET request. */
351 getURL
= createGetURL(responder
, requestData
);
352 if (getURL
&& CFURLGetBytes(getURL
, NULL
, 0) < 256) {
353 /* Get URI is less than 256 bytes encoded, making it safe even for
354 older proxy or caching servers, so let's use HTTP GET. */
355 secdebug("http", "GET[%ld] %@", CFURLGetBytes(getURL
, NULL
, 0), getURL
);
356 require_quiet(http
->request
= CFHTTPMessageCreateRequest(kCFAllocatorDefault
,
357 CFSTR("GET"), getURL
, kCFHTTPVersion1_1
), errOut
);
359 /* GET Request too big to ensure error free transmission, let's
360 create a HTTP POST http->request instead. */
361 secdebug("http", "POST %@ CRLF body", responder
);
362 require_quiet(http
->request
= CFHTTPMessageCreateRequest(kCFAllocatorDefault
,
363 CFSTR("POST"), responder
, kCFHTTPVersion1_1
), errOut
);
364 /* Set the body and required header fields. */
365 CFHTTPMessageSetBody(http
->request
, requestData
);
366 CFHTTPMessageSetHeaderFieldValue(http
->request
, kContentType
,
372 CFHTTPMessageSetHeaderFieldValue(http
->request
, CFSTR("Cache-Control"),
377 result
= asynchttp_request(NULL
, http
);
380 CFReleaseSafe(getURL
);
386 static void asynchttp_timer_proc(asynchttp_t
*http
) {
387 CFStringRef req_meth
= http
->request
? CFHTTPMessageCopyRequestMethod(http
->request
) : NULL
;
388 CFURLRef req_url
= http
->request
? CFHTTPMessageCopyRequestURL(http
->request
) : NULL
;
389 secnotice("http", "Timeout during %@ %@.", req_meth
, req_url
);
390 CFReleaseSafe(req_url
);
391 CFReleaseSafe(req_meth
);
392 asynchttp_complete(http
);
396 void asynchttp_free(asynchttp_t
*http
) {
398 CFReleaseNull(http
->request
);
399 CFReleaseNull(http
->response
);
400 CFReleaseNull(http
->data
);
401 CFReleaseNull(http
->stream
);
402 dispatch_release_null(http
->timer
);
406 /* Return true, iff we didn't schedule any work, return false if we did. */
407 bool asynchttp_request(CFHTTPMessageRef request
, asynchttp_t
*http
) {
408 secdebug("http", "request %@", request
);
410 http
->request
= request
;
414 /* Create the stream for the request. */
415 require_quiet(http
->stream
= CFReadStreamCreateForHTTPRequest(
416 kCFAllocatorDefault
, http
->request
), errOut
);
418 /* Set a reasonable timeout */
419 require_quiet(http
->timer
= dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER
, 0, 0, http
->queue
), errOut
);
420 dispatch_source_set_event_handler(http
->timer
, ^{
421 asynchttp_timer_proc(http
);
423 // Set the timer's fire time to now + STREAM_TIMEOUT seconds with a .5 second fuzz factor.
424 dispatch_source_set_timer(http
->timer
, dispatch_time(DISPATCH_TIME_NOW
, STREAM_TIMEOUT
),
425 DISPATCH_TIME_FOREVER
, (int64_t)(500 * NSEC_PER_MSEC
));
426 dispatch_resume(http
->timer
);
428 /* Set up possible proxy info */
429 CFDictionaryRef proxyDict
= CFNetworkCopySystemProxySettings();
431 CFReadStreamSetProperty(http
->stream
, kCFStreamPropertyHTTPProxy
, proxyDict
);
432 CFRelease(proxyDict
);
435 http
->data
= CFDataCreateMutable(kCFAllocatorDefault
, 0);
437 CFStreamClientContext stream_context
= { .info
= http
};
438 CFReadStreamSetClient(http
->stream
,
439 (kCFStreamEventHasBytesAvailable
440 | kCFStreamEventErrorOccurred
441 | kCFStreamEventEndEncountered
),
442 handle_server_response
, &stream_context
);
443 CFReadStreamSetDispatchQueue(http
->stream
, http
->queue
);
444 CFReadStreamOpen(http
->stream
);
446 return false; /* false -> something was scheduled. */
449 /* Deschedule timer and free anything we might have retained so far. */
450 asynchttp_free(http
);