PageRenderTime 298ms CodeModel.GetById 16ms app.highlight 269ms RepoModel.GetById 2ms app.codeStats 0ms

/core/externals/update-engine/externals/gdata-objectivec-client/Source/HTTPFetcher/GTMHTTPFetcherLogging.m

http://macfuse.googlecode.com/
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>%@ &nbsp;&nbsp;&nbsp;&nbsp; ";
 597  [outputHTML appendFormat:dateLineFormat, [NSDate date]];
 598
 599  NSString *comment = [self comment];
 600  if (comment) {
 601    NSString *const commentFormat = @"%@ &nbsp;&nbsp;&nbsp;&nbsp; ";
 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 = @"&nbsp;&nbsp;&nbsp;<i>authorized</i>";
 634      BOOL isInsecure = [[requestURL scheme] isEqual:@"http"];
 635      if (isInsecure) {
 636        headerDetails = @"&nbsp;&nbsp;&nbsp;<i>authorized, non-SSL</i>"
 637          "<FONT COLOR='#FF00FF'> &#x26A0;</FONT> "; // 26A0 = ?
 638      }
 639    }
 640    NSString *cookiesHdr = [requestHeaders objectForKey:@"Cookie"];
 641    if (cookiesHdr) {
 642      headerDetails = [headerDetails stringByAppendingString:
 643                       @"&nbsp;&nbsp;&nbsp;<i>cookies</i>"];
 644    }
 645    NSString *matchHdr = [requestHeaders objectForKey:@"If-Match"];
 646    if (matchHdr) {
 647      headerDetails = [headerDetails stringByAppendingString:
 648                       @"&nbsp;&nbsp;&nbsp;<i>if-match</i>"];
 649    }
 650    matchHdr = [requestHeaders objectForKey:@"If-None-Match"];
 651    if (matchHdr) {
 652      headerDetails = [headerDetails stringByAppendingString:
 653                       @"&nbsp;&nbsp;&nbsp;<i>if-none-match</i>"];
 654    }
 655    [outputHTML appendFormat:@"&nbsp;&nbsp; headers: %d  %@<br>",
 656     (int)numberOfRequestHeaders, headerDetails];
 657  } else {
 658    [outputHTML appendFormat:@"&nbsp;&nbsp; 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:@"&nbsp;&nbsp; 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 = @"&nbsp;&nbsp;&nbsp;<i>JSON error:</i> <FONT"
 722                @" COLOR='#FF00FF'>%@ %@ &nbsp;&#x2691;</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 ? @"&nbsp;&#x2691;" : @""); // 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>&nbsp;&nbsp;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 ? @"&nbsp;&nbsp;<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 ? @"&nbsp;&nbsp;<FONT COLOR='#990066'>"
 763                                "<i>redirects</i></FONT>" : @"");
 764
 765      [outputHTML appendFormat:@"&nbsp;&nbsp; headers: %d  %@ %@<br>\n",
 766       (int)numberOfResponseHeaders, cookiesStr, redirectsStr];
 767    } else {
 768      [outputHTML appendString:@"&nbsp;&nbsp; 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:@"&nbsp;&nbsp; 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 = @"&nbsp;&nbsp; data: %d bytes, <code>"
 791        "%@</code>&nbsp;&nbsp;&nbsp;<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:@"&nbsp;&nbsp; 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:&copyableError]) {
 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