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