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