#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 <security_utilities/debugging.h>
#include <asl.h>
#include <string.h>
#if __LP64__
#define PRIstatus "d"
#else
#define PRIstatus "ld"
#endif
#define ocspdErrorLog(args...) asl_log(NULL, NULL, ASL_LEVEL_ERR, ## args)
static CFStringRef kContentType = CFSTR("Content-Type");
static CFStringRef kAppOcspRequest = CFSTR("application/ocsp-request");
#define _kCFStreamPropertyReadTimeout CFSTR("_kCFStreamPropertyReadTimeout")
#define _kCFStreamPropertyWriteTimeout CFSTR("_kCFStreamPropertyWriteTimeout")
#define STREAM_TIMEOUT 7.0
#define POST_BUFSIZE 2048
static void terminate_stream(CFReadStreamRef stream)
{
CFReadStreamSetClient(stream, kCFStreamEventNone, NULL, NULL);
CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(),
kCFRunLoopCommonModes);
CFReadStreamClose(stream);
}
static CFStringRef parseMaxAge(CFStringRef cacheControlHeader) {
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) {
done = true;
}
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 == '=') {
equalIdx ++;
while (equalIdx < maxCompRg && CFCharacterSetIsCharacterMember(whitespaceSet, CFStringGetCharacterAtIndex(cacheControlHeader, equalIdx))) {
equalIdx ++;
}
if (equalIdx < maxCompRg) {
maxAgeValue = CFStringCreateWithSubstring(kCFAllocatorDefault, cacheControlHeader, CFRangeMake(equalIdx, maxCompRg-equalIdx));
}
} else if (!CFCharacterSetIsCharacterMember(whitespaceSet, equalCh)) {
break;
}
}
}
if (!done && maxAgeValue) {
done = true;
}
if (!done) {
componentRange.location = charIndex + 1;
}
}
return maxAgeValue;
}
static void asynchttp_complete(asynchttp_t *http) {
secdebug("http", "http: %p", http);
if (http->stream) {
terminate_stream(http->stream);
CFReleaseNull(http->stream);
}
if (http->timer) {
CFRunLoopTimerInvalidate(http->timer);
CFReleaseNull(http->timer);
}
if (http->completed) {
CFTimeInterval maxAge = NULL_TIME;
if (http->response) {
CFStringRef cacheControl = CFHTTPMessageCopyHeaderFieldValue(
http->response, CFSTR("cache-control"));
if (cacheControl) {
CFStringRef maxAgeValue = parseMaxAge(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;
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) {
asynchttp_complete(http);
break;
} else if (length > 0) {
CFDataAppendBytes(http->data, buffer, length);
} else {
asynchttp_complete(http);
break;
}
} while (CFReadStreamHasBytesAvailable(stream));
break;
}
case kCFStreamEventErrorOccurred:
{
CFStreamError error = CFReadStreamGetError(stream);
secdebug("http",
"stream: %@ kCFStreamEventErrorOccurred domain: %ld error: %ld",
stream, error.domain, 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;
}
}
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);
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);
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 ,
asynchttp_t *http) {
bool result = true;
CFURLRef getURL = NULL;
getURL = createGetURL(responder, requestData);
if (getURL && CFURLGetBytes(getURL, NULL, 0) < 256) {
secdebug("http", "GET[%ld] %@", CFURLGetBytes(getURL, NULL, 0), getURL);
require_quiet(http->request = CFHTTPMessageCreateRequest(kCFAllocatorDefault,
CFSTR("GET"), getURL, kCFHTTPVersion1_1), errOut);
} else {
secdebug("http", "POST %@ CRLF body", responder);
require_quiet(http->request = CFHTTPMessageCreateRequest(kCFAllocatorDefault,
CFSTR("POST"), responder, kCFHTTPVersion1_1), errOut);
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(CFRunLoopTimerRef timer, void *info) {
asynchttp_t *http = (asynchttp_t *)info;
CFStringRef req_meth = http->request ? CFHTTPMessageCopyRequestMethod(http->request) : NULL;
CFURLRef req_url = http->request ? CFHTTPMessageCopyRequestURL(http->request) : NULL;
secdebug("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);
CFReleaseNull(http->source);
if (http->timer) {
CFRunLoopTimerInvalidate(http->timer);
CFReleaseNull(http->timer);
}
}
}
bool asynchttp_request(CFHTTPMessageRef request, asynchttp_t *http) {
secdebug("http", "request %@", request);
if (request) {
http->request = request;
CFRetain(request);
}
require_quiet(http->stream = CFReadStreamCreateForHTTPRequest(
kCFAllocatorDefault, http->request), errOut);
CFRunLoopTimerContext tctx = { .info = http };
http->timer = CFRunLoopTimerCreate(kCFAllocatorDefault,
CFAbsoluteTimeGetCurrent() + STREAM_TIMEOUT,
0, 0, 0, asynchttp_timer_proc, &tctx);
if (http->timer == NULL) {
asl_log(NULL, NULL, ASL_LEVEL_ERR, "FATAL: failed to create timer.");
} else {
CFRunLoopAddTimer(CFRunLoopGetCurrent(), http->timer,
kCFRunLoopDefaultMode);
}
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);
CFReadStreamScheduleWithRunLoop(http->stream, CFRunLoopGetCurrent(),
kCFRunLoopCommonModes);
CFReadStreamOpen(http->stream);
return false;
errOut:
asynchttp_free(http);
return true;
}