]> git.saurik.com Git - apple/security.git/blob - sec/securityd/asynchttp.c
Security-55163.44.tar.gz
[apple/security.git] / sec / securityd / asynchttp.c
1 /*
2 * Copyright (c) 2009-2010 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 <security_utilities/debugging.h>
37 #include <asl.h>
38 #include <string.h>
39
40 #if __LP64__
41 #define PRIstatus "d"
42 #else
43 #define PRIstatus "ld"
44 #endif
45
46 #define ocspdErrorLog(args...) asl_log(NULL, NULL, ASL_LEVEL_ERR, ## args)
47
48 /* POST method has Content-Type header line equal to
49 "application/ocsp-request" */
50 static CFStringRef kContentType = CFSTR("Content-Type");
51 static CFStringRef kAppOcspRequest = CFSTR("application/ocsp-request");
52
53 /* SPI to specify timeout on CFReadStream */
54 #define _kCFStreamPropertyReadTimeout CFSTR("_kCFStreamPropertyReadTimeout")
55 #define _kCFStreamPropertyWriteTimeout CFSTR("_kCFStreamPropertyWriteTimeout")
56
57 /* the timeout we set */
58 #define STREAM_TIMEOUT 7.0
59
60 #define POST_BUFSIZE 2048
61
62 static void terminate_stream(CFReadStreamRef stream)
63 {
64 CFReadStreamSetClient(stream, kCFStreamEventNone, NULL, NULL);
65 CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(),
66 kCFRunLoopCommonModes);
67 CFReadStreamClose(stream);
68 //CFRelease(stream);
69 }
70
71 /* There has got to be an easier way to do this. For now we based this code
72 on CFNetwork/Connection/URLResponse.cpp. */
73 static CFStringRef parseMaxAge(CFStringRef cacheControlHeader) {
74 /* The format of the cache control header is a comma-separated list, but
75 each list element could be a key-value pair, with the value quoted and
76 possibly containing a comma. */
77 CFStringInlineBuffer inlineBuf;
78 CFRange componentRange;
79 CFIndex length = CFStringGetLength(cacheControlHeader);
80 bool done = false;
81 CFCharacterSetRef whitespaceSet = CFCharacterSetGetPredefined(kCFCharacterSetWhitespace);
82 CFStringRef maxAgeValue = NULL;
83
84 CFStringInitInlineBuffer(cacheControlHeader, &inlineBuf, CFRangeMake(0, length));
85 componentRange.location = 0;
86
87 while (!done) {
88 bool inQuotes = false;
89 bool foundComponentStart = false;
90 CFIndex charIndex = componentRange.location;
91 CFIndex componentEnd = -1;
92 CFRange maxAgeRg;
93 componentRange.length = 0;
94
95 while (charIndex < length) {
96 UniChar ch = CFStringGetCharacterFromInlineBuffer(&inlineBuf, charIndex);
97 if (!inQuotes && ch == ',') {
98 componentRange.length = charIndex - componentRange.location;
99 break;
100 }
101 if (!CFCharacterSetIsCharacterMember(whitespaceSet, ch)) {
102 if (!foundComponentStart) {
103 foundComponentStart = true;
104 componentRange.location = charIndex;
105 } else {
106 componentEnd = charIndex;
107 }
108 if (ch == '\"') {
109 inQuotes = (inQuotes == false);
110 }
111 }
112 charIndex ++;
113 }
114
115 if (componentEnd == -1) {
116 componentRange.length = charIndex - componentRange.location;
117 } else {
118 componentRange.length = componentEnd - componentRange.location + 1;
119 }
120
121 if (charIndex == length) {
122 /* Fell off the end; this is the last component. */
123 done = true;
124 }
125
126 /* componentRange should now contain the range of the current
127 component; trimmed of any whitespace. */
128
129 /* We want to look for a max-age value. */
130 if (!maxAgeValue && CFStringFindWithOptions(cacheControlHeader, CFSTR("max-age"), componentRange, kCFCompareCaseInsensitive | kCFCompareAnchored, &maxAgeRg)) {
131 CFIndex equalIdx;
132 CFIndex maxCompRg = componentRange.location + componentRange.length;
133 for (equalIdx = maxAgeRg.location + maxAgeRg.length; equalIdx < maxCompRg; equalIdx ++) {
134 UniChar equalCh = CFStringGetCharacterFromInlineBuffer(&inlineBuf, equalIdx);
135 if (equalCh == '=') {
136 // Parse out max-age value
137 equalIdx ++;
138 while (equalIdx < maxCompRg && CFCharacterSetIsCharacterMember(whitespaceSet, CFStringGetCharacterAtIndex(cacheControlHeader, equalIdx))) {
139 equalIdx ++;
140 }
141 if (equalIdx < maxCompRg) {
142 maxAgeValue = CFStringCreateWithSubstring(kCFAllocatorDefault, cacheControlHeader, CFRangeMake(equalIdx, maxCompRg-equalIdx));
143 }
144 } else if (!CFCharacterSetIsCharacterMember(whitespaceSet, equalCh)) {
145 // Not a valid max-age header; break out doing nothing
146 break;
147 }
148 }
149 }
150
151 if (!done && maxAgeValue) {
152 done = true;
153 }
154 if (!done) {
155 /* Advance to the next component; + 1 to get past the comma. */
156 componentRange.location = charIndex + 1;
157 }
158 }
159
160 return maxAgeValue;
161 }
162
163 static void asynchttp_complete(asynchttp_t *http) {
164 secdebug("http", "http: %p", http);
165 /* Shutdown streams and timers, we're about to invoke our client callback. */
166 if (http->stream) {
167 terminate_stream(http->stream);
168 CFReleaseNull(http->stream);
169 }
170 if (http->timer) {
171 CFRunLoopTimerInvalidate(http->timer);
172 CFReleaseNull(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 = parseMaxAge(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 switch (type) {
199 case kCFStreamEventHasBytesAvailable:
200 {
201 UInt8 buffer[POST_BUFSIZE];
202 CFIndex length;
203 do {
204 #if 1
205 length = CFReadStreamRead(stream, buffer, sizeof(buffer));
206 #else
207 const UInt8 *buffer = CFReadStreamGetBuffer(stream, -1, &length);
208 #endif
209 secdebug("http",
210 "stream: %@ kCFStreamEventHasBytesAvailable read: %lu bytes",
211 stream, length);
212 if (length < 0) {
213 /* Negative length == error */
214 asynchttp_complete(http);
215 break;
216 } else if (length > 0) {
217 //CFHTTPMessageAppendBytes(http->response, buffer, length);
218 CFDataAppendBytes(http->data, buffer, length);
219 } else {
220 /* Read 0 bytes, are we are done or do we wait for
221 kCFStreamEventEndEncountered? */
222 asynchttp_complete(http);
223 break;
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, error.error);
235
236 if (error.domain == kCFStreamErrorDomainPOSIX) {
237 ocspdErrorLog("CFReadStream posix: %s", strerror(error.error));
238 } else if (error.domain == kCFStreamErrorDomainMacOSStatus) {
239 ocspdErrorLog("CFReadStream osstatus: %"PRIstatus, error.error);
240 } else {
241 ocspdErrorLog("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 ocspdErrorLog("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 require(peRequest = CFURLCreateStringByAddingPercentEscapes(
288 kCFAllocatorDefault, base64RequestString, NULL, CFSTR("+/="),
289 kCFStringEncodingUTF8), errOut);
290 #if 1
291 CFStringRef urlString = CFURLGetString(responder);
292 CFStringRef fullURL;
293 if (CFStringHasSuffix(urlString, CFSTR("/"))) {
294 fullURL = CFStringCreateWithFormat(kCFAllocatorDefault, NULL,
295 CFSTR("%@%@"), urlString, peRequest);
296 } else {
297 fullURL = CFStringCreateWithFormat(kCFAllocatorDefault, NULL,
298 CFSTR("%@/%@"), urlString, peRequest);
299 }
300 getURL = CFURLCreateWithString(kCFAllocatorDefault, fullURL, NULL);
301 CFRelease(fullURL);
302 #else
303 getURL = CFURLCreateWithString(kCFAllocatorDefault, peRequest, responder);
304 #endif
305
306 errOut:
307 CFReleaseSafe(base64Request);
308 CFReleaseSafe(base64RequestString);
309 CFReleaseSafe(peRequest);
310
311 return getURL;
312 }
313
314 bool asyncHttpPost(CFURLRef responder, CFDataRef requestData /* , bool force_nocache */ ,
315 asynchttp_t *http) {
316 bool result = true; /* True, we didn't schedule any work. */
317 /* resources to release on exit */
318 CFURLRef getURL = NULL;
319
320 /* Interesting tidbit from rfc5019
321 When sending requests that are less than or equal to 255 bytes in
322 total (after encoding) including the scheme and delimiters (http://),
323 server name and base64-encoded OCSPRequest structure, clients MUST
324 use the GET method (to enable OCSP response caching). OCSP requests
325 larger than 255 bytes SHOULD be submitted using the POST method.
326
327 Interesting tidbit from rfc2616:
328 Note: Servers ought to be cautious about depending on URI lengths
329 above 255 bytes, because some older client or proxy
330 implementations might not properly support these lengths.
331
332 Given the second note I'm assuming that the note in rfc5019 is about the
333 length of the URI, not the length of the entire HTTP request.
334
335 If we need to consider the entire request we need to have 17 bytes less, or
336 17 + 25 = 42 if we are appending a "Cache-Control: no-cache CRLF" header
337 field.
338
339 The 17 and 42 above are based on the request encoding from rfc2616
340 Method SP Request-URI SP HTTP-Version CRLF (header CRLF)* CRLF
341 so in our case it's:
342 GET SP URI SP HTTP/1.1 CRLF CRLF
343 17 + len(URI) bytes
344 or
345 GET SP URI SP HTTP/1.1 CRLF Cache-Control: SP no-cache CRLF CRLF
346 42 + len(URI) bytes
347 */
348
349 /* First let's try creating a GET request. */
350 getURL = createGetURL(responder, requestData);
351 if (getURL && CFURLGetBytes(getURL, NULL, 0) < 256) {
352 /* Get URI is less than 256 bytes encoded, making it safe even for
353 older proxy or caching servers, so let's use HTTP GET. */
354 secdebug("http", "GET[%ld] %@", CFURLGetBytes(getURL, NULL, 0), getURL);
355 require_quiet(http->request = CFHTTPMessageCreateRequest(kCFAllocatorDefault,
356 CFSTR("GET"), getURL, kCFHTTPVersion1_1), errOut);
357 } else {
358 /* GET Request too big to ensure error free transmission, let's
359 create a HTTP POST http->request instead. */
360 secdebug("http", "POST %@ CRLF body", responder);
361 require_quiet(http->request = CFHTTPMessageCreateRequest(kCFAllocatorDefault,
362 CFSTR("POST"), responder, kCFHTTPVersion1_1), errOut);
363 /* Set the body and required header fields. */
364 CFHTTPMessageSetBody(http->request, requestData);
365 CFHTTPMessageSetHeaderFieldValue(http->request, kContentType,
366 kAppOcspRequest);
367 }
368
369 #if 0
370 if (force_nocache) {
371 CFHTTPMessageSetHeaderFieldValue(http->request, CFSTR("Cache-Control"),
372 CFSTR("no-cache"));
373 }
374 #endif
375
376 result = asynchttp_request(NULL, http);
377
378 errOut:
379 CFReleaseSafe(getURL);
380
381 return result;
382 }
383
384
385 static void asynchttp_timer_proc(CFRunLoopTimerRef timer, void *info) {
386 asynchttp_t *http = (asynchttp_t *)info;
387 CFStringRef req_meth = http->request ? CFHTTPMessageCopyRequestMethod(http->request) : NULL;
388 CFURLRef req_url = http->request ? CFHTTPMessageCopyRequestURL(http->request) : NULL;
389 secdebug("http", "Timeout during %@ %@.", req_meth, req_url);
390 /* TODO: Add logging of url that timed out. */
391 //asl_log(NULL, NULL, ASL_LEVEL_NOTICE, "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->request);
401 CFReleaseNull(http->response);
402 CFReleaseNull(http->data);
403 CFReleaseNull(http->stream);
404 CFReleaseNull(http->source);
405 if (http->timer) {
406 CFRunLoopTimerInvalidate(http->timer);
407 CFReleaseNull(http->timer);
408 }
409 }
410 }
411
412 /* Return true, iff we didn't schedule any work, return false if we did. */
413 bool asynchttp_request(CFHTTPMessageRef request, asynchttp_t *http) {
414 secdebug("http", "request %@", request);
415 if (request) {
416 http->request = request;
417 CFRetain(request);
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 CFRunLoopTimerContext tctx = { .info = http };
426 http->timer = CFRunLoopTimerCreate(kCFAllocatorDefault,
427 CFAbsoluteTimeGetCurrent() + STREAM_TIMEOUT,
428 0, 0, 0, asynchttp_timer_proc, &tctx);
429 if (http->timer == NULL) {
430 asl_log(NULL, NULL, ASL_LEVEL_ERR, "FATAL: failed to create timer.");
431 } else {
432 CFRunLoopAddTimer(CFRunLoopGetCurrent(), http->timer,
433 kCFRunLoopDefaultMode);
434 }
435
436 /* Set up possible proxy info */
437 CFDictionaryRef proxyDict = CFNetworkCopySystemProxySettings();
438 if (proxyDict) {
439 CFReadStreamSetProperty(http->stream, kCFStreamPropertyHTTPProxy, proxyDict);
440 CFRelease(proxyDict);
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 CFReadStreamScheduleWithRunLoop(http->stream, CFRunLoopGetCurrent(),
452 kCFRunLoopCommonModes);
453 CFReadStreamOpen(http->stream);
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 }