]> git.saurik.com Git - apple/security.git/blob - OSX/sec/securityd/asynchttp.c
Security-57740.20.22.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 /* POST method has Content-Type header line equal to
51 "application/ocsp-request" */
52 static CFStringRef kContentType = CFSTR("Content-Type");
53 static CFStringRef kAppOcspRequest = CFSTR("application/ocsp-request");
54
55 /* SPI to specify timeout on CFReadStream */
56 #define _kCFStreamPropertyReadTimeout CFSTR("_kCFStreamPropertyReadTimeout")
57 #define _kCFStreamPropertyWriteTimeout CFSTR("_kCFStreamPropertyWriteTimeout")
58
59 /* The timeout we set - 7 seconds */
60 #define STREAM_TIMEOUT (7 * NSEC_PER_SEC)
61
62 #define POST_BUFSIZE 2048
63
64 /* There has got to be an easier way to do this. For now we based this code
65 on CFNetwork/Connection/URLResponse.cpp. */
66 static CFStringRef copyParseMaxAge(CFStringRef cacheControlHeader) {
67 /* The format of the cache control header is a comma-separated list, but
68 each list element could be a key-value pair, with the value quoted and
69 possibly containing a comma. */
70 CFStringInlineBuffer inlineBuf = {};
71 CFRange componentRange;
72 CFIndex length = CFStringGetLength(cacheControlHeader);
73 bool done = false;
74 CFCharacterSetRef whitespaceSet = CFCharacterSetGetPredefined(kCFCharacterSetWhitespace);
75 CFStringRef maxAgeValue = NULL;
76
77 CFStringInitInlineBuffer(cacheControlHeader, &inlineBuf, CFRangeMake(0, length));
78 componentRange.location = 0;
79
80 while (!done) {
81 bool inQuotes = false;
82 bool foundComponentStart = false;
83 CFIndex charIndex = componentRange.location;
84 CFIndex componentEnd = -1;
85 CFRange maxAgeRg;
86 componentRange.length = 0;
87
88 while (charIndex < length) {
89 UniChar ch = CFStringGetCharacterFromInlineBuffer(&inlineBuf, charIndex);
90 if (!inQuotes && ch == ',') {
91 componentRange.length = charIndex - componentRange.location;
92 break;
93 }
94 if (!CFCharacterSetIsCharacterMember(whitespaceSet, ch)) {
95 if (!foundComponentStart) {
96 foundComponentStart = true;
97 componentRange.location = charIndex;
98 } else {
99 componentEnd = charIndex;
100 }
101 if (ch == '\"') {
102 inQuotes = (inQuotes == false);
103 }
104 }
105 charIndex ++;
106 }
107
108 if (componentEnd == -1) {
109 componentRange.length = charIndex - componentRange.location;
110 } else {
111 componentRange.length = componentEnd - componentRange.location + 1;
112 }
113
114 if (charIndex == length) {
115 /* Fell off the end; this is the last component. */
116 done = true;
117 }
118
119 /* componentRange should now contain the range of the current
120 component; trimmed of any whitespace. */
121
122 /* We want to look for a max-age value. */
123 if (!maxAgeValue && CFStringFindWithOptions(cacheControlHeader, CFSTR("max-age"), componentRange, kCFCompareCaseInsensitive | kCFCompareAnchored, &maxAgeRg)) {
124 CFIndex equalIdx;
125 CFIndex maxCompRg = componentRange.location + componentRange.length;
126 for (equalIdx = maxAgeRg.location + maxAgeRg.length; equalIdx < maxCompRg; equalIdx ++) {
127 UniChar equalCh = CFStringGetCharacterFromInlineBuffer(&inlineBuf, equalIdx);
128 if (equalCh == '=') {
129 // Parse out max-age value
130 equalIdx ++;
131 while (equalIdx < maxCompRg && CFCharacterSetIsCharacterMember(whitespaceSet, CFStringGetCharacterAtIndex(cacheControlHeader, equalIdx))) {
132 equalIdx ++;
133 }
134 if (equalIdx < maxCompRg) {
135 CFReleaseNull(maxAgeValue);
136 maxAgeValue = CFStringCreateWithSubstring(kCFAllocatorDefault, cacheControlHeader, CFRangeMake(equalIdx, maxCompRg-equalIdx));
137 }
138 } else if (!CFCharacterSetIsCharacterMember(whitespaceSet, equalCh)) {
139 // Not a valid max-age header; break out doing nothing
140 break;
141 }
142 }
143 }
144
145 if (!done && maxAgeValue) {
146 done = true;
147 }
148 if (!done) {
149 /* Advance to the next component; + 1 to get past the comma. */
150 componentRange.location = charIndex + 1;
151 }
152 }
153
154 return maxAgeValue;
155 }
156
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. */
160 if (http->stream) {
161 CFReadStreamSetClient(http->stream, kCFStreamEventNone, NULL, NULL);
162 CFReadStreamSetDispatchQueue(http->stream, NULL);
163 CFReadStreamClose(http->stream);
164 CFReleaseNull(http->stream);
165 }
166 if (http->timer) {
167 dispatch_source_cancel(http->timer);
168 dispatch_release_null(http->timer);
169 }
170
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"));
177 if (cacheControl) {
178 CFStringRef maxAgeValue = copyParseMaxAge(cacheControl);
179 CFRelease(cacheControl);
180 if (maxAgeValue) {
181 secdebug("http", "http header max-age: %@", maxAgeValue);
182 maxAge = CFStringGetDoubleValue(maxAgeValue);
183 CFRelease(maxAgeValue);
184 }
185 }
186 }
187 http->completed(http, maxAge);
188 }
189 }
190
191 static void handle_server_response(CFReadStreamRef stream,
192 CFStreamEventType type, void *info) {
193 asynchttp_t *http = (asynchttp_t *)info;
194 if (!http->stream) {
195 secerror("Avoiding crash due to CFReadStream invoking us after we called CFReadStreamSetDispatchQueue(stream, NULL) on a different block on our serial queue");
196 return;
197 }
198
199 switch (type) {
200 case kCFStreamEventHasBytesAvailable:
201 {
202 UInt8 buffer[POST_BUFSIZE];
203 CFIndex length;
204 do {
205 #if 1
206 length = CFReadStreamRead(stream, buffer, sizeof(buffer));
207 #else
208 const UInt8 *buffer = CFReadStreamGetBuffer(stream, -1, &length);
209 #endif
210 secdebug("http",
211 "stream: %@ kCFStreamEventHasBytesAvailable read: %lu bytes",
212 stream, length);
213 if (length < 0) {
214 /* Negative length == error */
215 asynchttp_complete(http);
216 break;
217 } else if (length > 0) {
218 //CFHTTPMessageAppendBytes(http->response, buffer, length);
219 CFDataAppendBytes(http->data, buffer, length);
220 } else {
221 /* Read 0 bytes. This is a no-op, but we need to keep
222 reading until CFReadStreamHasBytesAvailable is false.
223 */
224 }
225 } while (CFReadStreamHasBytesAvailable(stream));
226 break;
227 }
228 case kCFStreamEventErrorOccurred:
229 {
230 CFStreamError error = CFReadStreamGetError(stream);
231
232 secdebug("http",
233 "stream: %@ kCFStreamEventErrorOccurred domain: %ld error: %ld",
234 stream, error.domain, (long) error.error);
235
236 if (error.domain == kCFStreamErrorDomainPOSIX) {
237 secerror("CFReadStream posix: %s", strerror(error.error));
238 } else if (error.domain == kCFStreamErrorDomainMacOSStatus) {
239 secerror("CFReadStream osstatus: %"PRIstatus, error.error);
240 } else {
241 secerror("CFReadStream domain: %ld error: %"PRIstatus,
242 error.domain, error.error);
243 }
244 asynchttp_complete(http);
245 break;
246 }
247 case kCFStreamEventEndEncountered:
248 {
249 http->response = (CFHTTPMessageRef)CFReadStreamCopyProperty(
250 stream, kCFStreamPropertyHTTPResponseHeader);
251 secdebug("http", "stream: %@ kCFStreamEventEndEncountered hdr: %@",
252 stream, http->response);
253 CFHTTPMessageSetBody(http->response, http->data);
254 asynchttp_complete(http);
255 break;
256 }
257 default:
258 secerror("handle_server_response unexpected event type: %lu",
259 type);
260 break;
261 }
262 }
263
264 /* Create a URI suitable for use in an http GET request, will return NULL if
265 the length would exceed 255 bytes. */
266 static CFURLRef createGetURL(CFURLRef responder, CFDataRef request) {
267 CFURLRef getURL = NULL;
268 CFMutableDataRef base64Request = NULL;
269 CFStringRef base64RequestString = NULL;
270 CFStringRef peRequest = NULL;
271 CFIndex base64Len;
272
273 base64Len = SecBase64Encode(NULL, CFDataGetLength(request), NULL, 0);
274 /* Don't bother doing all the work below if we know the end result will
275 exceed 255 bytes (minus one for the '/' separator makes 254). */
276 if (base64Len + CFURLGetBytes(responder, NULL, 0) > 254)
277 return NULL;
278
279 require(base64Request = CFDataCreateMutable(kCFAllocatorDefault,
280 base64Len), errOut);
281 CFDataSetLength(base64Request, base64Len);
282 SecBase64Encode(CFDataGetBytePtr(request), CFDataGetLength(request),
283 (char *)CFDataGetMutableBytePtr(base64Request), base64Len);
284 require(base64RequestString = CFStringCreateWithBytes(kCFAllocatorDefault,
285 CFDataGetBytePtr(base64Request), base64Len, kCFStringEncodingUTF8,
286 false), errOut);
287 /* percent-encode all reserved characters from RFC 3986 [2.2] */
288 require(peRequest = CFURLCreateStringByAddingPercentEscapes(
289 kCFAllocatorDefault, base64RequestString, NULL,
290 CFSTR(":/?#[]@!$&'()*+,;="), kCFStringEncodingUTF8), errOut);
291 #if 1
292 CFStringRef urlString = CFURLGetString(responder);
293 CFStringRef fullURL;
294 if (CFStringHasSuffix(urlString, CFSTR("/"))) {
295 fullURL = CFStringCreateWithFormat(kCFAllocatorDefault, NULL,
296 CFSTR("%@%@"), urlString, peRequest);
297 } else {
298 fullURL = CFStringCreateWithFormat(kCFAllocatorDefault, NULL,
299 CFSTR("%@/%@"), urlString, peRequest);
300 }
301 getURL = CFURLCreateWithString(kCFAllocatorDefault, fullURL, NULL);
302 CFRelease(fullURL);
303 #else
304 getURL = CFURLCreateWithString(kCFAllocatorDefault, peRequest, responder);
305 #endif
306
307 errOut:
308 CFReleaseSafe(base64Request);
309 CFReleaseSafe(base64RequestString);
310 CFReleaseSafe(peRequest);
311
312 return getURL;
313 }
314
315 bool asyncHttpPost(CFURLRef responder, CFDataRef requestData /* , bool force_nocache */ ,
316 uint64_t timeout, asynchttp_t *http) {
317 bool result = true; /* True, we didn't schedule any work. */
318 /* resources to release on exit */
319 CFURLRef getURL = NULL;
320
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.
327
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.
332
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.
335
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
338 field.
339
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
342 so in our case it's:
343 GET SP URI SP HTTP/1.1 CRLF CRLF
344 17 + len(URI) bytes
345 or
346 GET SP URI SP HTTP/1.1 CRLF Cache-Control: SP no-cache CRLF CRLF
347 42 + len(URI) bytes
348 */
349
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);
358 } else {
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,
367 kAppOcspRequest);
368 }
369
370 #if 0
371 if (force_nocache) {
372 CFHTTPMessageSetHeaderFieldValue(http->request, CFSTR("Cache-Control"),
373 CFSTR("no-cache"));
374 }
375 #endif
376
377 result = asynchttp_request(NULL, timeout, http);
378
379 errOut:
380 CFReleaseSafe(getURL);
381
382 return result;
383 }
384
385
386 static void asynchttp_timer_proc(asynchttp_t *http CF_CONSUMED) {
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);
393 }
394
395
396 void asynchttp_free(asynchttp_t *http) {
397 if (http) {
398 CFReleaseNull(http->token);
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. */
408 bool asynchttp_request(CFHTTPMessageRef request, uint64_t timeout, 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 uint64_t stream_timeout = timeout;
426 if (timeout == 0) {
427 stream_timeout = STREAM_TIMEOUT;
428 }
429 dispatch_source_set_timer(http->timer, dispatch_time(DISPATCH_TIME_NOW, stream_timeout),
430 DISPATCH_TIME_FOREVER, (int64_t)(500 * NSEC_PER_MSEC));
431 dispatch_resume(http->timer);
432
433 /* Set up possible proxy info */
434 CFDictionaryRef proxyDict = CFNetworkCopySystemProxySettings();
435 if (proxyDict) {
436 CFReadStreamSetProperty(http->stream, kCFStreamPropertyHTTPProxy, proxyDict);
437 CFRelease(proxyDict);
438 }
439
440 /* Set source application property info */
441 if (http->token) {
442 CFReadStreamSetProperty(http->stream, kCFStreamPropertySourceApplication, http->token);
443 }
444
445 http->data = CFDataCreateMutable(kCFAllocatorDefault, 0);
446
447 CFStreamClientContext stream_context = { .info = http };
448 CFReadStreamSetClient(http->stream,
449 (kCFStreamEventHasBytesAvailable
450 | kCFStreamEventErrorOccurred
451 | kCFStreamEventEndEncountered),
452 handle_server_response, &stream_context);
453 CFReadStreamSetDispatchQueue(http->stream, http->queue);
454 CFReadStreamOpen(http->stream);
455
456 return false; /* false -> something was scheduled. */
457
458 errOut:
459 /* Deschedule timer and free anything we might have retained so far. */
460 asynchttp_free(http);
461 return true;
462 }