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