2012-08-25 10:38:29 +00:00
|
|
|
/* 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
|
|
|
|
@interface GTMReadMonitorInputStream : NSInputStream
|
|
|
|
+ (id)inputStreamWithStream:(NSInputStream *)input;
|
|
|
|
@property (assign) id readDelegate;
|
|
|
|
@property (assign) SEL readSelector;
|
|
|
|
@property (retain) NSArray *runLoopModes;
|
|
|
|
@end
|
|
|
|
|
|
|
|
// If GTMNSJSONSerialization is available, it is used for formatting JSON
|
2013-01-31 21:22:37 +00:00
|
|
|
#if (TARGET_OS_MAC && !TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED < 1070)) || \
|
|
|
|
(TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED < 50000))
|
2012-08-25 10:38:29 +00:00
|
|
|
@interface GTMNSJSONSerialization : NSObject
|
|
|
|
+ (NSData *)dataWithJSONObject:(id)obj options:(NSUInteger)opt error:(NSError **)error;
|
|
|
|
+ (id)JSONObjectWithData:(NSData *)data options:(NSUInteger)opt error:(NSError **)error;
|
|
|
|
@end
|
2013-01-31 21:22:37 +00:00
|
|
|
#endif
|
2012-08-25 10:38:29 +00:00
|
|
|
|
|
|
|
// 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, [gLoggingProcessName 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-04-27 21:14:05 +00:00
|
|
|
- (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) {
|
|
|
|
[self performSelectorOnMainThread:@selector(logFetchWithError:)
|
|
|
|
withObject:nil
|
|
|
|
waitUntilDone:NO];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)shouldDeferResponseBodyLogging {
|
|
|
|
@synchronized(self) {
|
|
|
|
return shouldDeferResponseBodyLogging_;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-08-25 10:38:29 +00:00
|
|
|
// 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];
|
|
|
|
|
|
|
|
// make a directory for this run's logs, like
|
|
|
|
// SyncProto_logs_10-16_01-56-58PM
|
|
|
|
NSString *dirName = [NSString stringWithFormat:@"%@_log_%@",
|
|
|
|
processName, 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 = @"aperçu_http_log.html";
|
|
|
|
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
|
2013-04-27 21:14:05 +00:00
|
|
|
NSString *const dateLineFormat = @"<b>%@ ";
|
2012-08-25 10:38:29 +00:00
|
|
|
[outputHTML appendFormat:dateLineFormat, [NSDate date]];
|
|
|
|
|
|
|
|
NSString *comment = [self comment];
|
|
|
|
if (comment) {
|
2013-04-27 21:14:05 +00:00
|
|
|
NSString *const commentFormat = @"%@ ";
|
2012-08-25 10:38:29 +00:00
|
|
|
[outputHTML appendFormat:commentFormat, comment];
|
|
|
|
}
|
|
|
|
|
2013-04-27 21:14:05 +00:00
|
|
|
NSString *const reqRespFormat = @"</b><a href='%@'><i>request/response log</i></a><br>";
|
2012-08-25 10:38:29 +00:00
|
|
|
[outputHTML appendFormat:reqRespFormat, copyableFileName];
|
|
|
|
|
|
|
|
// write the request URL
|
|
|
|
NSString *requestMethod = [request HTTPMethod];
|
|
|
|
NSURL *requestURL = [request URL];
|
|
|
|
[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>"];
|
|
|
|
|
2013-04-27 21:14:05 +00:00
|
|
|
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:@"&"];
|
|
|
|
}
|
2012-08-25 10:38:29 +00:00
|
|
|
}
|
|
|
|
} 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) {
|
2013-04-27 21:14:05 +00:00
|
|
|
NSString *const jsonErrFmt = @" <i>JSON error:</i> <FONT"
|
2012-08-25 10:38:29 +00:00
|
|
|
@" 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 = ⚑
|
2013-04-27 21:14:05 +00:00
|
|
|
NSString *const statusFormat = @"<FONT COLOR='#FF00FF'>%ld %@</FONT>";
|
2012-08-25 10:38:29 +00:00
|
|
|
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]]) {
|
2013-04-27 21:14:05 +00:00
|
|
|
NSString *const responseURLFormat = @"<FONT COLOR='#FF00FF'>response URL:"
|
2012-08-25 10:38:29 +00:00
|
|
|
"</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];
|
2013-04-27 21:14:05 +00:00
|
|
|
NSString *const fmt = @"<a href=\"%@\"><img src='%@' alt='image'"
|
2012-08-25 10:38:29 +00:00
|
|
|
" 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
|
2013-04-27 21:14:05 +00:00
|
|
|
NSString *const fmt = @" data: %d bytes, <code>"
|
2012-08-25 10:38:29 +00:00
|
|
|
"%@</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]];
|
|
|
|
[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) {
|
2013-04-27 21:14:05 +00:00
|
|
|
if (logResponseBody_) {
|
|
|
|
responseDataStr = [[logResponseBody_ copy] autorelease];
|
|
|
|
[logResponseBody_ release];
|
|
|
|
logResponseBody_ = nil;
|
|
|
|
}
|
2012-08-25 10:38:29 +00:00
|
|
|
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 *symlinkName = [NSString stringWithFormat:@"%@_log_newest.html",
|
|
|
|
processName];
|
|
|
|
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
|