You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

982 lines
39 KiB

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