1/*
2 * Copyright (c) 2009-2010,2012-2014 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24/*
25 * asynchttp.c - asynchronous http get/post engine.
26 */
27
28#include "asynchttp.h"
29
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>
38#include <asl.h>
39#include <string.h>
40
41#include <inttypes.h>
42
43#if __LP64__
44#define PRIstatus "d"
45#else
46#define PRIstatus "ld"
47#endif
48
49#define ocspdErrorLog(args...)     asl_log(NULL, NULL, ASL_LEVEL_ERR, ## args)
50
51/* POST method has Content-Type header line equal to
52   "application/ocsp-request" */
53static CFStringRef kContentType		= CFSTR("Content-Type");
54static CFStringRef kAppOcspRequest	= CFSTR("application/ocsp-request");
55
56/* SPI to specify timeout on CFReadStream */
57#define _kCFStreamPropertyReadTimeout   CFSTR("_kCFStreamPropertyReadTimeout")
58#define _kCFStreamPropertyWriteTimeout   CFSTR("_kCFStreamPropertyWriteTimeout")
59
60/* The timeout we set - 7 seconds */
61#define STREAM_TIMEOUT		(7 * NSEC_PER_SEC)
62
63#define POST_BUFSIZE   2048
64
65/* There has got to be an easier way to do this.  For now we based this code
66   on CFNetwork/Connection/URLResponse.cpp. */
67static 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);
74    bool done = false;
75    CFCharacterSetRef whitespaceSet = CFCharacterSetGetPredefined(kCFCharacterSetWhitespace);
76    CFStringRef maxAgeValue = NULL;
77
78    CFStringInitInlineBuffer(cacheControlHeader, &inlineBuf, CFRangeMake(0, length));
79    componentRange.location = 0;
80
81    while (!done) {
82        bool inQuotes = false;
83        bool foundComponentStart = false;
84        CFIndex charIndex = componentRange.location;
85        CFIndex componentEnd = -1;
86        CFRange maxAgeRg;
87        componentRange.length = 0;
88
89        while (charIndex < length) {
90            UniChar ch = CFStringGetCharacterFromInlineBuffer(&inlineBuf, charIndex);
91            if (!inQuotes && ch == ',') {
92                componentRange.length = charIndex - componentRange.location;
93                break;
94            }
95            if (!CFCharacterSetIsCharacterMember(whitespaceSet, ch)) {
96                if (!foundComponentStart) {
97                    foundComponentStart = true;
98                    componentRange.location = charIndex;
99                } else {
100                    componentEnd = charIndex;
101                }
102                if (ch == '\"') {
103                    inQuotes = (inQuotes == false);
104                }
105            }
106            charIndex ++;
107        }
108
109        if (componentEnd == -1) {
110            componentRange.length = charIndex - componentRange.location;
111        } else {
112            componentRange.length = componentEnd - componentRange.location + 1;
113        }
114
115        if (charIndex == length) {
116            /* Fell off the end; this is the last component. */
117            done = true;
118        }
119
120        /* componentRange should now contain the range of the current
121           component; trimmed of any whitespace. */
122
123        /* We want to look for a max-age value. */
124        if (!maxAgeValue && CFStringFindWithOptions(cacheControlHeader, CFSTR("max-age"), componentRange, kCFCompareCaseInsensitive | kCFCompareAnchored, &maxAgeRg)) {
125            CFIndex equalIdx;
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
131                    equalIdx ++;
132                    while (equalIdx < maxCompRg && CFCharacterSetIsCharacterMember(whitespaceSet, CFStringGetCharacterAtIndex(cacheControlHeader, equalIdx))) {
133                        equalIdx ++;
134                    }
135                    if (equalIdx < maxCompRg) {
136                        CFReleaseNull(maxAgeValue);
137                        maxAgeValue = CFStringCreateWithSubstring(kCFAllocatorDefault, cacheControlHeader, CFRangeMake(equalIdx, maxCompRg-equalIdx));
138                    }
139                } else if (!CFCharacterSetIsCharacterMember(whitespaceSet, equalCh)) {
140                    // Not a valid max-age header; break out doing nothing
141                    break;
142                }
143            }
144        }
145
146        if (!done && maxAgeValue) {
147            done = true;
148        }
149        if (!done) {
150            /* Advance to the next component; + 1 to get past the comma. */
151            componentRange.location = charIndex + 1;
152        }
153    }
154
155    return maxAgeValue;
156}
157
158static void asynchttp_complete(asynchttp_t *http) {
159    secdebug("http", "http: %p", http);
160    /* Shutdown streams and timer, we're about to invoke our client callback. */
161    if (http->stream) {
162        CFReadStreamSetClient(http->stream, kCFStreamEventNone, NULL, NULL);
163        CFReadStreamSetDispatchQueue(http->stream, NULL);
164        CFReadStreamClose(http->stream);
165        CFReleaseNull(http->stream);
166    }
167    if (http->timer) {
168        dispatch_source_cancel(http->timer);
169        dispatch_release_null(http->timer);
170    }
171
172    if (http->completed) {
173        /* This should probably move to our clients. */
174        CFTimeInterval maxAge = NULL_TIME;
175        if (http->response) {
176            CFStringRef cacheControl = CFHTTPMessageCopyHeaderFieldValue(
177                http->response, CFSTR("cache-control"));
178            if (cacheControl) {
179                CFStringRef maxAgeValue = copyParseMaxAge(cacheControl);
180                CFRelease(cacheControl);
181                if (maxAgeValue) {
182                    secdebug("http", "http header max-age: %@", maxAgeValue);
183                    maxAge = CFStringGetDoubleValue(maxAgeValue);
184                    CFRelease(maxAgeValue);
185                }
186            }
187        }
188        http->completed(http, maxAge);
189    }
190}
191
192static void handle_server_response(CFReadStreamRef stream,
193    CFStreamEventType type, void *info) {
194    asynchttp_t *http = (asynchttp_t *)info;
195    if (!http->stream) {
196        secerror("Avoiding crash due to CFReadStream invoking us after we called CFReadStreamSetDispatchQueue(stream, NULL) on a different block on our serial queue");
197        return;
198    }
199
200    switch (type) {
201    case kCFStreamEventHasBytesAvailable:
202    {
203        UInt8 buffer[POST_BUFSIZE];
204        CFIndex length;
205        do {
206#if 1
207            length = CFReadStreamRead(stream, buffer, sizeof(buffer));
208#else
209            const UInt8 *buffer = CFReadStreamGetBuffer(stream, -1, &length);
210#endif
211            secdebug("http",
212                "stream: %@ kCFStreamEventHasBytesAvailable read: %lu bytes",
213                stream, length);
214            if (length < 0) {
215                /* Negative length == error */
216                asynchttp_complete(http);
217                break;
218            } else if (length > 0) {
219                //CFHTTPMessageAppendBytes(http->response, buffer, length);
220                CFDataAppendBytes(http->data, buffer, length);
221            } else {
222                /* Read 0 bytes. This is a no-op, but we need to keep
223                   reading until CFReadStreamHasBytesAvailable is false.
224                 */
225            }
226        } while (CFReadStreamHasBytesAvailable(stream));
227        break;
228    }
229    case kCFStreamEventErrorOccurred:
230    {
231        CFStreamError error = CFReadStreamGetError(stream);
232
233        secdebug("http",
234            "stream: %@ kCFStreamEventErrorOccurred domain: %ld error: %ld",
235            stream, error.domain, (long) error.error);
236
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);
241        } else {
242            ocspdErrorLog("CFReadStream domain: %ld error: %"PRIstatus,
243                error.domain, error.error);
244        }
245        asynchttp_complete(http);
246        break;
247    }
248    case kCFStreamEventEndEncountered:
249    {
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);
256        break;
257    }
258    default:
259        ocspdErrorLog("handle_server_response unexpected event type: %lu",
260            type);
261        break;
262    }
263}
264
265/* Create a URI suitable for use in an http GET request, will return NULL if
266   the length would exceed 255 bytes. */
267static CFURLRef createGetURL(CFURLRef responder, CFDataRef request) {
268    CFURLRef getURL = NULL;
269    CFMutableDataRef base64Request = NULL;
270    CFStringRef base64RequestString = NULL;
271    CFStringRef peRequest = NULL;
272    CFIndex base64Len;
273
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)
278        return NULL;
279
280    require(base64Request = CFDataCreateMutable(kCFAllocatorDefault,
281        base64Len), errOut);
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,
287        false), errOut);
288    /* percent-encode all reserved characters from RFC 3986 [2.2] */
289    require(peRequest = CFURLCreateStringByAddingPercentEscapes(
290        kCFAllocatorDefault, base64RequestString, NULL,
291        CFSTR(":/?#[]@!$&'()*+,;="), kCFStringEncodingUTF8), errOut);
292#if 1
293    CFStringRef urlString = CFURLGetString(responder);
294    CFStringRef fullURL;
295    if (CFStringHasSuffix(urlString, CFSTR("/"))) {
296        fullURL = CFStringCreateWithFormat(kCFAllocatorDefault, NULL,
297            CFSTR("%@%@"), urlString, peRequest);
298    } else {
299        fullURL = CFStringCreateWithFormat(kCFAllocatorDefault, NULL,
300            CFSTR("%@/%@"), urlString, peRequest);
301    }
302    getURL = CFURLCreateWithString(kCFAllocatorDefault, fullURL, NULL);
303    CFRelease(fullURL);
304#else
305    getURL = CFURLCreateWithString(kCFAllocatorDefault, peRequest, responder);
306#endif
307
308errOut:
309    CFReleaseSafe(base64Request);
310    CFReleaseSafe(base64RequestString);
311    CFReleaseSafe(peRequest);
312
313    return getURL;
314}
315
316bool asyncHttpPost(CFURLRef responder, CFDataRef requestData /* , bool force_nocache */ ,
317    asynchttp_t *http) {
318    bool result = true; /* True, we didn't schedule any work. */
319	/* resources to release on exit */
320    CFURLRef getURL = NULL;
321
322/* Interesting tidbit from rfc5019
323   When sending requests that are less than or equal to 255 bytes in
324   total (after encoding) including the scheme and delimiters (http://),
325   server name and base64-encoded OCSPRequest structure, clients MUST
326   use the GET method (to enable OCSP response caching).  OCSP requests
327   larger than 255 bytes SHOULD be submitted using the POST method.
328
329   Interesting tidbit from rfc2616:
330   Note: Servers ought to be cautious about depending on URI lengths
331   above 255 bytes, because some older client or proxy
332   implementations might not properly support these lengths.
333
334   Given the second note I'm assuming that the note in rfc5019 is about the
335   length of the URI, not the length of the entire HTTP request.
336
337   If we need to consider the entire request we need to have 17 bytes less, or
338   17 + 25 = 42 if we are appending a "Cache-Control: no-cache CRLF" header
339   field.
340
341   The 17 and 42 above are based on the request encoding from rfc2616
342   Method SP Request-URI SP HTTP-Version CRLF (header CRLF)* CRLF
343   so in our case it's:
344   GET SP URI SP HTTP/1.1 CRLF CRLF
345   17 + len(URI) bytes
346   or
347   GET SP URI SP HTTP/1.1 CRLF Cache-Control: SP no-cache CRLF CRLF
348   42 + len(URI) bytes
349 */
350
351    /* First let's try creating a GET request. */
352    getURL = createGetURL(responder, requestData);
353    if (getURL && CFURLGetBytes(getURL, NULL, 0) < 256) {
354        /* Get URI is less than 256 bytes encoded, making it safe even for
355           older proxy or caching servers, so let's use HTTP GET. */
356        secdebug("http", "GET[%ld] %@", CFURLGetBytes(getURL, NULL, 0), getURL);
357        require_quiet(http->request = CFHTTPMessageCreateRequest(kCFAllocatorDefault,
358            CFSTR("GET"), getURL, kCFHTTPVersion1_1), errOut);
359    } else {
360        /* GET Request too big to ensure error free transmission, let's
361           create a HTTP POST http->request instead. */
362        secdebug("http", "POST %@ CRLF body", responder);
363        require_quiet(http->request = CFHTTPMessageCreateRequest(kCFAllocatorDefault,
364            CFSTR("POST"), responder, kCFHTTPVersion1_1), errOut);
365        /* Set the body and required header fields. */
366        CFHTTPMessageSetBody(http->request, requestData);
367        CFHTTPMessageSetHeaderFieldValue(http->request, kContentType,
368            kAppOcspRequest);
369    }
370
371#if 0
372    if (force_nocache) {
373        CFHTTPMessageSetHeaderFieldValue(http->request, CFSTR("Cache-Control"),
374            CFSTR("no-cache"));
375    }
376#endif
377
378    result = asynchttp_request(NULL, http);
379
380errOut:
381    CFReleaseSafe(getURL);
382
383    return result;
384}
385
386
387static void asynchttp_timer_proc(asynchttp_t *http CF_CONSUMED) {
388    CFStringRef req_meth = http->request ? CFHTTPMessageCopyRequestMethod(http->request) : NULL;
389    CFURLRef req_url = http->request ? CFHTTPMessageCopyRequestURL(http->request) : NULL;
390    secnotice("http", "Timeout during %@ %@.", req_meth, req_url);
391    CFReleaseSafe(req_url);
392    CFReleaseSafe(req_meth);
393    asynchttp_complete(http);
394}
395
396
397void asynchttp_free(asynchttp_t *http) {
398    if (http) {
399        CFReleaseNull(http->request);
400        CFReleaseNull(http->response);
401        CFReleaseNull(http->data);
402        CFReleaseNull(http->stream);
403        dispatch_release_null(http->timer);
404    }
405}
406
407/* Return true, iff we didn't schedule any work, return false if we did. */
408bool asynchttp_request(CFHTTPMessageRef request, asynchttp_t *http) {
409    secdebug("http", "request %@", request);
410    if (request) {
411        http->request = request;
412        CFRetain(request);
413    }
414
415    /* Create the stream for the request. */
416    require_quiet(http->stream = CFReadStreamCreateForHTTPRequest(
417        kCFAllocatorDefault, http->request), errOut);
418
419	/* Set a reasonable timeout */
420    require_quiet(http->timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, http->queue), errOut);
421    dispatch_source_set_event_handler(http->timer, ^{
422        asynchttp_timer_proc(http);
423    });
424    // Set the timer's fire time to now + STREAM_TIMEOUT seconds with a .5 second fuzz factor.
425    dispatch_source_set_timer(http->timer, dispatch_time(DISPATCH_TIME_NOW, STREAM_TIMEOUT),
426                              DISPATCH_TIME_FOREVER, (int64_t)(500 * NSEC_PER_MSEC));
427    dispatch_resume(http->timer);
428
429	/* Set up possible proxy info */
430	CFDictionaryRef proxyDict = CFNetworkCopySystemProxySettings();
431	if (proxyDict) {
432		CFReadStreamSetProperty(http->stream, kCFStreamPropertyHTTPProxy, proxyDict);
433        CFRelease(proxyDict);
434    }
435
436    http->data = CFDataCreateMutable(kCFAllocatorDefault, 0);
437
438    CFStreamClientContext stream_context = { .info = http };
439    CFReadStreamSetClient(http->stream,
440        (kCFStreamEventHasBytesAvailable
441         | kCFStreamEventErrorOccurred
442         | kCFStreamEventEndEncountered),
443        handle_server_response, &stream_context);
444    CFReadStreamSetDispatchQueue(http->stream, http->queue);
445    CFReadStreamOpen(http->stream);
446
447    return false; /* false -> something was scheduled. */
448
449errOut:
450    /* Deschedule timer and free anything we might have retained so far. */
451    asynchttp_free(http);
452    return true;
453}
454