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