]> git.saurik.com Git - apple/security.git/blobdiff - Security/sec/securityd/asynchttp.c
Security-57031.1.35.tar.gz
[apple/security.git] / Security / sec / securityd / asynchttp.c
diff --git a/Security/sec/securityd/asynchttp.c b/Security/sec/securityd/asynchttp.c
new file mode 100644 (file)
index 0000000..2a29fd2
--- /dev/null
@@ -0,0 +1,453 @@
+/*
+ * Copyright (c) 2009-2010,2012-2014 Apple Inc. All Rights Reserved.
+ *
+ * @APPLE_LICENSE_HEADER_START@
+ * 
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apple Public Source License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://www.opensource.apple.com/apsl/ and read it before using this
+ * file.
+ * 
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ * 
+ * @APPLE_LICENSE_HEADER_END@
+ */
+
+/*
+ * asynchttp.c - asynchronous http get/post engine.
+ */
+
+#include "asynchttp.h"
+
+#include <CoreFoundation/CFNumber.h>
+#include <CoreFoundation/CFStream.h>
+#include <CFNetwork/CFProxySupport.h>
+#include <Security/SecInternal.h>
+#include "SecBase64.h"
+#include <AssertMacros.h>
+#include <utilities/debugging.h>
+#include <utilities/SecDispatchRelease.h>
+#include <asl.h>
+#include <string.h>
+
+#include <inttypes.h>
+
+#if __LP64__
+#define PRIstatus "d"
+#else
+#define PRIstatus "ld"
+#endif
+
+#define ocspdErrorLog(args...)     asl_log(NULL, NULL, ASL_LEVEL_ERR, ## args)
+
+/* POST method has Content-Type header line equal to
+   "application/ocsp-request" */
+static CFStringRef kContentType                = CFSTR("Content-Type");
+static CFStringRef kAppOcspRequest     = CFSTR("application/ocsp-request");
+
+/* SPI to specify timeout on CFReadStream */
+#define _kCFStreamPropertyReadTimeout   CFSTR("_kCFStreamPropertyReadTimeout")
+#define _kCFStreamPropertyWriteTimeout   CFSTR("_kCFStreamPropertyWriteTimeout")
+
+/* The timeout we set - 7 seconds */
+#define STREAM_TIMEOUT         (7 * NSEC_PER_SEC)
+
+#define POST_BUFSIZE   2048
+
+/* There has got to be an easier way to do this.  For now we based this code
+   on CFNetwork/Connection/URLResponse.cpp. */
+static CFStringRef copyParseMaxAge(CFStringRef cacheControlHeader) {
+    /* The format of the cache control header is a comma-separated list, but
+       each list element could be a key-value pair, with the value quoted and
+       possibly containing a comma. */
+    CFStringInlineBuffer inlineBuf;
+    CFRange componentRange;
+    CFIndex length = CFStringGetLength(cacheControlHeader);
+    bool done = false;
+    CFCharacterSetRef whitespaceSet = CFCharacterSetGetPredefined(kCFCharacterSetWhitespace);
+    CFStringRef maxAgeValue = NULL;
+
+    CFStringInitInlineBuffer(cacheControlHeader, &inlineBuf, CFRangeMake(0, length));
+    componentRange.location = 0;
+
+    while (!done) {
+        bool inQuotes = false;
+        bool foundComponentStart = false;
+        CFIndex charIndex = componentRange.location;
+        CFIndex componentEnd = -1;
+        CFRange maxAgeRg;
+        componentRange.length = 0;
+
+        while (charIndex < length) {
+            UniChar ch = CFStringGetCharacterFromInlineBuffer(&inlineBuf, charIndex);
+            if (!inQuotes && ch == ',') {
+                componentRange.length = charIndex - componentRange.location;
+                break;
+            }
+            if (!CFCharacterSetIsCharacterMember(whitespaceSet, ch)) {
+                if (!foundComponentStart) {
+                    foundComponentStart = true;
+                    componentRange.location = charIndex;
+                } else {
+                    componentEnd = charIndex;
+                }
+                if (ch == '\"') {
+                    inQuotes = (inQuotes == false);
+                }
+            }
+            charIndex ++;
+        }
+
+        if (componentEnd == -1) {
+            componentRange.length = charIndex - componentRange.location;
+        } else {
+            componentRange.length = componentEnd - componentRange.location + 1;
+        }
+
+        if (charIndex == length) {
+            /* Fell off the end; this is the last component. */
+            done = true;
+        }
+
+        /* componentRange should now contain the range of the current
+           component; trimmed of any whitespace. */
+
+        /* We want to look for a max-age value. */
+        if (!maxAgeValue && CFStringFindWithOptions(cacheControlHeader, CFSTR("max-age"), componentRange, kCFCompareCaseInsensitive | kCFCompareAnchored, &maxAgeRg)) {
+            CFIndex equalIdx;
+            CFIndex maxCompRg = componentRange.location + componentRange.length;
+            for (equalIdx = maxAgeRg.location + maxAgeRg.length; equalIdx < maxCompRg; equalIdx ++) {
+                UniChar equalCh = CFStringGetCharacterFromInlineBuffer(&inlineBuf, equalIdx);
+                if (equalCh == '=') {
+                    // Parse out max-age value
+                    equalIdx ++;
+                    while (equalIdx < maxCompRg && CFCharacterSetIsCharacterMember(whitespaceSet, CFStringGetCharacterAtIndex(cacheControlHeader, equalIdx))) {
+                        equalIdx ++;
+                    }
+                    if (equalIdx < maxCompRg) {
+                        CFReleaseNull(maxAgeValue);
+                        maxAgeValue = CFStringCreateWithSubstring(kCFAllocatorDefault, cacheControlHeader, CFRangeMake(equalIdx, maxCompRg-equalIdx));
+                    }
+                } else if (!CFCharacterSetIsCharacterMember(whitespaceSet, equalCh)) {
+                    // Not a valid max-age header; break out doing nothing
+                    break;
+                }
+            }
+        }
+
+        if (!done && maxAgeValue) {
+            done = true;
+        }
+        if (!done) {
+            /* Advance to the next component; + 1 to get past the comma. */
+            componentRange.location = charIndex + 1;
+        }
+    }
+
+    return maxAgeValue;
+}
+
+static void asynchttp_complete(asynchttp_t *http) {
+    secdebug("http", "http: %p", http);
+    /* Shutdown streams and timer, we're about to invoke our client callback. */
+    if (http->stream) {
+        CFReadStreamSetClient(http->stream, kCFStreamEventNone, NULL, NULL);
+        CFReadStreamSetDispatchQueue(http->stream, NULL);
+        CFReadStreamClose(http->stream);
+        CFReleaseNull(http->stream);
+    }
+    if (http->timer) {
+        dispatch_source_cancel(http->timer);
+        dispatch_release_null(http->timer);
+    }
+
+    if (http->completed) {
+        /* This should probably move to our clients. */
+        CFTimeInterval maxAge = NULL_TIME;
+        if (http->response) {
+            CFStringRef cacheControl = CFHTTPMessageCopyHeaderFieldValue(
+                http->response, CFSTR("cache-control"));
+            if (cacheControl) {
+                CFStringRef maxAgeValue = copyParseMaxAge(cacheControl);
+                CFRelease(cacheControl);
+                if (maxAgeValue) {
+                    secdebug("http", "http header max-age: %@", maxAgeValue);
+                    maxAge = CFStringGetDoubleValue(maxAgeValue);
+                    CFRelease(maxAgeValue);
+                }
+            }
+        }
+        http->completed(http, maxAge);
+    }
+}
+
+static void handle_server_response(CFReadStreamRef stream,
+    CFStreamEventType type, void *info) {
+    asynchttp_t *http = (asynchttp_t *)info;
+    if (!http->stream) {
+        secerror("Avoiding crash due to CFReadStream invoking us after we called CFReadStreamSetDispatchQueue(stream, NULL) on a different block on our serial queue");
+        return;
+    }
+
+    switch (type) {
+    case kCFStreamEventHasBytesAvailable:
+    {
+        UInt8 buffer[POST_BUFSIZE];
+        CFIndex length;
+        do {
+#if 1
+            length = CFReadStreamRead(stream, buffer, sizeof(buffer));
+#else
+            const UInt8 *buffer = CFReadStreamGetBuffer(stream, -1, &length);
+#endif
+            secdebug("http",
+                "stream: %@ kCFStreamEventHasBytesAvailable read: %lu bytes",
+                stream, length);
+            if (length < 0) {
+                /* Negative length == error */
+                asynchttp_complete(http);
+                break;
+            } else if (length > 0) {
+                //CFHTTPMessageAppendBytes(http->response, buffer, length);
+                CFDataAppendBytes(http->data, buffer, length);
+            } else {
+                /* Read 0 bytes. This is a no-op, but we need to keep
+                   reading until CFReadStreamHasBytesAvailable is false.
+                 */
+            }
+        } while (CFReadStreamHasBytesAvailable(stream));
+        break;
+    }
+    case kCFStreamEventErrorOccurred:
+    {
+        CFStreamError error = CFReadStreamGetError(stream);
+
+        secdebug("http",
+            "stream: %@ kCFStreamEventErrorOccurred domain: %ld error: %ld",
+            stream, error.domain, (long) error.error);
+
+        if (error.domain == kCFStreamErrorDomainPOSIX) {
+            ocspdErrorLog("CFReadStream posix: %s", strerror(error.error));
+        } else if (error.domain == kCFStreamErrorDomainMacOSStatus) {
+            ocspdErrorLog("CFReadStream osstatus: %"PRIstatus, error.error);
+        } else {
+            ocspdErrorLog("CFReadStream domain: %ld error: %"PRIstatus,
+                error.domain, error.error);
+        }
+        asynchttp_complete(http);
+        break;
+    }
+    case kCFStreamEventEndEncountered:
+    {
+        http->response = (CFHTTPMessageRef)CFReadStreamCopyProperty(
+            stream, kCFStreamPropertyHTTPResponseHeader);
+        secdebug("http", "stream: %@ kCFStreamEventEndEncountered hdr: %@",
+            stream, http->response);
+        CFHTTPMessageSetBody(http->response, http->data);
+        asynchttp_complete(http);
+        break;
+    }
+    default:
+        ocspdErrorLog("handle_server_response unexpected event type: %lu",
+            type);
+        break;
+    }
+}
+
+/* Create a URI suitable for use in an http GET request, will return NULL if
+   the length would exceed 255 bytes. */
+static CFURLRef createGetURL(CFURLRef responder, CFDataRef request) {
+    CFURLRef getURL = NULL;
+    CFMutableDataRef base64Request = NULL;
+    CFStringRef base64RequestString = NULL;
+    CFStringRef peRequest = NULL;
+    CFIndex base64Len;
+
+    base64Len = SecBase64Encode(NULL, CFDataGetLength(request), NULL, 0);
+    /* Don't bother doing all the work below if we know the end result will
+       exceed 255 bytes (minus one for the '/' separator makes 254). */
+    if (base64Len + CFURLGetBytes(responder, NULL, 0) > 254)
+        return NULL;
+
+    require(base64Request = CFDataCreateMutable(kCFAllocatorDefault,
+        base64Len), errOut);
+    CFDataSetLength(base64Request, base64Len);
+    SecBase64Encode(CFDataGetBytePtr(request), CFDataGetLength(request),
+        (char *)CFDataGetMutableBytePtr(base64Request), base64Len);
+    require(base64RequestString = CFStringCreateWithBytes(kCFAllocatorDefault,
+        CFDataGetBytePtr(base64Request), base64Len, kCFStringEncodingUTF8,
+        false), errOut);
+    /* percent-encode all reserved characters from RFC 3986 [2.2] */
+    require(peRequest = CFURLCreateStringByAddingPercentEscapes(
+        kCFAllocatorDefault, base64RequestString, NULL,
+        CFSTR(":/?#[]@!$&'()*+,;="), kCFStringEncodingUTF8), errOut);
+#if 1
+    CFStringRef urlString = CFURLGetString(responder);
+    CFStringRef fullURL;
+    if (CFStringHasSuffix(urlString, CFSTR("/"))) {
+        fullURL = CFStringCreateWithFormat(kCFAllocatorDefault, NULL,
+            CFSTR("%@%@"), urlString, peRequest);
+    } else {
+        fullURL = CFStringCreateWithFormat(kCFAllocatorDefault, NULL,
+            CFSTR("%@/%@"), urlString, peRequest);
+    }
+    getURL = CFURLCreateWithString(kCFAllocatorDefault, fullURL, NULL);
+    CFRelease(fullURL);
+#else
+    getURL = CFURLCreateWithString(kCFAllocatorDefault, peRequest, responder);
+#endif
+
+errOut:
+    CFReleaseSafe(base64Request);
+    CFReleaseSafe(base64RequestString);
+    CFReleaseSafe(peRequest);
+
+    return getURL;
+}
+
+bool asyncHttpPost(CFURLRef responder, CFDataRef requestData /* , bool force_nocache */ ,
+    asynchttp_t *http) {
+    bool result = true; /* True, we didn't schedule any work. */
+       /* resources to release on exit */
+    CFURLRef getURL = NULL;
+
+/* Interesting tidbit from rfc5019
+   When sending requests that are less than or equal to 255 bytes in
+   total (after encoding) including the scheme and delimiters (http://),
+   server name and base64-encoded OCSPRequest structure, clients MUST
+   use the GET method (to enable OCSP response caching).  OCSP requests
+   larger than 255 bytes SHOULD be submitted using the POST method.
+
+   Interesting tidbit from rfc2616:
+   Note: Servers ought to be cautious about depending on URI lengths
+   above 255 bytes, because some older client or proxy
+   implementations might not properly support these lengths.
+
+   Given the second note I'm assuming that the note in rfc5019 is about the
+   length of the URI, not the length of the entire HTTP request.
+
+   If we need to consider the entire request we need to have 17 bytes less, or
+   17 + 25 = 42 if we are appending a "Cache-Control: no-cache CRLF" header
+   field.
+
+   The 17 and 42 above are based on the request encoding from rfc2616
+   Method SP Request-URI SP HTTP-Version CRLF (header CRLF)* CRLF
+   so in our case it's:
+   GET SP URI SP HTTP/1.1 CRLF CRLF
+   17 + len(URI) bytes
+   or
+   GET SP URI SP HTTP/1.1 CRLF Cache-Control: SP no-cache CRLF CRLF
+   42 + len(URI) bytes
+ */
+
+    /* First let's try creating a GET request. */
+    getURL = createGetURL(responder, requestData);
+    if (getURL && CFURLGetBytes(getURL, NULL, 0) < 256) {
+        /* Get URI is less than 256 bytes encoded, making it safe even for
+           older proxy or caching servers, so let's use HTTP GET. */
+        secdebug("http", "GET[%ld] %@", CFURLGetBytes(getURL, NULL, 0), getURL);
+        require_quiet(http->request = CFHTTPMessageCreateRequest(kCFAllocatorDefault,
+            CFSTR("GET"), getURL, kCFHTTPVersion1_1), errOut);
+    } else {
+        /* GET Request too big to ensure error free transmission, let's
+           create a HTTP POST http->request instead. */
+        secdebug("http", "POST %@ CRLF body", responder);
+        require_quiet(http->request = CFHTTPMessageCreateRequest(kCFAllocatorDefault,
+            CFSTR("POST"), responder, kCFHTTPVersion1_1), errOut);
+        /* Set the body and required header fields. */
+        CFHTTPMessageSetBody(http->request, requestData);
+        CFHTTPMessageSetHeaderFieldValue(http->request, kContentType,
+            kAppOcspRequest);
+    }
+
+#if 0
+    if (force_nocache) {
+        CFHTTPMessageSetHeaderFieldValue(http->request, CFSTR("Cache-Control"),
+            CFSTR("no-cache"));
+    }
+#endif
+
+    result = asynchttp_request(NULL, http);
+
+errOut:
+    CFReleaseSafe(getURL);
+
+    return result;
+}
+
+
+static void asynchttp_timer_proc(asynchttp_t *http CF_CONSUMED) {
+    CFStringRef req_meth = http->request ? CFHTTPMessageCopyRequestMethod(http->request) : NULL;
+    CFURLRef req_url = http->request ? CFHTTPMessageCopyRequestURL(http->request) : NULL;
+    secnotice("http", "Timeout during %@ %@.", req_meth, req_url);
+    CFReleaseSafe(req_url);
+    CFReleaseSafe(req_meth);
+    asynchttp_complete(http);
+}
+
+
+void asynchttp_free(asynchttp_t *http) {
+    if (http) {
+        CFReleaseNull(http->request);
+        CFReleaseNull(http->response);
+        CFReleaseNull(http->data);
+        CFReleaseNull(http->stream);
+        dispatch_release_null(http->timer);
+    }
+}
+
+/* Return true, iff we didn't schedule any work, return false if we did. */
+bool asynchttp_request(CFHTTPMessageRef request, asynchttp_t *http) {
+    secdebug("http", "request %@", request);
+    if (request) {
+        http->request = request;
+        CFRetain(request);
+    }
+
+    /* Create the stream for the request. */
+    require_quiet(http->stream = CFReadStreamCreateForHTTPRequest(
+        kCFAllocatorDefault, http->request), errOut);
+
+       /* Set a reasonable timeout */
+    require_quiet(http->timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, http->queue), errOut);
+    dispatch_source_set_event_handler(http->timer, ^{
+        asynchttp_timer_proc(http);
+    });
+    // Set the timer's fire time to now + STREAM_TIMEOUT seconds with a .5 second fuzz factor.
+    dispatch_source_set_timer(http->timer, dispatch_time(DISPATCH_TIME_NOW, STREAM_TIMEOUT),
+                              DISPATCH_TIME_FOREVER, (int64_t)(500 * NSEC_PER_MSEC));
+    dispatch_resume(http->timer);
+
+       /* Set up possible proxy info */
+       CFDictionaryRef proxyDict = CFNetworkCopySystemProxySettings();
+       if (proxyDict) {
+               CFReadStreamSetProperty(http->stream, kCFStreamPropertyHTTPProxy, proxyDict);
+        CFRelease(proxyDict);
+    }
+
+    http->data = CFDataCreateMutable(kCFAllocatorDefault, 0);
+
+    CFStreamClientContext stream_context = { .info = http };
+    CFReadStreamSetClient(http->stream,
+        (kCFStreamEventHasBytesAvailable
+         | kCFStreamEventErrorOccurred
+         | kCFStreamEventEndEncountered),
+        handle_server_response, &stream_context);
+    CFReadStreamSetDispatchQueue(http->stream, http->queue);
+    CFReadStreamOpen(http->stream);
+
+    return false; /* false -> something was scheduled. */
+
+errOut:
+    /* Deschedule timer and free anything we might have retained so far. */
+    asynchttp_free(http);
+    return true;
+}