123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982 |
- /* 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>%@ ", now];
-
- NSString *comment = [self comment];
- if (comment.length > 0) {
- [outputHTML appendFormat:@"%@ ", 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 =
- @" <i>authorized, non-SSL</i><FONT COLOR='#FF00FF'> ⚠</FONT> ";
- } else {
- headerDetails = @" <i>authorized</i>";
- }
- }
- 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
- 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:@" 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 =
- @" <i>JSON error:</i> <FONT COLOR='#FF00FF'>%@ %@ ⚑</FONT>";
- statusString = [statusString stringByAppendingFormat:jsonErrFmt,
- jsonCode ? jsonCode : @"",
- jsonMessage ? jsonMessage : @""];
- }
- }
- }
- } else {
- // purple for anything other than 200 or 201
- NSString *flag = status >= 400 ? @" ⚑" : @""; // 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> 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) {
- if (isResponseImage) {
- // Make a small inline image that links to the full image file
- [outputHTML appendFormat:@" 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 =
- @" data: %lld bytes, <code>%@</code> <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:@" 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:©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];
-
- [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
|