summaryrefslogtreecommitdiff
path: root/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.m
diff options
context:
space:
mode:
Diffstat (limited to 'StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.m')
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.m982
1 files changed, 982 insertions, 0 deletions
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.m b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.m
new file mode 100644
index 00000000..cdf5c179
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.m
@@ -0,0 +1,982 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#include <sys/stat.h>
+#include <unistd.h>
+
+#import "GTMSessionFetcherLogging.h"
+
+#ifndef STRIP_GTM_FETCH_LOGGING
+ #error GTMSessionFetcher headers should have defaulted this if it wasn't already defined.
+#endif
+
+#if !STRIP_GTM_FETCH_LOGGING
+
+// Sensitive credential strings are replaced in logs with _snip_
+//
+// Apps that must see the contents of sensitive tokens can set this to 1
+#ifndef SKIP_GTM_FETCH_LOGGING_SNIPPING
+#define SKIP_GTM_FETCH_LOGGING_SNIPPING 0
+#endif
+
+// If GTMReadMonitorInputStream is available, it can be used for
+// capturing uploaded streams of data
+//
+// We locally declare methods of GTMReadMonitorInputStream so we
+// do not need to import the header, as some projects may not have it available
+#if !GTMSESSION_BUILD_COMBINED_SOURCES
+@interface GTMReadMonitorInputStream : NSInputStream
+
++ (instancetype)inputStreamWithStream:(NSInputStream *)input;
+
+@property (assign) id readDelegate;
+@property (assign) SEL readSelector;
+
+@end
+#else
+@class GTMReadMonitorInputStream;
+#endif // !GTMSESSION_BUILD_COMBINED_SOURCES
+
+@interface GTMSessionFetcher (GTMHTTPFetcherLoggingUtilities)
+
++ (NSString *)headersStringForDictionary:(NSDictionary *)dict;
++ (NSString *)snipSubstringOfString:(NSString *)originalStr
+ betweenStartString:(NSString *)startStr
+ endString:(NSString *)endStr;
+- (void)inputStream:(GTMReadMonitorInputStream *)stream
+ readIntoBuffer:(void *)buffer
+ length:(int64_t)length;
+
+@end
+
+@implementation GTMSessionFetcher (GTMSessionFetcherLogging)
+
+// fetchers come and fetchers go, but statics are forever
+static BOOL gIsLoggingEnabled = NO;
+static BOOL gIsLoggingToFile = YES;
+static NSString *gLoggingDirectoryPath = nil;
+static NSString *gLogDirectoryForCurrentRun = nil;
+static NSString *gLoggingDateStamp = nil;
+static NSString *gLoggingProcessName = nil;
+
++ (void)setLoggingDirectory:(NSString *)path {
+ gLoggingDirectoryPath = [path copy];
+}
+
++ (NSString *)loggingDirectory {
+ if (!gLoggingDirectoryPath) {
+ NSArray *paths = nil;
+#if TARGET_IPHONE_SIMULATOR
+ // default to a directory called GTMHTTPDebugLogs into a sandbox-safe
+ // directory that a developer can find easily, the application home
+ paths = @[ NSHomeDirectory() ];
+#elif TARGET_OS_IPHONE
+ // Neither ~/Desktop nor ~/Home is writable on an actual iOS, watchOS, or tvOS device.
+ // Put it in ~/Documents.
+ paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+#else
+ // default to a directory called GTMHTTPDebugLogs in the desktop folder
+ paths = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES);
+#endif
+
+ NSString *desktopPath = paths.firstObject;
+ if (desktopPath) {
+ NSString *const kGTMLogFolderName = @"GTMHTTPDebugLogs";
+ NSString *logsFolderPath = [desktopPath stringByAppendingPathComponent:kGTMLogFolderName];
+
+ NSFileManager *fileMgr = [NSFileManager defaultManager];
+ BOOL isDir;
+ BOOL doesFolderExist = [fileMgr fileExistsAtPath:logsFolderPath isDirectory:&isDir];
+ if (!doesFolderExist) {
+ // make the directory
+ doesFolderExist = [fileMgr createDirectoryAtPath:logsFolderPath
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:NULL];
+ if (doesFolderExist) {
+ // The directory has been created. Exclude it from backups.
+ NSURL *pathURL = [NSURL fileURLWithPath:logsFolderPath isDirectory:YES];
+ [pathURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:NULL];
+ }
+ }
+
+ if (doesFolderExist) {
+ // it's there; store it in the global
+ gLoggingDirectoryPath = [logsFolderPath copy];
+ }
+ }
+ }
+ return gLoggingDirectoryPath;
+}
+
++ (void)setLogDirectoryForCurrentRun:(NSString *)logDirectoryForCurrentRun {
+ // Set the path for this run's logs.
+ gLogDirectoryForCurrentRun = [logDirectoryForCurrentRun copy];
+}
+
++ (NSString *)logDirectoryForCurrentRun {
+ // make a directory for this run's logs, like SyncProto_logs_10-16_01-56-58PM
+ if (gLogDirectoryForCurrentRun) return gLogDirectoryForCurrentRun;
+
+ NSString *parentDir = [self loggingDirectory];
+ NSString *logNamePrefix = [self processNameLogPrefix];
+ NSString *dateStamp = [self loggingDateStamp];
+ NSString *dirName = [NSString stringWithFormat:@"%@%@", logNamePrefix, dateStamp];
+ NSString *logDirectory = [parentDir stringByAppendingPathComponent:dirName];
+
+ if (gIsLoggingToFile) {
+ NSFileManager *fileMgr = [NSFileManager defaultManager];
+ // Be sure that the first time this app runs, it's not writing to a preexisting folder
+ static BOOL gShouldReuseFolder = NO;
+ if (!gShouldReuseFolder) {
+ gShouldReuseFolder = YES;
+ NSString *origLogDir = logDirectory;
+ for (int ctr = 2; ctr < 20; ++ctr) {
+ if (![fileMgr fileExistsAtPath:logDirectory]) break;
+
+ // append a digit
+ logDirectory = [origLogDir stringByAppendingFormat:@"_%d", ctr];
+ }
+ }
+ if (![fileMgr createDirectoryAtPath:logDirectory
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:NULL]) return nil;
+ }
+ gLogDirectoryForCurrentRun = logDirectory;
+
+ return gLogDirectoryForCurrentRun;
+}
+
++ (void)setLoggingEnabled:(BOOL)isLoggingEnabled {
+ gIsLoggingEnabled = isLoggingEnabled;
+}
+
++ (BOOL)isLoggingEnabled {
+ return gIsLoggingEnabled;
+}
+
++ (void)setLoggingToFileEnabled:(BOOL)isLoggingToFileEnabled {
+ gIsLoggingToFile = isLoggingToFileEnabled;
+}
+
++ (BOOL)isLoggingToFileEnabled {
+ return gIsLoggingToFile;
+}
+
++ (void)setLoggingProcessName:(NSString *)processName {
+ gLoggingProcessName = [processName copy];
+}
+
++ (NSString *)loggingProcessName {
+ // get the process name (once per run) replacing spaces with underscores
+ if (!gLoggingProcessName) {
+ NSString *procName = [[NSProcessInfo processInfo] processName];
+ gLoggingProcessName = [procName stringByReplacingOccurrencesOfString:@" " withString:@"_"];
+ }
+ return gLoggingProcessName;
+}
+
++ (void)setLoggingDateStamp:(NSString *)dateStamp {
+ gLoggingDateStamp = [dateStamp copy];
+}
+
++ (NSString *)loggingDateStamp {
+ // We'll pick one date stamp per run, so a run that starts at a later second
+ // will get a unique results html file
+ if (!gLoggingDateStamp) {
+ // produce a string like 08-21_01-41-23PM
+
+ NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
+ [formatter setFormatterBehavior:NSDateFormatterBehavior10_4];
+ [formatter setDateFormat:@"M-dd_hh-mm-ssa"];
+
+ gLoggingDateStamp = [formatter stringFromDate:[NSDate date]];
+ }
+ return gLoggingDateStamp;
+}
+
++ (NSString *)processNameLogPrefix {
+ static NSString *gPrefix = nil;
+ if (!gPrefix) {
+ NSString *processName = [self loggingProcessName];
+ gPrefix = [[NSString alloc] initWithFormat:@"%@_log_", processName];
+ }
+ return gPrefix;
+}
+
++ (NSString *)symlinkNameSuffix {
+ return @"_log_newest.html";
+}
+
++ (NSString *)htmlFileName {
+ return @"aperçu_http_log.html";
+}
+
++ (void)deleteLogDirectoriesOlderThanDate:(NSDate *)cutoffDate {
+ NSFileManager *fileMgr = [NSFileManager defaultManager];
+ NSURL *parentDir = [NSURL fileURLWithPath:[[self class] loggingDirectory]];
+ NSURL *logDirectoryForCurrentRun =
+ [NSURL fileURLWithPath:[[self class] logDirectoryForCurrentRun]];
+ NSError *error;
+ NSArray *contents = [fileMgr contentsOfDirectoryAtURL:parentDir
+ includingPropertiesForKeys:@[ NSURLContentModificationDateKey ]
+ options:0
+ error:&error];
+ for (NSURL *itemURL in contents) {
+ if ([itemURL isEqual:logDirectoryForCurrentRun]) continue;
+
+ NSDate *modDate;
+ if ([itemURL getResourceValue:&modDate
+ forKey:NSURLContentModificationDateKey
+ error:&error]) {
+ if ([modDate compare:cutoffDate] == NSOrderedAscending) {
+ if (![fileMgr removeItemAtURL:itemURL error:&error]) {
+ NSLog(@"deleteLogDirectoriesOlderThanDate failed to delete %@: %@",
+ itemURL.path, error);
+ }
+ }
+ } else {
+ NSLog(@"deleteLogDirectoriesOlderThanDate failed to get mod date of %@: %@",
+ itemURL.path, error);
+ }
+ }
+}
+
+// formattedStringFromData returns a prettyprinted string for XML or JSON input,
+// and a plain string for other input data
+- (NSString *)formattedStringFromData:(NSData *)inputData
+ contentType:(NSString *)contentType
+ JSON:(NSDictionary **)outJSON {
+ if (!inputData) return nil;
+
+ // if the content type is JSON and we have the parsing class available, use that
+ if ([contentType hasPrefix:@"application/json"] && inputData.length > 5) {
+ // convert from JSON string to NSObjects and back to a formatted string
+ NSMutableDictionary *obj = [NSJSONSerialization JSONObjectWithData:inputData
+ options:NSJSONReadingMutableContainers
+ error:NULL];
+ if (obj) {
+ if (outJSON) *outJSON = obj;
+ if ([obj isKindOfClass:[NSMutableDictionary class]]) {
+ // for security and privacy, omit OAuth 2 response access and refresh tokens
+ if ([obj valueForKey:@"refresh_token"] != nil) {
+ [obj setObject:@"_snip_" forKey:@"refresh_token"];
+ }
+ if ([obj valueForKey:@"access_token"] != nil) {
+ [obj setObject:@"_snip_" forKey:@"access_token"];
+ }
+ }
+ NSData *data = [NSJSONSerialization dataWithJSONObject:obj
+ options:NSJSONWritingPrettyPrinted
+ error:NULL];
+ if (data) {
+ NSString *jsonStr = [[NSString alloc] initWithData:data
+ encoding:NSUTF8StringEncoding];
+ return jsonStr;
+ }
+ }
+ }
+
+#if !TARGET_OS_IPHONE && !GTM_SKIP_LOG_XMLFORMAT
+ // verify that this data starts with the bytes indicating XML
+
+ NSString *const kXMLLintPath = @"/usr/bin/xmllint";
+ static BOOL gHasCheckedAvailability = NO;
+ static BOOL gIsXMLLintAvailable = NO;
+
+ if (!gHasCheckedAvailability) {
+ gIsXMLLintAvailable = [[NSFileManager defaultManager] fileExistsAtPath:kXMLLintPath];
+ gHasCheckedAvailability = YES;
+ }
+ if (gIsXMLLintAvailable
+ && inputData.length > 5
+ && strncmp(inputData.bytes, "<?xml", 5) == 0) {
+
+ // call xmllint to format the data
+ NSTask *task = [[NSTask alloc] init];
+ [task setLaunchPath:kXMLLintPath];
+
+ // use the dash argument to specify stdin as the source file
+ [task setArguments:@[ @"--format", @"-" ]];
+ [task setEnvironment:@{}];
+
+ NSPipe *inputPipe = [NSPipe pipe];
+ NSPipe *outputPipe = [NSPipe pipe];
+ [task setStandardInput:inputPipe];
+ [task setStandardOutput:outputPipe];
+
+ [task launch];
+
+ [[inputPipe fileHandleForWriting] writeData:inputData];
+ [[inputPipe fileHandleForWriting] closeFile];
+
+ // drain the stdout before waiting for the task to exit
+ NSData *formattedData = [[outputPipe fileHandleForReading] readDataToEndOfFile];
+
+ [task waitUntilExit];
+
+ int status = [task terminationStatus];
+ if (status == 0 && formattedData.length > 0) {
+ // success
+ inputData = formattedData;
+ }
+ }
+#else
+ // we can't call external tasks on the iPhone; leave the XML unformatted
+#endif
+
+ NSString *dataStr = [[NSString alloc] initWithData:inputData
+ encoding:NSUTF8StringEncoding];
+ return dataStr;
+}
+
+// stringFromStreamData creates a string given the supplied data
+//
+// If NSString can create a UTF-8 string from the data, then that is returned.
+//
+// Otherwise, this routine tries to find a MIME boundary at the beginning of the data block, and
+// uses that to break up the data into parts. Each part will be used to try to make a UTF-8 string.
+// For parts that fail, a replacement string showing the part header and <<n bytes>> is supplied
+// in place of the binary data.
+
+- (NSString *)stringFromStreamData:(NSData *)data
+ contentType:(NSString *)contentType {
+
+ if (!data) return nil;
+
+ // optimistically, see if the whole data block is UTF-8
+ NSString *streamDataStr = [self formattedStringFromData:data
+ contentType:contentType
+ JSON:NULL];
+ if (streamDataStr) return streamDataStr;
+
+ // Munge a buffer by replacing non-ASCII bytes with underscores, and turn that munged buffer an
+ // NSString. That gives us a string we can use with NSScanner.
+ NSMutableData *mutableData = [NSMutableData dataWithData:data];
+ unsigned char *bytes = (unsigned char *)mutableData.mutableBytes;
+
+ for (unsigned int idx = 0; idx < mutableData.length; ++idx) {
+ if (bytes[idx] > 0x7F || bytes[idx] == 0) {
+ bytes[idx] = '_';
+ }
+ }
+
+ NSString *mungedStr = [[NSString alloc] initWithData:mutableData
+ encoding:NSUTF8StringEncoding];
+ if (mungedStr) {
+
+ // scan for the boundary string
+ NSString *boundary = nil;
+ NSScanner *scanner = [NSScanner scannerWithString:mungedStr];
+
+ if ([scanner scanUpToString:@"\r\n" intoString:&boundary]
+ && [boundary hasPrefix:@"--"]) {
+
+ // we found a boundary string; use it to divide the string into parts
+ NSArray *mungedParts = [mungedStr componentsSeparatedByString:boundary];
+
+ // look at each munged part in the original string, and try to convert those into UTF-8
+ NSMutableArray *origParts = [NSMutableArray array];
+ NSUInteger offset = 0;
+ for (NSString *mungedPart in mungedParts) {
+ NSUInteger partSize = mungedPart.length;
+ NSData *origPartData = [data subdataWithRange:NSMakeRange(offset, partSize)];
+ NSString *origPartStr = [[NSString alloc] initWithData:origPartData
+ encoding:NSUTF8StringEncoding];
+ if (origPartStr) {
+ // we could make this original part into UTF-8; use the string
+ [origParts addObject:origPartStr];
+ } else {
+ // this part can't be made into UTF-8; scan the header, if we can
+ NSString *header = nil;
+ NSScanner *headerScanner = [NSScanner scannerWithString:mungedPart];
+ if (![headerScanner scanUpToString:@"\r\n\r\n" intoString:&header]) {
+ // we couldn't find a header
+ header = @"";
+ }
+ // make a part string with the header and <<n bytes>>
+ NSString *binStr = [NSString stringWithFormat:@"\r%@\r<<%lu bytes>>\r",
+ header, (long)(partSize - header.length)];
+ [origParts addObject:binStr];
+ }
+ offset += partSize + boundary.length;
+ }
+ // rejoin the original parts
+ streamDataStr = [origParts componentsJoinedByString:boundary];
+ }
+ }
+ if (!streamDataStr) {
+ // give up; just make a string showing the uploaded bytes
+ streamDataStr = [NSString stringWithFormat:@"<<%u bytes>>", (unsigned int)data.length];
+ }
+ return streamDataStr;
+}
+
+// logFetchWithError is called following a successful or failed fetch attempt
+//
+// This method does all the work for appending to and creating log files
+
+- (void)logFetchWithError:(NSError *)error {
+ if (![[self class] isLoggingEnabled]) return;
+ NSString *logDirectory = [[self class] logDirectoryForCurrentRun];
+ if (!logDirectory) return;
+ NSString *processName = [[self class] loggingProcessName];
+
+ // TODO: add Javascript to display response data formatted in hex
+
+ // each response's NSData goes into its own xml or txt file, though all responses for this run of
+ // the app share a main html file. This counter tracks all fetch responses for this app run.
+ //
+ // we'll use a local variable since this routine may be reentered while waiting for XML formatting
+ // to be completed by an external task
+ static int gResponseCounter = 0;
+ int responseCounter = ++gResponseCounter;
+
+ NSURLResponse *response = [self response];
+ NSDictionary *responseHeaders = [self responseHeaders];
+ NSString *responseDataStr = nil;
+ NSDictionary *responseJSON = nil;
+
+ // if there's response data, decide what kind of file to put it in based on the first bytes of the
+ // file or on the mime type supplied by the server
+ NSString *responseMIMEType = [response MIMEType];
+ BOOL isResponseImage = NO;
+
+ // file name for an image data file
+ NSString *responseDataFileName = nil;
+
+ int64_t responseDataLength = self.downloadedLength;
+ if (responseDataLength > 0) {
+ NSData *downloadedData = self.downloadedData;
+ if (downloadedData == nil
+ && responseDataLength > 0
+ && responseDataLength < 20000
+ && self.destinationFileURL) {
+ // There's a download file that's not too big, so get the data to display from the downloaded
+ // file.
+ NSURL *destinationURL = self.destinationFileURL;
+ downloadedData = [NSData dataWithContentsOfURL:destinationURL];
+ }
+ NSString *responseType = [responseHeaders valueForKey:@"Content-Type"];
+ responseDataStr = [self formattedStringFromData:downloadedData
+ contentType:responseType
+ JSON:&responseJSON];
+ NSString *responseDataExtn = nil;
+ NSData *dataToWrite = nil;
+ if (responseDataStr) {
+ // we were able to make a UTF-8 string from the response data
+ if ([responseMIMEType isEqual:@"application/atom+xml"]
+ || [responseMIMEType hasSuffix:@"/xml"]) {
+ responseDataExtn = @"xml";
+ dataToWrite = [responseDataStr dataUsingEncoding:NSUTF8StringEncoding];
+ }
+ } else if ([responseMIMEType isEqual:@"image/jpeg"]) {
+ responseDataExtn = @"jpg";
+ dataToWrite = downloadedData;
+ isResponseImage = YES;
+ } else if ([responseMIMEType isEqual:@"image/gif"]) {
+ responseDataExtn = @"gif";
+ dataToWrite = downloadedData;
+ isResponseImage = YES;
+ } else if ([responseMIMEType isEqual:@"image/png"]) {
+ responseDataExtn = @"png";
+ dataToWrite = downloadedData;
+ isResponseImage = YES;
+ } else {
+ // add more non-text types here
+ }
+ // if we have an extension, save the raw data in a file with that extension
+ if (responseDataExtn && dataToWrite) {
+ // generate a response file base name like
+ NSString *responseBaseName = [NSString stringWithFormat:@"fetch_%d_response", responseCounter];
+ responseDataFileName = [responseBaseName stringByAppendingPathExtension:responseDataExtn];
+ NSString *responseDataFilePath = [logDirectory stringByAppendingPathComponent:responseDataFileName];
+
+ NSError *downloadedError = nil;
+ if (gIsLoggingToFile && ![dataToWrite writeToFile:responseDataFilePath
+ options:0
+ error:&downloadedError]) {
+ NSLog(@"%@ logging write error:%@ (%@)", [self class], downloadedError, responseDataFileName);
+ }
+ }
+ }
+ // we'll have one main html file per run of the app
+ NSString *htmlName = [[self class] htmlFileName];
+ NSString *htmlPath =[logDirectory stringByAppendingPathComponent:htmlName];
+
+ // if the html file exists (from logging previous fetches) we don't need
+ // to re-write the header or the scripts
+ NSFileManager *fileMgr = [NSFileManager defaultManager];
+ BOOL didFileExist = [fileMgr fileExistsAtPath:htmlPath];
+
+ NSMutableString* outputHTML = [NSMutableString string];
+
+ // we need a header to say we'll have UTF-8 text
+ if (!didFileExist) {
+ [outputHTML appendFormat:@"<html><head><meta http-equiv=\"content-type\" "
+ "content=\"text/html; charset=UTF-8\"><title>%@ HTTP fetch log %@</title>",
+ processName, [[self class] loggingDateStamp]];
+ }
+ // now write the visible html elements
+ NSString *copyableFileName = [NSString stringWithFormat:@"fetch_%d.txt", responseCounter];
+
+ NSDate *now = [NSDate date];
+ // write the date & time, the comment, and the link to the plain-text (copyable) log
+ [outputHTML appendFormat:@"<b>%@ &nbsp;&nbsp;&nbsp;&nbsp; ", now];
+
+ NSString *comment = [self comment];
+ if (comment.length > 0) {
+ [outputHTML appendFormat:@"%@ &nbsp;&nbsp;&nbsp;&nbsp; ", comment];
+ }
+ [outputHTML appendFormat:@"</b><a href='%@'><i>request/response log</i></a><br>", copyableFileName];
+ NSTimeInterval elapsed = -self.initialBeginFetchDate.timeIntervalSinceNow;
+ [outputHTML appendFormat:@"elapsed: %5.3fsec<br>", elapsed];
+
+ // write the request URL
+ NSURLRequest *request = self.request;
+ NSString *requestMethod = request.HTTPMethod;
+ NSURL *requestURL = request.URL;
+
+ // Save the request URL for next time in case this redirects.
+ NSString *redirectedFromURLString = [self.redirectedFromURL absoluteString];
+ self.redirectedFromURL = [requestURL copy];
+ if (redirectedFromURLString) {
+ [outputHTML appendFormat:@"<FONT COLOR='#990066'><i>redirected from %@</i></FONT><br>",
+ redirectedFromURLString];
+ }
+ [outputHTML appendFormat:@"<b>request:</b> %@ <code>%@</code><br>\n", requestMethod, requestURL];
+
+ // write the request headers
+ NSDictionary *requestHeaders = request.allHTTPHeaderFields;
+ NSUInteger numberOfRequestHeaders = requestHeaders.count;
+ if (numberOfRequestHeaders > 0) {
+ // Indicate if the request is authorized; warn if the request is authorized but non-SSL
+ NSString *auth = [requestHeaders objectForKey:@"Authorization"];
+ NSString *headerDetails = @"";
+ if (auth) {
+ BOOL isInsecure = [[requestURL scheme] isEqual:@"http"];
+ if (isInsecure) {
+ // 26A0 = ⚠
+ headerDetails =
+ @"&nbsp;&nbsp;&nbsp;<i>authorized, non-SSL</i><FONT COLOR='#FF00FF'> &#x26A0;</FONT> ";
+ } else {
+ headerDetails = @"&nbsp;&nbsp;&nbsp;<i>authorized</i>";
+ }
+ }
+ NSString *cookiesHdr = [requestHeaders objectForKey:@"Cookie"];
+ if (cookiesHdr) {
+ headerDetails = [headerDetails stringByAppendingString:@"&nbsp;&nbsp;&nbsp;<i>cookies</i>"];
+ }
+ NSString *matchHdr = [requestHeaders objectForKey:@"If-Match"];
+ if (matchHdr) {
+ headerDetails = [headerDetails stringByAppendingString:@"&nbsp;&nbsp;&nbsp;<i>if-match</i>"];
+ }
+ matchHdr = [requestHeaders objectForKey:@"If-None-Match"];
+ if (matchHdr) {
+ headerDetails = [headerDetails stringByAppendingString:@"&nbsp;&nbsp;&nbsp;<i>if-none-match</i>"];
+ }
+ [outputHTML appendFormat:@"&nbsp;&nbsp; headers: %d %@<br>",
+ (int)numberOfRequestHeaders, headerDetails];
+ } else {
+ [outputHTML appendFormat:@"&nbsp;&nbsp; headers: none<br>"];
+ }
+ // write the request post data
+ NSData *bodyData = nil;
+ NSData *loggedStreamData = self.loggedStreamData;
+ if (loggedStreamData) {
+ bodyData = loggedStreamData;
+ } else {
+ bodyData = self.bodyData;
+ if (bodyData == nil) {
+ bodyData = self.request.HTTPBody;
+ }
+ }
+ uint64_t bodyDataLength = bodyData.length;
+
+ if (bodyData.length == 0) {
+ // If the data is in a body upload file URL, read that in if it's not huge.
+ NSURL *bodyFileURL = self.bodyFileURL;
+ if (bodyFileURL) {
+ NSNumber *fileSizeNum = nil;
+ NSError *fileSizeError = nil;
+ if ([bodyFileURL getResourceValue:&fileSizeNum
+ forKey:NSURLFileSizeKey
+ error:&fileSizeError]) {
+ bodyDataLength = [fileSizeNum unsignedLongLongValue];
+ if (bodyDataLength > 0 && bodyDataLength < 50000) {
+ bodyData = [NSData dataWithContentsOfURL:bodyFileURL
+ options:NSDataReadingUncached
+ error:&fileSizeError];
+ }
+ }
+ }
+ }
+ NSString *bodyDataStr = nil;
+ NSString *postType = [requestHeaders valueForKey:@"Content-Type"];
+
+ if (bodyDataLength > 0) {
+ [outputHTML appendFormat:@"&nbsp;&nbsp; data: %llu bytes, <code>%@</code><br>\n",
+ bodyDataLength, postType ? postType : @"(no type)"];
+ NSString *logRequestBody = self.logRequestBody;
+ if (logRequestBody) {
+ bodyDataStr = [logRequestBody copy];
+ self.logRequestBody = nil;
+ } else {
+ bodyDataStr = [self stringFromStreamData:bodyData
+ contentType:postType];
+ if (bodyDataStr) {
+ // remove OAuth 2 client secret and refresh token
+ bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr
+ betweenStartString:@"client_secret="
+ endString:@"&"];
+ bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr
+ betweenStartString:@"refresh_token="
+ endString:@"&"];
+ // remove ClientLogin password
+ bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr
+ betweenStartString:@"&Passwd="
+ endString:@"&"];
+ }
+ }
+ } else {
+ // no post data
+ }
+ // write the response status, MIME type, URL
+ NSInteger status = [self statusCode];
+ if (response) {
+ NSString *statusString = @"";
+ if (status != 0) {
+ if (status == 200 || status == 201) {
+ statusString = [NSString stringWithFormat:@"%ld", (long)status];
+
+ // report any JSON-RPC error
+ if ([responseJSON isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *jsonError = [responseJSON objectForKey:@"error"];
+ if ([jsonError isKindOfClass:[NSDictionary class]]) {
+ NSString *jsonCode = [[jsonError valueForKey:@"code"] description];
+ NSString *jsonMessage = [jsonError valueForKey:@"message"];
+ if (jsonCode || jsonMessage) {
+ // 2691 = ⚑
+ NSString *const jsonErrFmt =
+ @"&nbsp;&nbsp;&nbsp;<i>JSON error:</i> <FONT COLOR='#FF00FF'>%@ %@ &nbsp;&#x2691;</FONT>";
+ statusString = [statusString stringByAppendingFormat:jsonErrFmt,
+ jsonCode ? jsonCode : @"",
+ jsonMessage ? jsonMessage : @""];
+ }
+ }
+ }
+ } else {
+ // purple for anything other than 200 or 201
+ NSString *flag = status >= 400 ? @"&nbsp;&#x2691;" : @""; // 2691 = ⚑
+ NSString *explanation = [NSHTTPURLResponse localizedStringForStatusCode:status];
+ NSString *const statusFormat = @"<FONT COLOR='#FF00FF'>%ld %@ %@</FONT>";
+ statusString = [NSString stringWithFormat:statusFormat, (long)status, explanation, flag];
+ }
+ }
+ // show the response URL only if it's different from the request URL
+ NSString *responseURLStr = @"";
+ NSURL *responseURL = response.URL;
+
+ if (responseURL && ![responseURL isEqual:request.URL]) {
+ NSString *const responseURLFormat =
+ @"<FONT COLOR='#FF00FF'>response URL:</FONT> <code>%@</code><br>\n";
+ responseURLStr = [NSString stringWithFormat:responseURLFormat, [responseURL absoluteString]];
+ }
+ [outputHTML appendFormat:@"<b>response:</b>&nbsp;&nbsp;status %@<br>\n%@",
+ statusString, responseURLStr];
+ // Write the response headers
+ NSUInteger numberOfResponseHeaders = responseHeaders.count;
+ if (numberOfResponseHeaders > 0) {
+ // Indicate if the server is setting cookies
+ NSString *cookiesSet = [responseHeaders valueForKey:@"Set-Cookie"];
+ NSString *cookiesStr =
+ cookiesSet ? @"&nbsp;&nbsp;<FONT COLOR='#990066'><i>sets cookies</i></FONT>" : @"";
+ // Indicate if the server is redirecting
+ NSString *location = [responseHeaders valueForKey:@"Location"];
+ BOOL isRedirect = status >= 300 && status <= 399 && location != nil;
+ NSString *redirectsStr =
+ isRedirect ? @"&nbsp;&nbsp;<FONT COLOR='#990066'><i>redirects</i></FONT>" : @"";
+ [outputHTML appendFormat:@"&nbsp;&nbsp; headers: %d %@ %@<br>\n",
+ (int)numberOfResponseHeaders, cookiesStr, redirectsStr];
+ } else {
+ [outputHTML appendString:@"&nbsp;&nbsp; headers: none<br>\n"];
+ }
+ }
+ // error
+ if (error) {
+ [outputHTML appendFormat:@"<b>Error:</b> %@ <br>\n", error.description];
+ }
+ // Write the response data
+ if (responseDataFileName) {
+ if (isResponseImage) {
+ // Make a small inline image that links to the full image file
+ [outputHTML appendFormat:@"&nbsp;&nbsp; data: %lld bytes, <code>%@</code><br>",
+ responseDataLength, responseMIMEType];
+ NSString *const fmt =
+ @"<a href=\"%@\"><img src='%@' alt='image' style='border:solid thin;max-height:32'></a>\n";
+ [outputHTML appendFormat:fmt, responseDataFileName, responseDataFileName];
+ } else {
+ // The response data was XML; link to the xml file
+ NSString *const fmt =
+ @"&nbsp;&nbsp; data: %lld bytes, <code>%@</code>&nbsp;&nbsp;&nbsp;<i><a href=\"%@\">%@</a></i>\n";
+ [outputHTML appendFormat:fmt, responseDataLength, responseMIMEType,
+ responseDataFileName, [responseDataFileName pathExtension]];
+ }
+ } else {
+ // The response data was not an image; just show the length and MIME type
+ [outputHTML appendFormat:@"&nbsp;&nbsp; data: %lld bytes, <code>%@</code>\n",
+ responseDataLength, responseMIMEType ? responseMIMEType : @"(no response type)"];
+ }
+ // Make a single string of the request and response, suitable for copying
+ // to the clipboard and pasting into a bug report
+ NSMutableString *copyable = [NSMutableString string];
+ if (comment) {
+ [copyable appendFormat:@"%@\n\n", comment];
+ }
+ [copyable appendFormat:@"%@ elapsed: %5.3fsec\n", now, elapsed];
+ if (redirectedFromURLString) {
+ [copyable appendFormat:@"Redirected from %@\n", redirectedFromURLString];
+ }
+ [copyable appendFormat:@"Request: %@ %@\n", requestMethod, requestURL];
+ if (requestHeaders.count > 0) {
+ [copyable appendFormat:@"Request headers:\n%@\n",
+ [[self class] headersStringForDictionary:requestHeaders]];
+ }
+ if (bodyDataLength > 0) {
+ [copyable appendFormat:@"Request body: (%llu bytes)\n", bodyDataLength];
+ if (bodyDataStr) {
+ [copyable appendFormat:@"%@\n", bodyDataStr];
+ }
+ [copyable appendString:@"\n"];
+ }
+ if (response) {
+ [copyable appendFormat:@"Response: status %d\n", (int) status];
+ [copyable appendFormat:@"Response headers:\n%@\n",
+ [[self class] headersStringForDictionary:responseHeaders]];
+ [copyable appendFormat:@"Response body: (%lld bytes)\n", responseDataLength];
+ if (responseDataLength > 0) {
+ NSString *logResponseBody = self.logResponseBody;
+ if (logResponseBody) {
+ // The user has provided the response body text.
+ responseDataStr = [logResponseBody copy];
+ self.logResponseBody = nil;
+ }
+ if (responseDataStr != nil) {
+ [copyable appendFormat:@"%@\n", responseDataStr];
+ } else {
+ // Even though it's redundant, we'll put in text to indicate that all the bytes are binary.
+ if (self.destinationFileURL) {
+ [copyable appendFormat:@"<<%lld bytes>> to file %@\n",
+ responseDataLength, self.destinationFileURL.path];
+ } else {
+ [copyable appendFormat:@"<<%lld bytes>>\n", responseDataLength];
+ }
+ }
+ }
+ }
+ if (error) {
+ [copyable appendFormat:@"Error: %@\n", error];
+ }
+ // Save to log property before adding the separator
+ self.log = copyable;
+
+ [copyable appendString:@"-----------------------------------------------------------\n"];
+
+ // Write the copyable version to another file (linked to at the top of the html file, above)
+ //
+ // Ideally, something to just copy this to the clipboard like
+ // <span onCopy='window.event.clipboardData.setData(\"Text\",
+ // \"copyable stuff\");return false;'>Copy here.</span>"
+ // would work everywhere, but it only works in Safari as of 8/2010
+ if (gIsLoggingToFile) {
+ NSString *parentDir = [[self class] loggingDirectory];
+ NSString *copyablePath = [logDirectory stringByAppendingPathComponent:copyableFileName];
+ NSError *copyableError = nil;
+ if (![copyable writeToFile:copyablePath
+ atomically:NO
+ encoding:NSUTF8StringEncoding
+ error:&copyableError]) {
+ // Error writing to file
+ NSLog(@"%@ logging write error:%@ (%@)", [self class], copyableError, copyablePath);
+ }
+ [outputHTML appendString:@"<br><hr><p>"];
+
+ // Append the HTML to the main output file
+ const char* htmlBytes = outputHTML.UTF8String;
+ NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:htmlPath
+ append:YES];
+ [stream open];
+ [stream write:(const uint8_t *) htmlBytes maxLength:strlen(htmlBytes)];
+ [stream close];
+
+ // Make a symlink to the latest html
+ NSString *const symlinkNameSuffix = [[self class] symlinkNameSuffix];
+ NSString *symlinkName = [processName stringByAppendingString:symlinkNameSuffix];
+ NSString *symlinkPath = [parentDir stringByAppendingPathComponent:symlinkName];
+
+ [fileMgr removeItemAtPath:symlinkPath error:NULL];
+ [fileMgr createSymbolicLinkAtPath:symlinkPath
+ withDestinationPath:htmlPath
+ error:NULL];
+#if TARGET_OS_IPHONE
+ static BOOL gReportedLoggingPath = NO;
+ if (!gReportedLoggingPath) {
+ gReportedLoggingPath = YES;
+ NSLog(@"GTMSessionFetcher logging to \"%@\"", parentDir);
+ }
+#endif
+ }
+}
+
+- (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream {
+ if (!inputStream) return nil;
+ if (![GTMSessionFetcher isLoggingEnabled]) return inputStream;
+
+ [self clearLoggedStreamData]; // Clear any previous data.
+ Class monitorClass = NSClassFromString(@"GTMReadMonitorInputStream");
+ if (!monitorClass) {
+ NSString const *str = @"<<Uploaded stream log unavailable without GTMReadMonitorInputStream>>";
+ NSData *stringData = [str dataUsingEncoding:NSUTF8StringEncoding];
+ [self appendLoggedStreamData:stringData];
+ return inputStream;
+ }
+ inputStream = [monitorClass inputStreamWithStream:inputStream];
+
+ GTMReadMonitorInputStream *readMonitorInputStream = (GTMReadMonitorInputStream *)inputStream;
+ [readMonitorInputStream setReadDelegate:self];
+ SEL readSel = @selector(inputStream:readIntoBuffer:length:);
+ [readMonitorInputStream setReadSelector:readSel];
+
+ return inputStream;
+}
+
+- (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider:
+ (GTMSessionFetcherBodyStreamProvider)streamProvider {
+ if (!streamProvider) return nil;
+ if (![GTMSessionFetcher isLoggingEnabled]) return streamProvider;
+
+ [self clearLoggedStreamData]; // Clear any previous data.
+ Class monitorClass = NSClassFromString(@"GTMReadMonitorInputStream");
+ if (!monitorClass) {
+ NSString const *str = @"<<Uploaded stream log unavailable without GTMReadMonitorInputStream>>";
+ NSData *stringData = [str dataUsingEncoding:NSUTF8StringEncoding];
+ [self appendLoggedStreamData:stringData];
+ return streamProvider;
+ }
+ GTMSessionFetcherBodyStreamProvider loggedStreamProvider =
+ ^(GTMSessionFetcherBodyStreamProviderResponse response) {
+ streamProvider(^(NSInputStream *bodyStream) {
+ bodyStream = [self loggedInputStreamForInputStream:bodyStream];
+ response(bodyStream);
+ });
+ };
+ return loggedStreamProvider;
+}
+
+@end
+
+@implementation GTMSessionFetcher (GTMSessionFetcherLoggingUtilities)
+
+- (void)inputStream:(GTMReadMonitorInputStream *)stream
+ readIntoBuffer:(void *)buffer
+ length:(int64_t)length {
+ // append the captured data
+ NSData *data = [NSData dataWithBytesNoCopy:buffer
+ length:(NSUInteger)length
+ freeWhenDone:NO];
+ [self appendLoggedStreamData:data];
+}
+
+#pragma mark Fomatting Utilities
+
++ (NSString *)snipSubstringOfString:(NSString *)originalStr
+ betweenStartString:(NSString *)startStr
+ endString:(NSString *)endStr {
+#if SKIP_GTM_FETCH_LOGGING_SNIPPING
+ return originalStr;
+#else
+ if (!originalStr) return nil;
+
+ // Find the start string, and replace everything between it
+ // and the end string (or the end of the original string) with "_snip_"
+ NSRange startRange = [originalStr rangeOfString:startStr];
+ if (startRange.location == NSNotFound) return originalStr;
+
+ // We found the start string
+ NSUInteger originalLength = originalStr.length;
+ NSUInteger startOfTarget = NSMaxRange(startRange);
+ NSRange targetAndRest = NSMakeRange(startOfTarget, originalLength - startOfTarget);
+ NSRange endRange = [originalStr rangeOfString:endStr
+ options:0
+ range:targetAndRest];
+ NSRange replaceRange;
+ if (endRange.location == NSNotFound) {
+ // Found no end marker so replace to end of string
+ replaceRange = targetAndRest;
+ } else {
+ // Replace up to the endStr
+ replaceRange = NSMakeRange(startOfTarget, endRange.location - startOfTarget);
+ }
+ NSString *result = [originalStr stringByReplacingCharactersInRange:replaceRange
+ withString:@"_snip_"];
+ return result;
+#endif // SKIP_GTM_FETCH_LOGGING_SNIPPING
+}
+
++ (NSString *)headersStringForDictionary:(NSDictionary *)dict {
+ // Format the dictionary in http header style, like
+ // Accept: application/json
+ // Cache-Control: no-cache
+ // Content-Type: application/json; charset=utf-8
+ //
+ // Pad the key names, but not beyond 16 chars, since long custom header
+ // keys just create too much whitespace
+ NSArray *keys = [dict.allKeys sortedArrayUsingSelector:@selector(compare:)];
+
+ NSMutableString *str = [NSMutableString string];
+ for (NSString *key in keys) {
+ NSString *value = [dict valueForKey:key];
+ if ([key isEqual:@"Authorization"]) {
+ // Remove OAuth 1 token
+ value = [[self class] snipSubstringOfString:value
+ betweenStartString:@"oauth_token=\""
+ endString:@"\""];
+
+ // Remove OAuth 2 bearer token (draft 16, and older form)
+ value = [[self class] snipSubstringOfString:value
+ betweenStartString:@"Bearer "
+ endString:@"\n"];
+ value = [[self class] snipSubstringOfString:value
+ betweenStartString:@"OAuth "
+ endString:@"\n"];
+
+ // Remove Google ClientLogin
+ value = [[self class] snipSubstringOfString:value
+ betweenStartString:@"GoogleLogin auth="
+ endString:@"\n"];
+ }
+ [str appendFormat:@" %@: %@\n", key, value];
+ }
+ return str;
+}
+
+@end
+
+#endif // !STRIP_GTM_FETCH_LOGGING