/core/externals/update-engine/externals/gdata-objectivec-client/Source/HTTPFetcher/GTMHTTPFetcherLogging.m
http://macfuse.googlecode.com/ · Objective C · 1134 lines · 792 code · 166 blank · 176 comment · 173 complexity · 2dee050b283afad69515f4ce8d46480a MD5 · raw file
- /* Copyright (c) 2010 Google Inc.
- *
- * 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 !STRIP_GTM_FETCH_LOGGING
- #include <sys/stat.h>
- #include <unistd.h>
- #import "GTMHTTPFetcherLogging.h"
- // 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
- #ifndef GTM_NSSTREAM_DELEGATE
- @interface GTMReadMonitorInputStream : NSInputStream
- + (id)inputStreamWithStream:(NSInputStream *)input;
- @property (assign) id readDelegate;
- @property (assign) SEL readSelector;
- @property (retain) NSArray *runLoopModes;
- @end
- #endif
- // If GTMNSJSONSerialization is available, it is used for formatting JSON
- #if (TARGET_OS_MAC && !TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED < 1070)) || \
- (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED < 50000))
- @interface GTMNSJSONSerialization : NSObject
- + (NSData *)dataWithJSONObject:(id)obj options:(NSUInteger)opt error:(NSError **)error;
- + (id)JSONObjectWithData:(NSData *)data options:(NSUInteger)opt error:(NSError **)error;
- @end
- #endif
- // Otherwise, if SBJSON is available, it is used for formatting JSON
- @interface GTMFetcherSBJSON
- - (void)setHumanReadable:(BOOL)flag;
- - (NSString*)stringWithObject:(id)value error:(NSError**)error;
- - (id)objectWithString:(NSString*)jsonrep error:(NSError**)error;
- @end
- @interface GTMHTTPFetcher (GTMHTTPFetcherLoggingUtilities)
- + (NSString *)headersStringForDictionary:(NSDictionary *)dict;
- - (void)inputStream:(GTMReadMonitorInputStream *)stream
- readIntoBuffer:(void *)buffer
- length:(NSUInteger)length;
- // internal file utilities for logging
- + (BOOL)fileOrDirExistsAtPath:(NSString *)path;
- + (BOOL)makeDirectoryUpToPath:(NSString *)path;
- + (BOOL)removeItemAtPath:(NSString *)path;
- + (BOOL)createSymbolicLinkAtPath:(NSString *)newPath
- withDestinationPath:(NSString *)targetPath;
- + (NSString *)snipSubstringOfString:(NSString *)originalStr
- betweenStartString:(NSString *)startStr
- endString:(NSString *)endStr;
- + (id)JSONObjectWithData:(NSData *)data;
- + (id)stringWithJSONObject:(id)obj;
- @end
- @implementation GTMHTTPFetcher (GTMHTTPFetcherLogging)
- // fetchers come and fetchers go, but statics are forever
- static BOOL gIsLoggingEnabled = NO;
- static BOOL gIsLoggingToFile = YES;
- static NSString *gLoggingDirectoryPath = nil;
- static NSString *gLoggingDateStamp = nil;
- static NSString* gLoggingProcessName = nil;
- + (void)setLoggingDirectory:(NSString *)path {
- [gLoggingDirectoryPath autorelease];
- gLoggingDirectoryPath = [path copy];
- }
- + (NSString *)loggingDirectory {
- if (!gLoggingDirectoryPath) {
- NSArray *arr = nil;
- #if GTM_IPHONE && TARGET_IPHONE_SIMULATOR
- // default to a directory called GTMHTTPDebugLogs into a sandbox-safe
- // directory that a developer can find easily, the application home
- arr = [NSArray arrayWithObject:NSHomeDirectory()];
- #elif GTM_IPHONE
- // Neither ~/Desktop nor ~/Home is writable on an actual iPhone device.
- // Put it in ~/Documents.
- arr = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
- NSUserDomainMask, YES);
- #else
- // default to a directory called GTMHTTPDebugLogs in the desktop folder
- arr = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory,
- NSUserDomainMask, YES);
- #endif
- if ([arr count] > 0) {
- NSString *const kGTMLogFolderName = @"GTMHTTPDebugLogs";
- NSString *desktopPath = [arr objectAtIndex:0];
- NSString *logsFolderPath = [desktopPath stringByAppendingPathComponent:kGTMLogFolderName];
- BOOL doesFolderExist = [[self class] fileOrDirExistsAtPath:logsFolderPath];
- if (!doesFolderExist) {
- // make the directory
- doesFolderExist = [self makeDirectoryUpToPath:logsFolderPath];
- }
- if (doesFolderExist) {
- // it's there; store it in the global
- gLoggingDirectoryPath = [logsFolderPath copy];
- }
- }
- }
- return gLoggingDirectoryPath;
- }
- + (void)setLoggingEnabled:(BOOL)flag {
- gIsLoggingEnabled = flag;
- }
- + (BOOL)isLoggingEnabled {
- return gIsLoggingEnabled;
- }
- + (void)setLoggingToFileEnabled:(BOOL)flag {
- gIsLoggingToFile = flag;
- }
- + (BOOL)isLoggingToFileEnabled {
- return gIsLoggingToFile;
- }
- + (void)setLoggingProcessName:(NSString *)str {
- [gLoggingProcessName release];
- gLoggingProcessName = [str copy];
- }
- + (NSString *)loggingProcessName {
- // get the process name (once per run) replacing spaces with underscores
- if (!gLoggingProcessName) {
- NSString *procName = [[NSProcessInfo processInfo] processName];
- NSMutableString *loggingProcessName;
- loggingProcessName = [[NSMutableString alloc] initWithString:procName];
- [loggingProcessName replaceOccurrencesOfString:@" "
- withString:@"_"
- options:0
- range:NSMakeRange(0, [loggingProcessName length])];
- gLoggingProcessName = loggingProcessName;
- }
- return gLoggingProcessName;
- }
- + (void)setLoggingDateStamp:(NSString *)str {
- [gLoggingDateStamp release];
- gLoggingDateStamp = [str 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] autorelease];
- [formatter setFormatterBehavior:NSDateFormatterBehavior10_4];
- [formatter setDateFormat:@"M-dd_hh-mm-ssa"];
- gLoggingDateStamp = [[formatter stringFromDate:[NSDate date]] retain] ;
- }
- return gLoggingDateStamp;
- }
- + (NSString *)processNameLogPrefix {
- static NSString *prefix = nil;
- if (!prefix) {
- NSString *processName = [[self class] loggingProcessName];
- prefix = [[NSString alloc] initWithFormat:@"%@_log_", processName];
- }
- return prefix;
- }
- + (NSString *)symlinkNameSuffix {
- return @"_log_newest.html";
- }
- + (NSString *)htmlFileName {
- return @"aperçu_http_log.html";
- }
- // 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 == nil) 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 = [[self class] JSONObjectWithData:inputData];
- 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"];
- }
- }
- NSString *formatted = [[self class] stringWithJSONObject:obj];
- if (formatted) return formatted;
- }
- }
- #if !GTM_FOUNDATION_ONLY && !GTM_SKIP_LOG_XMLFORMAT
- // verify that this data starts with the bytes indicating XML
- NSString *const kXMLLintPath = @"/usr/bin/xmllint";
- static BOOL hasCheckedAvailability = NO;
- static BOOL isXMLLintAvailable;
- if (!hasCheckedAvailability) {
- isXMLLintAvailable = [[self class] fileOrDirExistsAtPath:kXMLLintPath];
- hasCheckedAvailability = YES;
- }
- if (isXMLLintAvailable
- && [inputData length] > 5
- && strncmp([inputData bytes], "<?xml", 5) == 0) {
- // call xmllint to format the data
- NSTask *task = [[[NSTask alloc] init] autorelease];
- [task setLaunchPath:kXMLLintPath];
- // use the dash argument to specify stdin as the source file
- [task setArguments:[NSArray arrayWithObjects:@"--format", @"-", nil]];
- [task setEnvironment:[NSDictionary dictionary]];
- 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] autorelease];
- return dataStr;
- }
- - (void)setupStreamLogging {
- // if logging is enabled, it needs a buffer to accumulate data from any
- // NSInputStream used for uploading. Logging will wrap the input
- // stream with a stream that lets us keep a copy the data being read.
- if ([GTMHTTPFetcher isLoggingEnabled]
- && loggedStreamData_ == nil
- && postStream_ != nil) {
- loggedStreamData_ = [[NSMutableData alloc] init];
- BOOL didCapture = [self logCapturePostStream];
- if (!didCapture) {
- // upload stream logging requires the class
- // GTMReadMonitorInputStream be available
- NSString const *str = @"<<Uploaded stream log unavailable without GTMReadMonitorInputStream>>";
- [loggedStreamData_ setData:[str dataUsingEncoding:NSUTF8StringEncoding]];
- }
- }
- }
- - (void)setLogRequestBody:(NSString *)bodyString {
- @synchronized(self) {
- [logRequestBody_ release];
- logRequestBody_ = [bodyString copy];
- }
- }
- - (NSString *)logRequestBody {
- @synchronized(self) {
- return logRequestBody_;
- }
- }
- - (void)setLogResponseBody:(NSString *)bodyString {
- @synchronized(self) {
- [logResponseBody_ release];
- logResponseBody_ = [bodyString copy];
- }
- }
- - (NSString *)logResponseBody {
- @synchronized(self) {
- return logResponseBody_;
- }
- }
- - (void)setShouldDeferResponseBodyLogging:(BOOL)flag {
- @synchronized(self) {
- if (flag != shouldDeferResponseBodyLogging_) {
- shouldDeferResponseBodyLogging_ = flag;
- if (!flag && !hasLoggedError_) {
- [self performSelectorOnMainThread:@selector(logFetchWithError:)
- withObject:nil
- waitUntilDone:NO];
- }
- }
- }
- }
- - (BOOL)shouldDeferResponseBodyLogging {
- @synchronized(self) {
- return shouldDeferResponseBodyLogging_;
- }
- }
- // 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 == nil) 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 = [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] autorelease];
- if (mungedStr != nil) {
- // 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 of the munged parts 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];
- NSRange range = NSMakeRange(offset, partSize);
- NSData *origPartData = [data subdataWithRange:range];
- NSString *origPartStr = [[[NSString alloc] initWithData:origPartData
- encoding:NSUTF8StringEncoding] autorelease];
- 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;
- // TODO: (grobbins) add Javascript to display response data formatted in hex
- NSString *parentDir = [[self class] loggingDirectory];
- NSString *processName = [[self class] loggingProcessName];
- NSString *dateStamp = [[self class] loggingDateStamp];
- NSString *logNamePrefix = [[self class] processNameLogPrefix];
- // make a directory for this run's logs, like
- // SyncProto_logs_10-16_01-56-58PM
- NSString *dirName = [NSString stringWithFormat:@"%@%@",
- logNamePrefix, dateStamp];
- NSString *logDirectory = [parentDir stringByAppendingPathComponent:dirName];
- if (gIsLoggingToFile) {
- // be sure that the first time this app runs, it's not writing to
- // a preexisting folder
- static BOOL shouldReuseFolder = NO;
- if (!shouldReuseFolder) {
- shouldReuseFolder = YES;
- NSString *origLogDir = logDirectory;
- for (int ctr = 2; ctr < 20; ctr++) {
- if (![[self class] fileOrDirExistsAtPath:logDirectory]) break;
- // append a digit
- logDirectory = [origLogDir stringByAppendingFormat:@"_%d", ctr];
- }
- }
- if (![[self class] makeDirectoryUpToPath:logDirectory]) return;
- }
- // 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 run of the app.
- //
- // 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 zResponseCounter = 0;
- int responseCounter = ++zResponseCounter;
- // file name for an image data file
- NSString *responseDataFileName = nil;
- NSUInteger responseDataLength;
- if (downloadFileHandle_) {
- responseDataLength = (NSUInteger) [downloadFileHandle_ offsetInFile];
- } else {
- responseDataLength = [downloadedData_ length];
- }
- NSURLResponse *response = [self response];
- NSDictionary *responseHeaders = [self responseHeaders];
- NSString *responseBaseName = nil;
- 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;
- NSData *dataToWrite = nil;
- if (responseDataLength > 0) {
- NSString *responseDataExtn = nil;
- // generate a response file base name like
- responseBaseName = [NSString stringWithFormat:@"fetch_%d_response",
- responseCounter];
- NSString *responseType = [responseHeaders valueForKey:@"Content-Type"];
- responseDataStr = [self formattedStringFromData:downloadedData_
- contentType:responseType
- JSON:&responseJSON];
- 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) {
- 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
- BOOL didFileExist = [[self class] fileOrDirExistsAtPath:htmlPath];
- NSMutableString* outputHTML = [NSMutableString string];
- NSURLRequest *request = [self mutableRequest];
- // 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, dateStamp];
- }
- // now write the visible html elements
- NSString *copyableFileName = [NSString stringWithFormat:@"fetch_%d.txt",
- responseCounter];
- // write the date & time, the comment, and the link to the plain-text
- // (copyable) log
- NSString *const dateLineFormat = @"<b>%@ ";
- [outputHTML appendFormat:dateLineFormat, [NSDate date]];
- NSString *comment = [self comment];
- if (comment) {
- NSString *const commentFormat = @"%@ ";
- [outputHTML appendFormat:commentFormat, comment];
- }
- NSString *const reqRespFormat = @"</b><a href='%@'><i>request/response log</i></a><br>";
- [outputHTML appendFormat:reqRespFormat, copyableFileName];
- // write the request URL
- NSString *requestMethod = [request HTTPMethod];
- NSURL *requestURL = [request URL];
- NSURL *redirectedFromHost = [[[redirectedFromURL_ host] copy] autorelease];
- // Save the request URL for next time in case this redirects.
- [redirectedFromURL_ release];
- redirectedFromURL_ = [requestURL copy];
- if (redirectedFromHost) {
- [outputHTML appendFormat:@"<FONT COLOR='#990066'><i>redirected from %@</i></FONT><br>",
- redirectedFromHost];
- }
- [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) {
- headerDetails = @" <i>authorized</i>";
- BOOL isInsecure = [[requestURL scheme] isEqual:@"http"];
- if (isInsecure) {
- headerDetails = @" <i>authorized, non-SSL</i>"
- "<FONT COLOR='#FF00FF'> ⚠</FONT> "; // 26A0 = ?
- }
- }
- NSString *cookiesHdr = [requestHeaders objectForKey:@"Cookie"];
- if (cookiesHdr) {
- headerDetails = [headerDetails stringByAppendingString:
- @" <i>cookies</i>"];
- }
- NSString *matchHdr = [requestHeaders objectForKey:@"If-Match"];
- if (matchHdr) {
- headerDetails = [headerDetails stringByAppendingString:
- @" <i>if-match</i>"];
- }
- matchHdr = [requestHeaders objectForKey:@"If-None-Match"];
- if (matchHdr) {
- headerDetails = [headerDetails stringByAppendingString:
- @" <i>if-none-match</i>"];
- }
- [outputHTML appendFormat:@" headers: %d %@<br>",
- (int)numberOfRequestHeaders, headerDetails];
- } else {
- [outputHTML appendFormat:@" headers: none<br>"];
- }
- // write the request post data, toggleable
- NSData *postData;
- if (loggedStreamData_) {
- postData = loggedStreamData_;
- } else if (postData_) {
- postData = postData_;
- } else {
- postData = [request_ HTTPBody];
- }
- NSString *postDataStr = nil;
- NSUInteger postDataLength = [postData length];
- NSString *postType = [requestHeaders valueForKey:@"Content-Type"];
- if (postDataLength > 0) {
- [outputHTML appendFormat:@" data: %d bytes, <code>%@</code><br>\n",
- (int)postDataLength, postType ? postType : @"<no type>"];
- if (logRequestBody_) {
- postDataStr = [[logRequestBody_ copy] autorelease];
- [logRequestBody_ release];
- logRequestBody_ = nil;
- } else {
- postDataStr = [self stringFromStreamData:postData
- contentType:postType];
- if (postDataStr) {
- // remove OAuth 2 client secret and refresh token
- postDataStr = [[self class] snipSubstringOfString:postDataStr
- betweenStartString:@"client_secret="
- endString:@"&"];
- postDataStr = [[self class] snipSubstringOfString:postDataStr
- betweenStartString:@"refresh_token="
- endString:@"&"];
- // remove ClientLogin password
- postDataStr = [[self class] snipSubstringOfString:postDataStr
- 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) {
- NSString *const jsonErrFmt = @" <i>JSON error:</i> <FONT"
- @" COLOR='#FF00FF'>%@ %@ ⚑</FONT>"; // 2691 = ?
- statusString = [statusString stringByAppendingFormat:jsonErrFmt,
- jsonCode ? jsonCode : @"",
- jsonMessage ? jsonMessage : @""];
- }
- }
- }
- } else {
- // purple for anything other than 200 or 201
- NSString *flag = (status >= 400 ? @" ⚑" : @""); // 2691 = ?
- NSString *const statusFormat = @"<FONT COLOR='#FF00FF'>%ld %@</FONT>";
- statusString = [NSString stringWithFormat:statusFormat,
- (long)status, 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> 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 ? @" <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 ? @" <FONT COLOR='#990066'>"
- "<i>redirects</i></FONT>" : @"");
- [outputHTML appendFormat:@" headers: %d %@ %@<br>\n",
- (int)numberOfResponseHeaders, cookiesStr, redirectsStr];
- } else {
- [outputHTML appendString:@" headers: none<br>\n"];
- }
- }
- // error
- if (error) {
- [outputHTML appendFormat:@"<b>Error:</b> %@ <br>\n", [error description]];
- }
- // Write the response data
- if (responseDataFileName) {
- NSString *escapedResponseFile = [responseDataFileName stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
- if (isResponseImage) {
- // Make a small inline image that links to the full image file
- [outputHTML appendFormat:@" data: %d bytes, <code>%@</code><br>",
- (int)responseDataLength, responseMIMEType];
- NSString *const fmt = @"<a href=\"%@\"><img src='%@' alt='image'"
- " style='border:solid thin;max-height:32'></a>\n";
- [outputHTML appendFormat:fmt,
- escapedResponseFile, escapedResponseFile];
- } else {
- // The response data was XML; link to the xml file
- NSString *const fmt = @" data: %d bytes, <code>"
- "%@</code> <i><a href=\"%@\">%@</a></i>\n";
- [outputHTML appendFormat:fmt,
- (int)responseDataLength, responseMIMEType,
- escapedResponseFile, [escapedResponseFile pathExtension]];
- }
- } else {
- // The response data was not an image; just show the length and MIME type
- [outputHTML appendFormat:@" data: %d bytes, <code>%@</code>\n",
- (int)responseDataLength, responseMIMEType];
- }
- // 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:@"%@\n", [NSDate date]];
- if (redirectedFromHost) {
- [copyable appendFormat:@"Redirected from %@\n", redirectedFromHost];
- }
- [copyable appendFormat:@"Request: %@ %@\n", requestMethod, requestURL];
- if ([requestHeaders count] > 0) {
- [copyable appendFormat:@"Request headers:\n%@\n",
- [[self class] headersStringForDictionary:requestHeaders]];
- }
- if (postDataLength > 0) {
- [copyable appendFormat:@"Request body: (%u bytes)\n",
- (unsigned int) postDataLength];
- if (postDataStr) {
- [copyable appendFormat:@"%@\n", postDataStr];
- }
- [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: (%u bytes)\n",
- (unsigned int) responseDataLength];
- if (responseDataLength > 0) {
- if (logResponseBody_) {
- responseDataStr = [[logResponseBody_ copy] autorelease];
- [logResponseBody_ release];
- logResponseBody_ = nil;
- }
- if (responseDataStr != nil) {
- [copyable appendFormat:@"%@\n", responseDataStr];
- } else if (status >= 400 && [temporaryDownloadPath_ length] > 0) {
- // Try to read in the saved data, which is probably a server error
- // message
- NSStringEncoding enc;
- responseDataStr = [NSString stringWithContentsOfFile:temporaryDownloadPath_
- usedEncoding:&enc
- error:NULL];
- if ([responseDataStr length] > 0) {
- [copyable appendFormat:@"%@\n", responseDataStr];
- } else {
- [copyable appendFormat:@"<<%u bytes to file>>\n",
- (unsigned int) responseDataLength];
- }
- } else {
- // Even though it's redundant, we'll put in text to indicate that all
- // the bytes are binary
- [copyable appendFormat:@"<<%u bytes>>\n",
- (unsigned int) 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 *copyablePath = [logDirectory stringByAppendingPathComponent:copyableFileName];
- NSError *copyableError = nil;
- if (![copyable writeToFile:copyablePath
- atomically:NO
- encoding:NSUTF8StringEncoding
- error:©ableError]) {
- // 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];
- [[self class] removeItemAtPath:symlinkPath];
- [[self class] createSymbolicLinkAtPath:symlinkPath
- withDestinationPath:htmlPath];
- #if GTM_IPHONE
- static BOOL gReportedLoggingPath = NO;
- if (!gReportedLoggingPath) {
- gReportedLoggingPath = YES;
- NSLog(@"GTMHTTPFetcher logging to \"%@\"", parentDir);
- }
- #endif
- }
- }
- - (BOOL)logCapturePostStream {
- // This is called when beginning a fetch. The caller should have already
- // verified that logging is enabled, and should have allocated
- // loggedStreamData_ as a mutable object.
- // If the class GTMReadMonitorInputStream is not available, bail now, since
- // we cannot capture this upload stream
- Class monitorClass = NSClassFromString(@"GTMReadMonitorInputStream");
- if (!monitorClass) return NO;
- // If we're logging, we need to wrap the upload stream with our monitor
- // stream that will call us back with the bytes being read from the stream
- // Our wrapper will retain the old post stream
- [postStream_ autorelease];
- postStream_ = [monitorClass inputStreamWithStream:postStream_];
- [postStream_ retain];
- [(GTMReadMonitorInputStream *)postStream_ setReadDelegate:self];
- [(GTMReadMonitorInputStream *)postStream_ setRunLoopModes:[self runLoopModes]];
- SEL readSel = @selector(inputStream:readIntoBuffer:length:);
- [(GTMReadMonitorInputStream *)postStream_ setReadSelector:readSel];
- return YES;
- }
- @end
- @implementation GTMHTTPFetcher (GTMHTTPFetcherLoggingUtilities)
- - (void)inputStream:(GTMReadMonitorInputStream *)stream
- readIntoBuffer:(void *)buffer
- length:(NSUInteger)length {
- // append the captured data
- [loggedStreamData_ appendBytes:buffer length:length];
- }
- #pragma mark Internal file routines
- // We implement plain Unix versions of NSFileManager methods to avoid
- // NSFileManager's issues with being used from multiple threads
- + (BOOL)fileOrDirExistsAtPath:(NSString *)path {
- struct stat buffer;
- int result = stat([path fileSystemRepresentation], &buffer);
- return (result == 0);
- }
- + (BOOL)makeDirectoryUpToPath:(NSString *)path {
- int result = 0;
- // Recursively create the parent directory of the requested path
- NSString *parent = [path stringByDeletingLastPathComponent];
- if (![self fileOrDirExistsAtPath:parent]) {
- result = [self makeDirectoryUpToPath:parent];
- }
- // Make the leaf directory
- if (result == 0 && ![self fileOrDirExistsAtPath:path]) {
- result = mkdir([path fileSystemRepresentation], S_IRWXU); // RWX for owner
- }
- return (result == 0);
- }
- + (BOOL)removeItemAtPath:(NSString *)path {
- int result = unlink([path fileSystemRepresentation]);
- return (result == 0);
- }
- + (BOOL)createSymbolicLinkAtPath:(NSString *)newPath
- withDestinationPath:(NSString *)targetPath {
- int result = symlink([targetPath fileSystemRepresentation],
- [newPath fileSystemRepresentation]);
- return (result == 0);
- }
- #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 == nil) 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;
- }
- + (id)JSONObjectWithData:(NSData *)data {
- Class serializer = NSClassFromString(@"NSJSONSerialization");
- if (serializer) {
- const NSUInteger kOpts = (1UL << 0); // NSJSONReadingMutableContainers
- NSMutableDictionary *obj;
- obj = [serializer JSONObjectWithData:data
- options:kOpts
- error:NULL];
- return obj;
- } else {
- // Try SBJsonParser or SBJSON
- Class jsonParseClass = NSClassFromString(@"SBJsonParser");
- if (!jsonParseClass) {
- jsonParseClass = NSClassFromString(@"SBJSON");
- }
- if (jsonParseClass) {
- GTMFetcherSBJSON *parser = [[[jsonParseClass alloc] init] autorelease];
- NSString *jsonStr = [[[NSString alloc] initWithData:data
- encoding:NSUTF8StringEncoding] autorelease];
- if (jsonStr) {
- NSMutableDictionary *obj = [parser objectWithString:jsonStr error:NULL];
- return obj;
- }
- }
- }
- return nil;
- }
- + (id)stringWithJSONObject:(id)obj {
- Class serializer = NSClassFromString(@"NSJSONSerialization");
- if (serializer) {
- const NSUInteger kOpts = (1UL << 0); // NSJSONWritingPrettyPrinted
- NSData *data;
- data = [serializer dataWithJSONObject:obj
- options:kOpts
- error:NULL];
- if (data) {
- NSString *jsonStr = [[[NSString alloc] initWithData:data
- encoding:NSUTF8StringEncoding] autorelease];
- return jsonStr;
- }
- } else {
- // Try SBJsonParser or SBJSON
- Class jsonWriterClass = NSClassFromString(@"SBJsonWriter");
- if (!jsonWriterClass) {
- jsonWriterClass = NSClassFromString(@"SBJSON");
- }
- if (jsonWriterClass) {
- GTMFetcherSBJSON *writer = [[[jsonWriterClass alloc] init] autorelease];
- [writer setHumanReadable:YES];
- NSString *jsonStr = [writer stringWithObject:obj error:NULL];
- return jsonStr;
- }
- }
- return nil;
- }
- @end
- #endif // !STRIP_GTM_FETCH_LOGGING