GTMSessionFetcherLogging.m 37 KB

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