/core/externals/update-engine/externals/gdata-objectivec-client/Source/HTTPFetcher/GTMHTTPFetcherLogging.m
Objective C | 1134 lines | 792 code | 166 blank | 176 comment | 173 complexity | 2dee050b283afad69515f4ce8d46480a MD5 | raw file
1/* Copyright (c) 2010 Google Inc. 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16#if !STRIP_GTM_FETCH_LOGGING 17 18#include <sys/stat.h> 19#include <unistd.h> 20 21#import "GTMHTTPFetcherLogging.h" 22 23// Sensitive credential strings are replaced in logs with _snip_ 24// 25// Apps that must see the contents of sensitive tokens can set this to 1 26#ifndef SKIP_GTM_FETCH_LOGGING_SNIPPING 27#define SKIP_GTM_FETCH_LOGGING_SNIPPING 0 28#endif 29 30// If GTMReadMonitorInputStream is available, it can be used for 31// capturing uploaded streams of data 32// 33// We locally declare methods of GTMReadMonitorInputStream so we 34// do not need to import the header, as some projects may not have it available 35#ifndef GTM_NSSTREAM_DELEGATE 36@interface GTMReadMonitorInputStream : NSInputStream 37+ (id)inputStreamWithStream:(NSInputStream *)input; 38@property (assign) id readDelegate; 39@property (assign) SEL readSelector; 40@property (retain) NSArray *runLoopModes; 41@end 42#endif 43 44// If GTMNSJSONSerialization is available, it is used for formatting JSON 45#if (TARGET_OS_MAC && !TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED < 1070)) || \ 46 (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED < 50000)) 47@interface GTMNSJSONSerialization : NSObject 48+ (NSData *)dataWithJSONObject:(id)obj options:(NSUInteger)opt error:(NSError **)error; 49+ (id)JSONObjectWithData:(NSData *)data options:(NSUInteger)opt error:(NSError **)error; 50@end 51#endif 52 53// Otherwise, if SBJSON is available, it is used for formatting JSON 54@interface GTMFetcherSBJSON 55- (void)setHumanReadable:(BOOL)flag; 56- (NSString*)stringWithObject:(id)value error:(NSError**)error; 57- (id)objectWithString:(NSString*)jsonrep error:(NSError**)error; 58@end 59 60@interface GTMHTTPFetcher (GTMHTTPFetcherLoggingUtilities) 61+ (NSString *)headersStringForDictionary:(NSDictionary *)dict; 62 63- (void)inputStream:(GTMReadMonitorInputStream *)stream 64 readIntoBuffer:(void *)buffer 65 length:(NSUInteger)length; 66 67// internal file utilities for logging 68+ (BOOL)fileOrDirExistsAtPath:(NSString *)path; 69+ (BOOL)makeDirectoryUpToPath:(NSString *)path; 70+ (BOOL)removeItemAtPath:(NSString *)path; 71+ (BOOL)createSymbolicLinkAtPath:(NSString *)newPath 72 withDestinationPath:(NSString *)targetPath; 73 74+ (NSString *)snipSubstringOfString:(NSString *)originalStr 75 betweenStartString:(NSString *)startStr 76 endString:(NSString *)endStr; 77 78+ (id)JSONObjectWithData:(NSData *)data; 79+ (id)stringWithJSONObject:(id)obj; 80@end 81 82@implementation GTMHTTPFetcher (GTMHTTPFetcherLogging) 83 84// fetchers come and fetchers go, but statics are forever 85static BOOL gIsLoggingEnabled = NO; 86static BOOL gIsLoggingToFile = YES; 87static NSString *gLoggingDirectoryPath = nil; 88static NSString *gLoggingDateStamp = nil; 89static NSString* gLoggingProcessName = nil; 90 91+ (void)setLoggingDirectory:(NSString *)path { 92 [gLoggingDirectoryPath autorelease]; 93 gLoggingDirectoryPath = [path copy]; 94} 95 96+ (NSString *)loggingDirectory { 97 98 if (!gLoggingDirectoryPath) { 99 NSArray *arr = nil; 100#if GTM_IPHONE && TARGET_IPHONE_SIMULATOR 101 // default to a directory called GTMHTTPDebugLogs into a sandbox-safe 102 // directory that a developer can find easily, the application home 103 arr = [NSArray arrayWithObject:NSHomeDirectory()]; 104#elif GTM_IPHONE 105 // Neither ~/Desktop nor ~/Home is writable on an actual iPhone device. 106 // Put it in ~/Documents. 107 arr = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, 108 NSUserDomainMask, YES); 109#else 110 // default to a directory called GTMHTTPDebugLogs in the desktop folder 111 arr = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, 112 NSUserDomainMask, YES); 113#endif 114 115 if ([arr count] > 0) { 116 NSString *const kGTMLogFolderName = @"GTMHTTPDebugLogs"; 117 118 NSString *desktopPath = [arr objectAtIndex:0]; 119 NSString *logsFolderPath = [desktopPath stringByAppendingPathComponent:kGTMLogFolderName]; 120 121 BOOL doesFolderExist = [[self class] fileOrDirExistsAtPath:logsFolderPath]; 122 123 if (!doesFolderExist) { 124 // make the directory 125 doesFolderExist = [self makeDirectoryUpToPath:logsFolderPath]; 126 } 127 128 if (doesFolderExist) { 129 // it's there; store it in the global 130 gLoggingDirectoryPath = [logsFolderPath copy]; 131 } 132 } 133 } 134 return gLoggingDirectoryPath; 135} 136 137+ (void)setLoggingEnabled:(BOOL)flag { 138 gIsLoggingEnabled = flag; 139} 140 141+ (BOOL)isLoggingEnabled { 142 return gIsLoggingEnabled; 143} 144 145+ (void)setLoggingToFileEnabled:(BOOL)flag { 146 gIsLoggingToFile = flag; 147} 148 149+ (BOOL)isLoggingToFileEnabled { 150 return gIsLoggingToFile; 151} 152 153+ (void)setLoggingProcessName:(NSString *)str { 154 [gLoggingProcessName release]; 155 gLoggingProcessName = [str copy]; 156} 157 158+ (NSString *)loggingProcessName { 159 160 // get the process name (once per run) replacing spaces with underscores 161 if (!gLoggingProcessName) { 162 163 NSString *procName = [[NSProcessInfo processInfo] processName]; 164 NSMutableString *loggingProcessName; 165 loggingProcessName = [[NSMutableString alloc] initWithString:procName]; 166 167 [loggingProcessName replaceOccurrencesOfString:@" " 168 withString:@"_" 169 options:0 170 range:NSMakeRange(0, [loggingProcessName length])]; 171 gLoggingProcessName = loggingProcessName; 172 } 173 return gLoggingProcessName; 174} 175 176+ (void)setLoggingDateStamp:(NSString *)str { 177 [gLoggingDateStamp release]; 178 gLoggingDateStamp = [str copy]; 179} 180 181+ (NSString *)loggingDateStamp { 182 // we'll pick one date stamp per run, so a run that starts at a later second 183 // will get a unique results html file 184 if (!gLoggingDateStamp) { 185 // produce a string like 08-21_01-41-23PM 186 187 NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease]; 188 [formatter setFormatterBehavior:NSDateFormatterBehavior10_4]; 189 [formatter setDateFormat:@"M-dd_hh-mm-ssa"]; 190 191 gLoggingDateStamp = [[formatter stringFromDate:[NSDate date]] retain] ; 192 } 193 return gLoggingDateStamp; 194} 195 196+ (NSString *)processNameLogPrefix { 197 static NSString *prefix = nil; 198 if (!prefix) { 199 NSString *processName = [[self class] loggingProcessName]; 200 prefix = [[NSString alloc] initWithFormat:@"%@_log_", processName]; 201 } 202 return prefix; 203} 204 205+ (NSString *)symlinkNameSuffix { 206 return @"_log_newest.html"; 207} 208 209+ (NSString *)htmlFileName { 210 return @"aperçu_http_log.html"; 211} 212 213// formattedStringFromData returns a prettyprinted string for XML or JSON input, 214// and a plain string for other input data 215- (NSString *)formattedStringFromData:(NSData *)inputData 216 contentType:(NSString *)contentType 217 JSON:(NSDictionary **)outJSON { 218 if (inputData == nil) return nil; 219 220 // if the content type is JSON and we have the parsing class available, 221 // use that 222 if ([contentType hasPrefix:@"application/json"] 223 && [inputData length] > 5) { 224 // convert from JSON string to NSObjects and back to a formatted string 225 NSMutableDictionary *obj = [[self class] JSONObjectWithData:inputData]; 226 if (obj) { 227 if (outJSON) *outJSON = obj; 228 if ([obj isKindOfClass:[NSMutableDictionary class]]) { 229 // for security and privacy, omit OAuth 2 response access and refresh 230 // tokens 231 if ([obj valueForKey:@"refresh_token"] != nil) { 232 [obj setObject:@"_snip_" forKey:@"refresh_token"]; 233 } 234 if ([obj valueForKey:@"access_token"] != nil) { 235 [obj setObject:@"_snip_" forKey:@"access_token"]; 236 } 237 } 238 NSString *formatted = [[self class] stringWithJSONObject:obj]; 239 if (formatted) return formatted; 240 } 241 } 242 243#if !GTM_FOUNDATION_ONLY && !GTM_SKIP_LOG_XMLFORMAT 244 // verify that this data starts with the bytes indicating XML 245 246 NSString *const kXMLLintPath = @"/usr/bin/xmllint"; 247 static BOOL hasCheckedAvailability = NO; 248 static BOOL isXMLLintAvailable; 249 250 if (!hasCheckedAvailability) { 251 isXMLLintAvailable = [[self class] fileOrDirExistsAtPath:kXMLLintPath]; 252 hasCheckedAvailability = YES; 253 } 254 255 if (isXMLLintAvailable 256 && [inputData length] > 5 257 && strncmp([inputData bytes], "<?xml", 5) == 0) { 258 259 // call xmllint to format the data 260 NSTask *task = [[[NSTask alloc] init] autorelease]; 261 [task setLaunchPath:kXMLLintPath]; 262 263 // use the dash argument to specify stdin as the source file 264 [task setArguments:[NSArray arrayWithObjects:@"--format", @"-", nil]]; 265 [task setEnvironment:[NSDictionary dictionary]]; 266 267 NSPipe *inputPipe = [NSPipe pipe]; 268 NSPipe *outputPipe = [NSPipe pipe]; 269 [task setStandardInput:inputPipe]; 270 [task setStandardOutput:outputPipe]; 271 272 [task launch]; 273 274 [[inputPipe fileHandleForWriting] writeData:inputData]; 275 [[inputPipe fileHandleForWriting] closeFile]; 276 277 // drain the stdout before waiting for the task to exit 278 NSData *formattedData = [[outputPipe fileHandleForReading] readDataToEndOfFile]; 279 280 [task waitUntilExit]; 281 282 int status = [task terminationStatus]; 283 if (status == 0 && [formattedData length] > 0) { 284 // success 285 inputData = formattedData; 286 } 287 } 288#else 289 // we can't call external tasks on the iPhone; leave the XML unformatted 290#endif 291 292 NSString *dataStr = [[[NSString alloc] initWithData:inputData 293 encoding:NSUTF8StringEncoding] autorelease]; 294 return dataStr; 295} 296 297- (void)setupStreamLogging { 298 // if logging is enabled, it needs a buffer to accumulate data from any 299 // NSInputStream used for uploading. Logging will wrap the input 300 // stream with a stream that lets us keep a copy the data being read. 301 if ([GTMHTTPFetcher isLoggingEnabled] 302 && loggedStreamData_ == nil 303 && postStream_ != nil) { 304 loggedStreamData_ = [[NSMutableData alloc] init]; 305 306 BOOL didCapture = [self logCapturePostStream]; 307 if (!didCapture) { 308 // upload stream logging requires the class 309 // GTMReadMonitorInputStream be available 310 NSString const *str = @"<<Uploaded stream log unavailable without GTMReadMonitorInputStream>>"; 311 [loggedStreamData_ setData:[str dataUsingEncoding:NSUTF8StringEncoding]]; 312 } 313 } 314} 315 316- (void)setLogRequestBody:(NSString *)bodyString { 317 @synchronized(self) { 318 [logRequestBody_ release]; 319 logRequestBody_ = [bodyString copy]; 320 } 321} 322 323- (NSString *)logRequestBody { 324 @synchronized(self) { 325 return logRequestBody_; 326 } 327} 328 329- (void)setLogResponseBody:(NSString *)bodyString { 330 @synchronized(self) { 331 [logResponseBody_ release]; 332 logResponseBody_ = [bodyString copy]; 333 } 334} 335 336- (NSString *)logResponseBody { 337 @synchronized(self) { 338 return logResponseBody_; 339 } 340} 341 342- (void)setShouldDeferResponseBodyLogging:(BOOL)flag { 343 @synchronized(self) { 344 if (flag != shouldDeferResponseBodyLogging_) { 345 shouldDeferResponseBodyLogging_ = flag; 346 if (!flag && !hasLoggedError_) { 347 [self performSelectorOnMainThread:@selector(logFetchWithError:) 348 withObject:nil 349 waitUntilDone:NO]; 350 } 351 } 352 } 353} 354 355- (BOOL)shouldDeferResponseBodyLogging { 356 @synchronized(self) { 357 return shouldDeferResponseBodyLogging_; 358 } 359} 360 361// stringFromStreamData creates a string given the supplied data 362// 363// If NSString can create a UTF-8 string from the data, then that is returned. 364// 365// Otherwise, this routine tries to find a MIME boundary at the beginning of 366// the data block, and uses that to break up the data into parts. Each part 367// will be used to try to make a UTF-8 string. For parts that fail, a 368// replacement string showing the part header and <<n bytes>> is supplied 369// in place of the binary data. 370 371- (NSString *)stringFromStreamData:(NSData *)data 372 contentType:(NSString *)contentType { 373 374 if (data == nil) return nil; 375 376 // optimistically, see if the whole data block is UTF-8 377 NSString *streamDataStr = [self formattedStringFromData:data 378 contentType:contentType 379 JSON:NULL]; 380 if (streamDataStr) return streamDataStr; 381 382 // Munge a buffer by replacing non-ASCII bytes with underscores, 383 // and turn that munged buffer an NSString. That gives us a string 384 // we can use with NSScanner. 385 NSMutableData *mutableData = [NSMutableData dataWithData:data]; 386 unsigned char *bytes = [mutableData mutableBytes]; 387 388 for (unsigned int idx = 0; idx < [mutableData length]; idx++) { 389 if (bytes[idx] > 0x7F || bytes[idx] == 0) { 390 bytes[idx] = '_'; 391 } 392 } 393 394 NSString *mungedStr = [[[NSString alloc] initWithData:mutableData 395 encoding:NSUTF8StringEncoding] autorelease]; 396 if (mungedStr != nil) { 397 398 // scan for the boundary string 399 NSString *boundary = nil; 400 NSScanner *scanner = [NSScanner scannerWithString:mungedStr]; 401 402 if ([scanner scanUpToString:@"\r\n" intoString:&boundary] 403 && [boundary hasPrefix:@"--"]) { 404 405 // we found a boundary string; use it to divide the string into parts 406 NSArray *mungedParts = [mungedStr componentsSeparatedByString:boundary]; 407 408 // look at each of the munged parts in the original string, and try to 409 // convert those into UTF-8 410 NSMutableArray *origParts = [NSMutableArray array]; 411 NSUInteger offset = 0; 412 for (NSString *mungedPart in mungedParts) { 413 NSUInteger partSize = [mungedPart length]; 414 415 NSRange range = NSMakeRange(offset, partSize); 416 NSData *origPartData = [data subdataWithRange:range]; 417 418 NSString *origPartStr = [[[NSString alloc] initWithData:origPartData 419 encoding:NSUTF8StringEncoding] autorelease]; 420 if (origPartStr) { 421 // we could make this original part into UTF-8; use the string 422 [origParts addObject:origPartStr]; 423 } else { 424 // this part can't be made into UTF-8; scan the header, if we can 425 NSString *header = nil; 426 NSScanner *headerScanner = [NSScanner scannerWithString:mungedPart]; 427 if (![headerScanner scanUpToString:@"\r\n\r\n" intoString:&header]) { 428 // we couldn't find a header 429 header = @""; 430 } 431 432 // make a part string with the header and <<n bytes>> 433 NSString *binStr = [NSString stringWithFormat:@"\r%@\r<<%lu bytes>>\r", 434 header, (long)(partSize - [header length])]; 435 [origParts addObject:binStr]; 436 } 437 offset += partSize + [boundary length]; 438 } 439 440 // rejoin the original parts 441 streamDataStr = [origParts componentsJoinedByString:boundary]; 442 } 443 } 444 445 if (!streamDataStr) { 446 // give up; just make a string showing the uploaded bytes 447 streamDataStr = [NSString stringWithFormat:@"<<%u bytes>>", 448 (unsigned int)[data length]]; 449 } 450 return streamDataStr; 451} 452 453// logFetchWithError is called following a successful or failed fetch attempt 454// 455// This method does all the work for appending to and creating log files 456 457- (void)logFetchWithError:(NSError *)error { 458 459 if (![[self class] isLoggingEnabled]) return; 460 461 // TODO: (grobbins) add Javascript to display response data formatted in hex 462 463 NSString *parentDir = [[self class] loggingDirectory]; 464 NSString *processName = [[self class] loggingProcessName]; 465 NSString *dateStamp = [[self class] loggingDateStamp]; 466 NSString *logNamePrefix = [[self class] processNameLogPrefix]; 467 468 // make a directory for this run's logs, like 469 // SyncProto_logs_10-16_01-56-58PM 470 NSString *dirName = [NSString stringWithFormat:@"%@%@", 471 logNamePrefix, dateStamp]; 472 NSString *logDirectory = [parentDir stringByAppendingPathComponent:dirName]; 473 474 if (gIsLoggingToFile) { 475 // be sure that the first time this app runs, it's not writing to 476 // a preexisting folder 477 static BOOL shouldReuseFolder = NO; 478 if (!shouldReuseFolder) { 479 shouldReuseFolder = YES; 480 NSString *origLogDir = logDirectory; 481 for (int ctr = 2; ctr < 20; ctr++) { 482 if (![[self class] fileOrDirExistsAtPath:logDirectory]) break; 483 484 // append a digit 485 logDirectory = [origLogDir stringByAppendingFormat:@"_%d", ctr]; 486 } 487 } 488 if (![[self class] makeDirectoryUpToPath:logDirectory]) return; 489 } 490 // each response's NSData goes into its own xml or txt file, though all 491 // responses for this run of the app share a main html file. This 492 // counter tracks all fetch responses for this run of the app. 493 // 494 // we'll use a local variable since this routine may be reentered while 495 // waiting for XML formatting to be completed by an external task 496 static int zResponseCounter = 0; 497 int responseCounter = ++zResponseCounter; 498 499 // file name for an image data file 500 NSString *responseDataFileName = nil; 501 NSUInteger responseDataLength; 502 if (downloadFileHandle_) { 503 responseDataLength = (NSUInteger) [downloadFileHandle_ offsetInFile]; 504 } else { 505 responseDataLength = [downloadedData_ length]; 506 } 507 508 NSURLResponse *response = [self response]; 509 NSDictionary *responseHeaders = [self responseHeaders]; 510 511 NSString *responseBaseName = nil; 512 NSString *responseDataStr = nil; 513 NSDictionary *responseJSON = nil; 514 515 // if there's response data, decide what kind of file to put it in based 516 // on the first bytes of the file or on the mime type supplied by the server 517 NSString *responseMIMEType = [response MIMEType]; 518 BOOL isResponseImage = NO; 519 NSData *dataToWrite = nil; 520 521 if (responseDataLength > 0) { 522 NSString *responseDataExtn = nil; 523 524 // generate a response file base name like 525 responseBaseName = [NSString stringWithFormat:@"fetch_%d_response", 526 responseCounter]; 527 528 NSString *responseType = [responseHeaders valueForKey:@"Content-Type"]; 529 responseDataStr = [self formattedStringFromData:downloadedData_ 530 contentType:responseType 531 JSON:&responseJSON]; 532 if (responseDataStr) { 533 // we were able to make a UTF-8 string from the response data 534 if ([responseMIMEType isEqual:@"application/atom+xml"] 535 || [responseMIMEType hasSuffix:@"/xml"]) { 536 responseDataExtn = @"xml"; 537 dataToWrite = [responseDataStr dataUsingEncoding:NSUTF8StringEncoding]; 538 } 539 } else if ([responseMIMEType isEqual:@"image/jpeg"]) { 540 responseDataExtn = @"jpg"; 541 dataToWrite = downloadedData_; 542 isResponseImage = YES; 543 } else if ([responseMIMEType isEqual:@"image/gif"]) { 544 responseDataExtn = @"gif"; 545 dataToWrite = downloadedData_; 546 isResponseImage = YES; 547 } else if ([responseMIMEType isEqual:@"image/png"]) { 548 responseDataExtn = @"png"; 549 dataToWrite = downloadedData_; 550 isResponseImage = YES; 551 } else { 552 // add more non-text types here 553 } 554 555 // if we have an extension, save the raw data in a file with that 556 // extension 557 if (responseDataExtn && dataToWrite) { 558 responseDataFileName = [responseBaseName stringByAppendingPathExtension:responseDataExtn]; 559 NSString *responseDataFilePath = [logDirectory stringByAppendingPathComponent:responseDataFileName]; 560 561 NSError *downloadedError = nil; 562 if (gIsLoggingToFile 563 && ![dataToWrite writeToFile:responseDataFilePath 564 options:0 565 error:&downloadedError]) { 566 NSLog(@"%@ logging write error:%@ (%@)", 567 [self class], downloadedError, responseDataFileName); 568 } 569 } 570 } 571 572 // we'll have one main html file per run of the app 573 NSString *htmlName = [[self class] htmlFileName]; 574 NSString *htmlPath =[logDirectory stringByAppendingPathComponent:htmlName]; 575 576 // if the html file exists (from logging previous fetches) we don't need 577 // to re-write the header or the scripts 578 BOOL didFileExist = [[self class] fileOrDirExistsAtPath:htmlPath]; 579 580 NSMutableString* outputHTML = [NSMutableString string]; 581 NSURLRequest *request = [self mutableRequest]; 582 583 // we need a header to say we'll have UTF-8 text 584 if (!didFileExist) { 585 [outputHTML appendFormat:@"<html><head><meta http-equiv=\"content-type\" " 586 "content=\"text/html; charset=UTF-8\"><title>%@ HTTP fetch log %@</title>", 587 processName, dateStamp]; 588 } 589 590 // now write the visible html elements 591 NSString *copyableFileName = [NSString stringWithFormat:@"fetch_%d.txt", 592 responseCounter]; 593 594 // write the date & time, the comment, and the link to the plain-text 595 // (copyable) log 596 NSString *const dateLineFormat = @"<b>%@ "; 597 [outputHTML appendFormat:dateLineFormat, [NSDate date]]; 598 599 NSString *comment = [self comment]; 600 if (comment) { 601 NSString *const commentFormat = @"%@ "; 602 [outputHTML appendFormat:commentFormat, comment]; 603 } 604 605 NSString *const reqRespFormat = @"</b><a href='%@'><i>request/response log</i></a><br>"; 606 [outputHTML appendFormat:reqRespFormat, copyableFileName]; 607 608 // write the request URL 609 NSString *requestMethod = [request HTTPMethod]; 610 NSURL *requestURL = [request URL]; 611 612 NSURL *redirectedFromHost = [[[redirectedFromURL_ host] copy] autorelease]; 613 // Save the request URL for next time in case this redirects. 614 [redirectedFromURL_ release]; 615 redirectedFromURL_ = [requestURL copy]; 616 if (redirectedFromHost) { 617 [outputHTML appendFormat:@"<FONT COLOR='#990066'><i>redirected from %@</i></FONT><br>", 618 redirectedFromHost]; 619 } 620 621 [outputHTML appendFormat:@"<b>request:</b> %@ <code>%@</code><br>\n", 622 requestMethod, requestURL]; 623 624 // write the request headers 625 NSDictionary *requestHeaders = [request allHTTPHeaderFields]; 626 NSUInteger numberOfRequestHeaders = [requestHeaders count]; 627 if (numberOfRequestHeaders > 0) { 628 // Indicate if the request is authorized; warn if the request is 629 // authorized but non-SSL 630 NSString *auth = [requestHeaders objectForKey:@"Authorization"]; 631 NSString *headerDetails = @""; 632 if (auth) { 633 headerDetails = @" <i>authorized</i>"; 634 BOOL isInsecure = [[requestURL scheme] isEqual:@"http"]; 635 if (isInsecure) { 636 headerDetails = @" <i>authorized, non-SSL</i>" 637 "<FONT COLOR='#FF00FF'> ⚠</FONT> "; // 26A0 = ? 638 } 639 } 640 NSString *cookiesHdr = [requestHeaders objectForKey:@"Cookie"]; 641 if (cookiesHdr) { 642 headerDetails = [headerDetails stringByAppendingString: 643 @" <i>cookies</i>"]; 644 } 645 NSString *matchHdr = [requestHeaders objectForKey:@"If-Match"]; 646 if (matchHdr) { 647 headerDetails = [headerDetails stringByAppendingString: 648 @" <i>if-match</i>"]; 649 } 650 matchHdr = [requestHeaders objectForKey:@"If-None-Match"]; 651 if (matchHdr) { 652 headerDetails = [headerDetails stringByAppendingString: 653 @" <i>if-none-match</i>"]; 654 } 655 [outputHTML appendFormat:@" headers: %d %@<br>", 656 (int)numberOfRequestHeaders, headerDetails]; 657 } else { 658 [outputHTML appendFormat:@" headers: none<br>"]; 659 } 660 661 // write the request post data, toggleable 662 NSData *postData; 663 if (loggedStreamData_) { 664 postData = loggedStreamData_; 665 } else if (postData_) { 666 postData = postData_; 667 } else { 668 postData = [request_ HTTPBody]; 669 } 670 671 NSString *postDataStr = nil; 672 NSUInteger postDataLength = [postData length]; 673 NSString *postType = [requestHeaders valueForKey:@"Content-Type"]; 674 675 if (postDataLength > 0) { 676 [outputHTML appendFormat:@" data: %d bytes, <code>%@</code><br>\n", 677 (int)postDataLength, postType ? postType : @"<no type>"]; 678 679 if (logRequestBody_) { 680 postDataStr = [[logRequestBody_ copy] autorelease]; 681 [logRequestBody_ release]; 682 logRequestBody_ = nil; 683 } else { 684 postDataStr = [self stringFromStreamData:postData 685 contentType:postType]; 686 if (postDataStr) { 687 // remove OAuth 2 client secret and refresh token 688 postDataStr = [[self class] snipSubstringOfString:postDataStr 689 betweenStartString:@"client_secret=" 690 endString:@"&"]; 691 692 postDataStr = [[self class] snipSubstringOfString:postDataStr 693 betweenStartString:@"refresh_token=" 694 endString:@"&"]; 695 696 // remove ClientLogin password 697 postDataStr = [[self class] snipSubstringOfString:postDataStr 698 betweenStartString:@"&Passwd=" 699 endString:@"&"]; 700 } 701 } 702 } else { 703 // no post data 704 } 705 706 // write the response status, MIME type, URL 707 NSInteger status = [self statusCode]; 708 if (response) { 709 NSString *statusString = @""; 710 if (status != 0) { 711 if (status == 200 || status == 201) { 712 statusString = [NSString stringWithFormat:@"%ld", (long)status]; 713 714 // report any JSON-RPC error 715 if ([responseJSON isKindOfClass:[NSDictionary class]]) { 716 NSDictionary *jsonError = [responseJSON objectForKey:@"error"]; 717 if ([jsonError isKindOfClass:[NSDictionary class]]) { 718 NSString *jsonCode = [[jsonError valueForKey:@"code"] description]; 719 NSString *jsonMessage = [jsonError valueForKey:@"message"]; 720 if (jsonCode || jsonMessage) { 721 NSString *const jsonErrFmt = @" <i>JSON error:</i> <FONT" 722 @" COLOR='#FF00FF'>%@ %@ ⚑</FONT>"; // 2691 = ? 723 statusString = [statusString stringByAppendingFormat:jsonErrFmt, 724 jsonCode ? jsonCode : @"", 725 jsonMessage ? jsonMessage : @""]; 726 } 727 } 728 } 729 } else { 730 // purple for anything other than 200 or 201 731 NSString *flag = (status >= 400 ? @" ⚑" : @""); // 2691 = ? 732 NSString *const statusFormat = @"<FONT COLOR='#FF00FF'>%ld %@</FONT>"; 733 statusString = [NSString stringWithFormat:statusFormat, 734 (long)status, flag]; 735 } 736 } 737 738 // show the response URL only if it's different from the request URL 739 NSString *responseURLStr = @""; 740 NSURL *responseURL = [response URL]; 741 742 if (responseURL && ![responseURL isEqual:[request URL]]) { 743 NSString *const responseURLFormat = @"<FONT COLOR='#FF00FF'>response URL:" 744 "</FONT> <code>%@</code><br>\n"; 745 responseURLStr = [NSString stringWithFormat:responseURLFormat, 746 [responseURL absoluteString]]; 747 } 748 749 [outputHTML appendFormat:@"<b>response:</b> status %@<br>\n%@", 750 statusString, responseURLStr]; 751 752 // Write the response headers 753 NSUInteger numberOfResponseHeaders = [responseHeaders count]; 754 if (numberOfResponseHeaders > 0) { 755 // Indicate if the server is setting cookies 756 NSString *cookiesSet = [responseHeaders valueForKey:@"Set-Cookie"]; 757 NSString *cookiesStr = (cookiesSet ? @" <FONT COLOR='#990066'>" 758 "<i>sets cookies</i></FONT>" : @""); 759 // Indicate if the server is redirecting 760 NSString *location = [responseHeaders valueForKey:@"Location"]; 761 BOOL isRedirect = (status >= 300 && status <= 399 && location != nil); 762 NSString *redirectsStr = (isRedirect ? @" <FONT COLOR='#990066'>" 763 "<i>redirects</i></FONT>" : @""); 764 765 [outputHTML appendFormat:@" headers: %d %@ %@<br>\n", 766 (int)numberOfResponseHeaders, cookiesStr, redirectsStr]; 767 } else { 768 [outputHTML appendString:@" headers: none<br>\n"]; 769 } 770 } 771 772 // error 773 if (error) { 774 [outputHTML appendFormat:@"<b>Error:</b> %@ <br>\n", [error description]]; 775 } 776 777 // Write the response data 778 if (responseDataFileName) { 779 NSString *escapedResponseFile = [responseDataFileName stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; 780 if (isResponseImage) { 781 // Make a small inline image that links to the full image file 782 [outputHTML appendFormat:@" data: %d bytes, <code>%@</code><br>", 783 (int)responseDataLength, responseMIMEType]; 784 NSString *const fmt = @"<a href=\"%@\"><img src='%@' alt='image'" 785 " style='border:solid thin;max-height:32'></a>\n"; 786 [outputHTML appendFormat:fmt, 787 escapedResponseFile, escapedResponseFile]; 788 } else { 789 // The response data was XML; link to the xml file 790 NSString *const fmt = @" data: %d bytes, <code>" 791 "%@</code> <i><a href=\"%@\">%@</a></i>\n"; 792 [outputHTML appendFormat:fmt, 793 (int)responseDataLength, responseMIMEType, 794 escapedResponseFile, [escapedResponseFile pathExtension]]; 795 } 796 } else { 797 // The response data was not an image; just show the length and MIME type 798 [outputHTML appendFormat:@" data: %d bytes, <code>%@</code>\n", 799 (int)responseDataLength, responseMIMEType]; 800 } 801 802 // Make a single string of the request and response, suitable for copying 803 // to the clipboard and pasting into a bug report 804 NSMutableString *copyable = [NSMutableString string]; 805 if (comment) { 806 [copyable appendFormat:@"%@\n\n", comment]; 807 } 808 [copyable appendFormat:@"%@\n", [NSDate date]]; 809 if (redirectedFromHost) { 810 [copyable appendFormat:@"Redirected from %@\n", redirectedFromHost]; 811 } 812 [copyable appendFormat:@"Request: %@ %@\n", requestMethod, requestURL]; 813 if ([requestHeaders count] > 0) { 814 [copyable appendFormat:@"Request headers:\n%@\n", 815 [[self class] headersStringForDictionary:requestHeaders]]; 816 } 817 818 if (postDataLength > 0) { 819 [copyable appendFormat:@"Request body: (%u bytes)\n", 820 (unsigned int) postDataLength]; 821 if (postDataStr) { 822 [copyable appendFormat:@"%@\n", postDataStr]; 823 } 824 [copyable appendString:@"\n"]; 825 } 826 827 if (response) { 828 [copyable appendFormat:@"Response: status %d\n", (int) status]; 829 [copyable appendFormat:@"Response headers:\n%@\n", 830 [[self class] headersStringForDictionary:responseHeaders]]; 831 [copyable appendFormat:@"Response body: (%u bytes)\n", 832 (unsigned int) responseDataLength]; 833 if (responseDataLength > 0) { 834 if (logResponseBody_) { 835 responseDataStr = [[logResponseBody_ copy] autorelease]; 836 [logResponseBody_ release]; 837 logResponseBody_ = nil; 838 } 839 if (responseDataStr != nil) { 840 [copyable appendFormat:@"%@\n", responseDataStr]; 841 } else if (status >= 400 && [temporaryDownloadPath_ length] > 0) { 842 // Try to read in the saved data, which is probably a server error 843 // message 844 NSStringEncoding enc; 845 responseDataStr = [NSString stringWithContentsOfFile:temporaryDownloadPath_ 846 usedEncoding:&enc 847 error:NULL]; 848 if ([responseDataStr length] > 0) { 849 [copyable appendFormat:@"%@\n", responseDataStr]; 850 } else { 851 [copyable appendFormat:@"<<%u bytes to file>>\n", 852 (unsigned int) responseDataLength]; 853 } 854 } else { 855 // Even though it's redundant, we'll put in text to indicate that all 856 // the bytes are binary 857 [copyable appendFormat:@"<<%u bytes>>\n", 858 (unsigned int) responseDataLength]; 859 } 860 } 861 } 862 863 if (error) { 864 [copyable appendFormat:@"Error: %@\n", error]; 865 } 866 867 // Save to log property before adding the separator 868 self.log = copyable; 869 870 [copyable appendString:@"-----------------------------------------------------------\n"]; 871 872 873 // Write the copyable version to another file (linked to at the top of the 874 // html file, above) 875 // 876 // Ideally, something to just copy this to the clipboard like 877 // <span onCopy='window.event.clipboardData.setData(\"Text\", 878 // \"copyable stuff\");return false;'>Copy here.</span>" 879 // would work everywhere, but it only works in Safari as of 8/2010 880 if (gIsLoggingToFile) { 881 NSString *copyablePath = [logDirectory stringByAppendingPathComponent:copyableFileName]; 882 NSError *copyableError = nil; 883 if (![copyable writeToFile:copyablePath 884 atomically:NO 885 encoding:NSUTF8StringEncoding 886 error:©ableError]) { 887 // Error writing to file 888 NSLog(@"%@ logging write error:%@ (%@)", 889 [self class], copyableError, copyablePath); 890 } 891 892 [outputHTML appendString:@"<br><hr><p>"]; 893 894 // Append the HTML to the main output file 895 const char* htmlBytes = [outputHTML UTF8String]; 896 NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:htmlPath 897 append:YES]; 898 [stream open]; 899 [stream write:(const uint8_t *) htmlBytes maxLength:strlen(htmlBytes)]; 900 [stream close]; 901 902 // Make a symlink to the latest html 903 NSString *const symlinkNameSuffix = [[self class] symlinkNameSuffix]; 904 NSString *symlinkName = [processName stringByAppendingString:symlinkNameSuffix]; 905 NSString *symlinkPath = [parentDir stringByAppendingPathComponent:symlinkName]; 906 907 [[self class] removeItemAtPath:symlinkPath]; 908 [[self class] createSymbolicLinkAtPath:symlinkPath 909 withDestinationPath:htmlPath]; 910 911#if GTM_IPHONE 912 static BOOL gReportedLoggingPath = NO; 913 if (!gReportedLoggingPath) { 914 gReportedLoggingPath = YES; 915 NSLog(@"GTMHTTPFetcher logging to \"%@\"", parentDir); 916 } 917#endif 918 } 919} 920 921- (BOOL)logCapturePostStream { 922 // This is called when beginning a fetch. The caller should have already 923 // verified that logging is enabled, and should have allocated 924 // loggedStreamData_ as a mutable object. 925 926 // If the class GTMReadMonitorInputStream is not available, bail now, since 927 // we cannot capture this upload stream 928 Class monitorClass = NSClassFromString(@"GTMReadMonitorInputStream"); 929 if (!monitorClass) return NO; 930 931 // If we're logging, we need to wrap the upload stream with our monitor 932 // stream that will call us back with the bytes being read from the stream 933 934 // Our wrapper will retain the old post stream 935 [postStream_ autorelease]; 936 937 postStream_ = [monitorClass inputStreamWithStream:postStream_]; 938 [postStream_ retain]; 939 940 [(GTMReadMonitorInputStream *)postStream_ setReadDelegate:self]; 941 [(GTMReadMonitorInputStream *)postStream_ setRunLoopModes:[self runLoopModes]]; 942 943 SEL readSel = @selector(inputStream:readIntoBuffer:length:); 944 [(GTMReadMonitorInputStream *)postStream_ setReadSelector:readSel]; 945 946 return YES; 947} 948 949@end 950 951@implementation GTMHTTPFetcher (GTMHTTPFetcherLoggingUtilities) 952 953- (void)inputStream:(GTMReadMonitorInputStream *)stream 954 readIntoBuffer:(void *)buffer 955 length:(NSUInteger)length { 956 // append the captured data 957 [loggedStreamData_ appendBytes:buffer length:length]; 958} 959 960#pragma mark Internal file routines 961 962// We implement plain Unix versions of NSFileManager methods to avoid 963// NSFileManager's issues with being used from multiple threads 964 965+ (BOOL)fileOrDirExistsAtPath:(NSString *)path { 966 struct stat buffer; 967 int result = stat([path fileSystemRepresentation], &buffer); 968 return (result == 0); 969} 970 971+ (BOOL)makeDirectoryUpToPath:(NSString *)path { 972 int result = 0; 973 974 // Recursively create the parent directory of the requested path 975 NSString *parent = [path stringByDeletingLastPathComponent]; 976 if (![self fileOrDirExistsAtPath:parent]) { 977 result = [self makeDirectoryUpToPath:parent]; 978 } 979 980 // Make the leaf directory 981 if (result == 0 && ![self fileOrDirExistsAtPath:path]) { 982 result = mkdir([path fileSystemRepresentation], S_IRWXU); // RWX for owner 983 } 984 return (result == 0); 985} 986 987+ (BOOL)removeItemAtPath:(NSString *)path { 988 int result = unlink([path fileSystemRepresentation]); 989 return (result == 0); 990} 991 992+ (BOOL)createSymbolicLinkAtPath:(NSString *)newPath 993 withDestinationPath:(NSString *)targetPath { 994 int result = symlink([targetPath fileSystemRepresentation], 995 [newPath fileSystemRepresentation]); 996 return (result == 0); 997} 998 999#pragma mark Fomatting Utilities 1000 1001+ (NSString *)snipSubstringOfString:(NSString *)originalStr 1002 betweenStartString:(NSString *)startStr 1003 endString:(NSString *)endStr { 1004#if SKIP_GTM_FETCH_LOGGING_SNIPPING 1005 return originalStr; 1006#else 1007 if (originalStr == nil) return nil; 1008 1009 // Find the start string, and replace everything between it 1010 // and the end string (or the end of the original string) with "_snip_" 1011 NSRange startRange = [originalStr rangeOfString:startStr]; 1012 if (startRange.location == NSNotFound) return originalStr; 1013 1014 // We found the start string 1015 NSUInteger originalLength = [originalStr length]; 1016 NSUInteger startOfTarget = NSMaxRange(startRange); 1017 NSRange targetAndRest = NSMakeRange(startOfTarget, 1018 originalLength - startOfTarget); 1019 NSRange endRange = [originalStr rangeOfString:endStr 1020 options:0 1021 range:targetAndRest]; 1022 NSRange replaceRange; 1023 if (endRange.location == NSNotFound) { 1024 // Found no end marker so replace to end of string 1025 replaceRange = targetAndRest; 1026 } else { 1027 // Replace up to the endStr 1028 replaceRange = NSMakeRange(startOfTarget, 1029 endRange.location - startOfTarget); 1030 } 1031 1032 NSString *result = [originalStr stringByReplacingCharactersInRange:replaceRange 1033 withString:@"_snip_"]; 1034 return result; 1035#endif // SKIP_GTM_FETCH_LOGGING_SNIPPING 1036} 1037 1038+ (NSString *)headersStringForDictionary:(NSDictionary *)dict { 1039 // Format the dictionary in http header style, like 1040 // Accept: application/json 1041 // Cache-Control: no-cache 1042 // Content-Type: application/json; charset=utf-8 1043 // 1044 // Pad the key names, but not beyond 16 chars, since long custom header 1045 // keys just create too much whitespace 1046 NSArray *keys = [[dict allKeys] sortedArrayUsingSelector:@selector(compare:)]; 1047 1048 NSMutableString *str = [NSMutableString string]; 1049 for (NSString *key in keys) { 1050 NSString *value = [dict valueForKey:key]; 1051 if ([key isEqual:@"Authorization"]) { 1052 // Remove OAuth 1 token 1053 value = [[self class] snipSubstringOfString:value 1054 betweenStartString:@"oauth_token=\"" 1055 endString:@"\""]; 1056 1057 // Remove OAuth 2 bearer token (draft 16, and older form) 1058 value = [[self class] snipSubstringOfString:value 1059 betweenStartString:@"Bearer " 1060 endString:@"\n"]; 1061 value = [[self class] snipSubstringOfString:value 1062 betweenStartString:@"OAuth " 1063 endString:@"\n"]; 1064 1065 // Remove Google ClientLogin 1066 value = [[self class] snipSubstringOfString:value 1067 betweenStartString:@"GoogleLogin auth=" 1068 endString:@"\n"]; 1069 } 1070 [str appendFormat:@" %@: %@\n", key, value]; 1071 } 1072 return str; 1073} 1074 1075+ (id)JSONObjectWithData:(NSData *)data { 1076 Class serializer = NSClassFromString(@"NSJSONSerialization"); 1077 if (serializer) { 1078 const NSUInteger kOpts = (1UL << 0); // NSJSONReadingMutableContainers 1079 NSMutableDictionary *obj; 1080 obj = [serializer JSONObjectWithData:data 1081 options:kOpts 1082 error:NULL]; 1083 return obj; 1084 } else { 1085 // Try SBJsonParser or SBJSON 1086 Class jsonParseClass = NSClassFromString(@"SBJsonParser"); 1087 if (!jsonParseClass) { 1088 jsonParseClass = NSClassFromString(@"SBJSON"); 1089 } 1090 if (jsonParseClass) { 1091 GTMFetcherSBJSON *parser = [[[jsonParseClass alloc] init] autorelease]; 1092 NSString *jsonStr = [[[NSString alloc] initWithData:data 1093 encoding:NSUTF8StringEncoding] autorelease]; 1094 if (jsonStr) { 1095 NSMutableDictionary *obj = [parser objectWithString:jsonStr error:NULL]; 1096 return obj; 1097 } 1098 } 1099 } 1100 return nil; 1101} 1102 1103+ (id)stringWithJSONObject:(id)obj { 1104 Class serializer = NSClassFromString(@"NSJSONSerialization"); 1105 if (serializer) { 1106 const NSUInteger kOpts = (1UL << 0); // NSJSONWritingPrettyPrinted 1107 NSData *data; 1108 data = [serializer dataWithJSONObject:obj 1109 options:kOpts 1110 error:NULL]; 1111 if (data) { 1112 NSString *jsonStr = [[[NSString alloc] initWithData:data 1113 encoding:NSUTF8StringEncoding] autorelease]; 1114 return jsonStr; 1115 } 1116 } else { 1117 // Try SBJsonParser or SBJSON 1118 Class jsonWriterClass = NSClassFromString(@"SBJsonWriter"); 1119 if (!jsonWriterClass) { 1120 jsonWriterClass = NSClassFromString(@"SBJSON"); 1121 } 1122 if (jsonWriterClass) { 1123 GTMFetcherSBJSON *writer = [[[jsonWriterClass alloc] init] autorelease]; 1124 [writer setHumanReadable:YES]; 1125 NSString *jsonStr = [writer stringWithObject:obj error:NULL]; 1126 return jsonStr; 1127 } 1128 } 1129 return nil; 1130} 1131 1132@end 1133 1134#endif // !STRIP_GTM_FETCH_LOGGING