]> git.saurik.com Git - apple/security.git/blob - Security/sec/securityd/asynchttp.c
Security-57031.30.12.tar.gz
[apple/security.git] / Security / sec / securityd / asynchttp.c
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" */
53 static CFStringRef kContentType = CFSTR("Content-Type");
54 static 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. */
67 static 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
158 static 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
192 static 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. */
267 static 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
308 errOut:
309 CFReleaseSafe(base64Request);
310 CFReleaseSafe(base64RequestString);
311 CFReleaseSafe(peRequest);
312
313 return getURL;
314 }
315
316 bool 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
380 errOut:
381 CFReleaseSafe(getURL);
382
383 return result;
384 }
385
386
387 static 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
397 void 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. */
408 bool 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
449 errOut:
450 /* Deschedule timer and free anything we might have retained so far. */
451 asynchttp_free(http);
452 return true;
453 }