blob: c74c05836a18983504391868803f3224b0b7f616 [file] [log] [blame]
// Copyright (c) 2011, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#import <fcntl.h>
#import <sys/stat.h>
#include <TargetConditionals.h>
#import <unistd.h>
#import <SystemConfiguration/SystemConfiguration.h>
#import "common/mac/HTTPMultipartUpload.h"
#import "client/apple/Framework/BreakpadDefines.h"
#import "client/mac/sender/uploader.h"
#import "common/mac/GTMLogger.h"
const int kMinidumpFileLengthLimit = 2 * 1024 * 1024; // 2MB
#define kApplePrefsSyncExcludeAllKey \
@"com.apple.PreferenceSync.ExcludeAllSyncKeys"
NSString *const kGoogleServerType = @"google";
NSString *const kSocorroServerType = @"socorro";
NSString *const kDefaultServerType = @"google";
#pragma mark -
namespace {
// Read one line from the configuration file.
NSString *readString(int fileId) {
NSMutableString *str = [NSMutableString stringWithCapacity:32];
char ch[2] = { 0 };
while (read(fileId, &ch[0], 1) == 1) {
if (ch[0] == '\n') {
// Break if this is the first newline after reading some other string
// data.
if ([str length])
break;
} else {
[str appendString:[NSString stringWithUTF8String:ch]];
}
}
return str;
}
//=============================================================================
// Read |length| of binary data from the configuration file. This method will
// returns |nil| in case of error.
NSData *readData(int fileId, ssize_t length) {
NSMutableData *data = [NSMutableData dataWithLength:length];
char *bytes = (char *)[data bytes];
if (read(fileId, bytes, length) != length)
return nil;
return data;
}
//=============================================================================
// Read the configuration from the config file.
NSDictionary *readConfigurationData(const char *configFile) {
int fileId = open(configFile, O_RDONLY, 0600);
if (fileId == -1) {
GTMLoggerDebug(@"Couldn't open config file %s - %s",
configFile,
strerror(errno));
}
// we want to avoid a build-up of old config files even if they
// have been incorrectly written by the framework
if (unlink(configFile)) {
GTMLoggerDebug(@"Couldn't unlink config file %s - %s",
configFile,
strerror(errno));
}
if (fileId == -1) {
return nil;
}
NSMutableDictionary *config = [NSMutableDictionary dictionary];
while (1) {
NSString *key = readString(fileId);
if (![key length])
break;
// Read the data. Try to convert to a UTF-8 string, or just save
// the data
NSString *lenStr = readString(fileId);
ssize_t len = [lenStr intValue];
NSData *data = readData(fileId, len);
id value = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
[config setObject:(value ? value : data) forKey:key];
[value release];
}
close(fileId);
return config;
}
} // namespace
#pragma mark -
@interface Uploader(PrivateMethods)
// Update |parameters_| as well as the server parameters using |config|.
- (void)translateConfigurationData:(NSDictionary *)config;
// Read the minidump referenced in |parameters_| and update |minidumpContents_|
// with its content.
- (BOOL)readMinidumpData;
// Read the log files referenced in |parameters_| and update |logFileData_|
// with their content.
- (BOOL)readLogFileData;
// Returns a unique client id (user-specific), creating a persistent
// one in the user defaults, if necessary.
- (NSString*)clientID;
// Returns a dictionary that can be used to map Breakpad parameter names to
// URL parameter names.
- (NSMutableDictionary *)dictionaryForServerType:(NSString *)serverType;
// Helper method to set HTTP parameters based on server type. This is
// called right before the upload - crashParameters will contain, on exit,
// URL parameters that should be sent with the minidump.
- (BOOL)populateServerDictionary:(NSMutableDictionary *)crashParameters;
// Initialization helper to create dictionaries mapping Breakpad
// parameters to URL parameters
- (void)createServerParameterDictionaries;
// Accessor method for the URL parameter dictionary
- (NSMutableDictionary *)urlParameterDictionary;
// Records the uploaded crash ID to the log file.
- (void)logUploadWithID:(const char *)uploadID;
// Builds an URL parameter for a given dictionary key. Uses Uploader's
// parameters to provide its value. Returns nil if no item is stored for the
// given key.
- (NSURLQueryItem *)queryItemWithName:(NSString *)queryItemName
forParamKey:(NSString *)key;
@end
@implementation Uploader
//=============================================================================
- (id)initWithConfigFile:(const char *)configFile {
NSDictionary *config = readConfigurationData(configFile);
if (!config)
return nil;
return [self initWithConfig:config];
}
//=============================================================================
- (id)initWithConfig:(NSDictionary *)config {
if ((self = [super init])) {
// Because the reporter is embedded in the framework (and many copies
// of the framework may exist) its not completely certain that the OS
// will obey the com.apple.PreferenceSync.ExcludeAllSyncKeys in our
// Info.plist. To make sure, also set the key directly if needed.
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
if (![ud boolForKey:kApplePrefsSyncExcludeAllKey]) {
[ud setBool:YES forKey:kApplePrefsSyncExcludeAllKey];
}
[self createServerParameterDictionaries];
[self translateConfigurationData:config];
// Read the minidump into memory.
[self readMinidumpData];
[self readLogFileData];
}
return self;
}
//=============================================================================
+ (NSDictionary *)readConfigurationDataFromFile:(NSString *)configFile {
return readConfigurationData([configFile fileSystemRepresentation]);
}
//=============================================================================
- (void)translateConfigurationData:(NSDictionary *)config {
parameters_ = [[NSMutableDictionary alloc] init];
NSEnumerator *it = [config keyEnumerator];
while (NSString *key = [it nextObject]) {
// If the keyname is prefixed by BREAKPAD_SERVER_PARAMETER_PREFIX
// that indicates that it should be uploaded to the server along
// with the minidump, so we treat it specially.
if ([key hasPrefix:@BREAKPAD_SERVER_PARAMETER_PREFIX]) {
NSString *urlParameterKey =
[key substringFromIndex:[@BREAKPAD_SERVER_PARAMETER_PREFIX length]];
if ([urlParameterKey length]) {
id value = [config objectForKey:key];
if ([value isKindOfClass:[NSString class]]) {
[self addServerParameter:(NSString *)value
forKey:urlParameterKey];
} else {
[self addServerParameter:(NSData *)value
forKey:urlParameterKey];
}
}
} else {
[parameters_ setObject:[config objectForKey:key] forKey:key];
}
}
// generate a unique client ID based on this host's MAC address
// then add a key/value pair for it
NSString *clientID = [self clientID];
[parameters_ setObject:clientID forKey:@"guid"];
}
// Per user per machine
- (NSString *)clientID {
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSString *crashClientID = [ud stringForKey:kClientIdPreferenceKey];
if (crashClientID) {
return crashClientID;
}
// Otherwise, if we have no client id, generate one!
srandom((int)[[NSDate date] timeIntervalSince1970]);
long clientId1 = random();
long clientId2 = random();
long clientId3 = random();
crashClientID = [NSString stringWithFormat:@"%lx%lx%lx",
clientId1, clientId2, clientId3];
[ud setObject:crashClientID forKey:kClientIdPreferenceKey];
[ud synchronize];
return crashClientID;
}
//=============================================================================
- (BOOL)readLogFileData {
#if TARGET_OS_IPHONE
return NO;
#else
unsigned int logFileCounter = 0;
NSString *logPath;
size_t logFileTailSize =
[[parameters_ objectForKey:@BREAKPAD_LOGFILE_UPLOAD_SIZE] intValue];
NSMutableArray *logFilenames; // An array of NSString, one per log file
logFilenames = [[NSMutableArray alloc] init];
char tmpDirTemplate[80] = "/tmp/CrashUpload-XXXXX";
char *tmpDir = mkdtemp(tmpDirTemplate);
// Construct key names for the keys we expect to contain log file paths
for(logFileCounter = 0;; logFileCounter++) {
NSString *logFileKey = [NSString stringWithFormat:@"%@%d",
@BREAKPAD_LOGFILE_KEY_PREFIX,
logFileCounter];
logPath = [parameters_ objectForKey:logFileKey];
// They should all be consecutive, so if we don't find one, assume
// we're done
if (!logPath) {
break;
}
NSData *entireLogFile = [[NSData alloc] initWithContentsOfFile:logPath];
if (entireLogFile == nil) {
continue;
}
NSRange fileRange;
// Truncate the log file, only if necessary
if ([entireLogFile length] <= logFileTailSize) {
fileRange = NSMakeRange(0, [entireLogFile length]);
} else {
fileRange = NSMakeRange([entireLogFile length] - logFileTailSize,
logFileTailSize);
}
char tmpFilenameTemplate[100];
// Generate a template based on the log filename
sprintf(tmpFilenameTemplate,"%s/%s-XXXX", tmpDir,
[[logPath lastPathComponent] fileSystemRepresentation]);
char *tmpFile = mktemp(tmpFilenameTemplate);
NSData *logSubdata = [entireLogFile subdataWithRange:fileRange];
NSString *tmpFileString = [NSString stringWithUTF8String:tmpFile];
[logSubdata writeToFile:tmpFileString atomically:NO];
[logFilenames addObject:[tmpFileString lastPathComponent]];
[entireLogFile release];
}
if ([logFilenames count] == 0) {
[logFilenames release];
logFileData_ = nil;
return NO;
}
// now, bzip all files into one
NSTask *tarTask = [[NSTask alloc] init];
[tarTask setCurrentDirectoryPath:[NSString stringWithUTF8String:tmpDir]];
[tarTask setLaunchPath:@"/usr/bin/tar"];
NSMutableArray *bzipArgs = [NSMutableArray arrayWithObjects:@"-cjvf",
@"log.tar.bz2",nil];
[bzipArgs addObjectsFromArray:logFilenames];
[logFilenames release];
[tarTask setArguments:bzipArgs];
[tarTask launch];
[tarTask waitUntilExit];
[tarTask release];
NSString *logTarFile = [NSString stringWithFormat:@"%s/log.tar.bz2",tmpDir];
logFileData_ = [[NSData alloc] initWithContentsOfFile:logTarFile];
if (logFileData_ == nil) {
GTMLoggerDebug(@"Cannot find temp tar log file: %@", logTarFile);
return NO;
}
return YES;
#endif // TARGET_OS_IPHONE
}
//=============================================================================
- (BOOL)readMinidumpData {
NSString *minidumpDir =
[parameters_ objectForKey:@kReporterMinidumpDirectoryKey];
NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey];
if (![minidumpID length])
return NO;
NSString *path = [minidumpDir stringByAppendingPathComponent:minidumpID];
path = [path stringByAppendingPathExtension:@"dmp"];
// check the size of the minidump and limit it to a reasonable size
// before attempting to load into memory and upload
const char *fileName = [path fileSystemRepresentation];
struct stat fileStatus;
BOOL success = YES;
if (!stat(fileName, &fileStatus)) {
if (fileStatus.st_size > kMinidumpFileLengthLimit) {
fprintf(stderr, "Breakpad Uploader: minidump file too large " \
"to upload : %d\n", (int)fileStatus.st_size);
success = NO;
}
} else {
fprintf(stderr, "Breakpad Uploader: unable to determine minidump " \
"file length\n");
success = NO;
}
if (success) {
minidumpContents_ = [[NSData alloc] initWithContentsOfFile:path];
success = ([minidumpContents_ length] ? YES : NO);
}
if (!success) {
// something wrong with the minidump file -- delete it
unlink(fileName);
}
return success;
}
#pragma mark -
//=============================================================================
- (void)createServerParameterDictionaries {
serverDictionary_ = [[NSMutableDictionary alloc] init];
socorroDictionary_ = [[NSMutableDictionary alloc] init];
googleDictionary_ = [[NSMutableDictionary alloc] init];
extraServerVars_ = [[NSMutableDictionary alloc] init];
[serverDictionary_ setObject:socorroDictionary_ forKey:kSocorroServerType];
[serverDictionary_ setObject:googleDictionary_ forKey:kGoogleServerType];
[googleDictionary_ setObject:@"ptime" forKey:@BREAKPAD_PROCESS_UP_TIME];
[googleDictionary_ setObject:@"email" forKey:@BREAKPAD_EMAIL];
[googleDictionary_ setObject:@"comments" forKey:@BREAKPAD_COMMENTS];
[googleDictionary_ setObject:@"prod" forKey:@BREAKPAD_PRODUCT];
[googleDictionary_ setObject:@"ver" forKey:@BREAKPAD_VERSION];
[googleDictionary_ setObject:@"guid" forKey:@"guid"];
[socorroDictionary_ setObject:@"Comments" forKey:@BREAKPAD_COMMENTS];
[socorroDictionary_ setObject:@"CrashTime"
forKey:@BREAKPAD_PROCESS_CRASH_TIME];
[socorroDictionary_ setObject:@"StartupTime"
forKey:@BREAKPAD_PROCESS_START_TIME];
[socorroDictionary_ setObject:@"Version"
forKey:@BREAKPAD_VERSION];
[socorroDictionary_ setObject:@"ProductName"
forKey:@BREAKPAD_PRODUCT];
[socorroDictionary_ setObject:@"Email"
forKey:@BREAKPAD_EMAIL];
}
- (NSMutableDictionary *)dictionaryForServerType:(NSString *)serverType {
if (serverType == nil || [serverType length] == 0) {
return [serverDictionary_ objectForKey:kDefaultServerType];
}
return [serverDictionary_ objectForKey:serverType];
}
- (NSMutableDictionary *)urlParameterDictionary {
NSString *serverType = [parameters_ objectForKey:@BREAKPAD_SERVER_TYPE];
return [self dictionaryForServerType:serverType];
}
- (BOOL)populateServerDictionary:(NSMutableDictionary *)crashParameters {
NSDictionary *urlParameterNames = [self urlParameterDictionary];
id key;
NSEnumerator *enumerator = [parameters_ keyEnumerator];
while ((key = [enumerator nextObject])) {
// The key from parameters_ corresponds to a key in
// urlParameterNames. The value in parameters_ gets stored in
// crashParameters with a key that is the value in
// urlParameterNames.
// For instance, if parameters_ has [PRODUCT_NAME => "FOOBAR"] and
// urlParameterNames has [PRODUCT_NAME => "pname"] the final HTTP
// URL parameter becomes [pname => "FOOBAR"].
NSString *breakpadParameterName = (NSString *)key;
NSString *urlParameter = [urlParameterNames
objectForKey:breakpadParameterName];
if (urlParameter) {
[crashParameters setObject:[parameters_ objectForKey:key]
forKey:urlParameter];
}
}
// Now, add the parameters that were added by the application.
enumerator = [extraServerVars_ keyEnumerator];
while ((key = [enumerator nextObject])) {
NSString *urlParameterName = (NSString *)key;
NSString *urlParameterValue =
[extraServerVars_ objectForKey:urlParameterName];
[crashParameters setObject:urlParameterValue
forKey:urlParameterName];
}
return YES;
}
- (void)addServerParameter:(id)value forKey:(NSString *)key {
[extraServerVars_ setObject:value forKey:key];
}
//=============================================================================
- (void)handleNetworkResponse:(NSData *)data withError:(NSError *)error {
NSString *result = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
const char *reportID = "ERR";
if (error) {
fprintf(stderr, "Breakpad Uploader: Send Error: %s\n",
[[error description] UTF8String]);
} else {
NSCharacterSet *trimSet =
[NSCharacterSet whitespaceAndNewlineCharacterSet];
reportID = [[result stringByTrimmingCharactersInSet:trimSet] UTF8String];
[self logUploadWithID:reportID];
}
// rename the minidump file according to the id returned from the server
NSString *minidumpDir =
[parameters_ objectForKey:@kReporterMinidumpDirectoryKey];
NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey];
NSString *srcString = [NSString stringWithFormat:@"%@/%@.dmp",
minidumpDir, minidumpID];
NSString *destString = [NSString stringWithFormat:@"%@/%s.dmp",
minidumpDir, reportID];
const char *src = [srcString fileSystemRepresentation];
const char *dest = [destString fileSystemRepresentation];
if (rename(src, dest) == 0) {
GTMLoggerInfo(@"Breakpad Uploader: Renamed %s to %s after successful " \
"upload",src, dest);
}
else {
// can't rename - don't worry - it's not important for users
GTMLoggerDebug(@"Breakpad Uploader: successful upload report ID = %s\n",
reportID );
}
[result release];
}
//=============================================================================
- (NSURLQueryItem *)queryItemWithName:(NSString *)queryItemName
forParamKey:(NSString *)key {
NSString *value = [parameters_ objectForKey:key];
NSString *escapedValue =
[value stringByAddingPercentEncodingWithAllowedCharacters:
[NSCharacterSet URLQueryAllowedCharacterSet]];
return [NSURLQueryItem queryItemWithName:queryItemName value:escapedValue];
}
//=============================================================================
- (void)report {
NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@BREAKPAD_URL]];
NSString *serverType = [parameters_ objectForKey:@BREAKPAD_SERVER_TYPE];
if ([serverType length] == 0 ||
[serverType isEqualToString:kGoogleServerType]) {
// when communicating to Google's crash collecting service, add URL params
// which identify the product
NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url
resolvingAgainstBaseURL:false];
NSMutableArray *queryItemsToAdd = [urlComponents.queryItems mutableCopy];
if (queryItemsToAdd == nil) {
queryItemsToAdd = [[NSMutableArray alloc] init];
}
NSURLQueryItem *queryItemProduct =
[self queryItemWithName:@"product" forParamKey:@BREAKPAD_PRODUCT];
NSURLQueryItem *queryItemVersion =
[self queryItemWithName:@"version" forParamKey:@BREAKPAD_VERSION];
NSURLQueryItem *queryItemGuid =
[self queryItemWithName:@"guid" forParamKey:@"guid"];
if (queryItemProduct != nil) [queryItemsToAdd addObject:queryItemProduct];
if (queryItemVersion != nil) [queryItemsToAdd addObject:queryItemVersion];
if (queryItemGuid != nil) [queryItemsToAdd addObject:queryItemGuid];
urlComponents.queryItems = queryItemsToAdd;
url = [urlComponents URL];
}
HTTPMultipartUpload *upload = [[HTTPMultipartUpload alloc] initWithURL:url];
NSMutableDictionary *uploadParameters = [NSMutableDictionary dictionary];
if (![self populateServerDictionary:uploadParameters]) {
[upload release];
return;
}
[upload setParameters:uploadParameters];
// Add minidump file
if (minidumpContents_) {
[upload addFileContents:minidumpContents_ name:@"upload_file_minidump"];
// If there is a log file, upload it together with the minidump.
if (logFileData_) {
[upload addFileContents:logFileData_ name:@"log"];
}
// Send it
NSError *error = nil;
NSData *data = [upload send:&error];
if (![url isFileURL]) {
[self handleNetworkResponse:data withError:error];
} else {
if (error) {
fprintf(stderr, "Breakpad Uploader: Error writing request file: %s\n",
[[error description] UTF8String]);
}
}
} else {
// Minidump is missing -- upload just the log file.
if (logFileData_) {
[self uploadData:logFileData_ name:@"log"];
}
}
[upload release];
}
- (void)uploadData:(NSData *)data name:(NSString *)name {
NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@BREAKPAD_URL]];
NSMutableDictionary *uploadParameters = [NSMutableDictionary dictionary];
if (![self populateServerDictionary:uploadParameters])
return;
HTTPMultipartUpload *upload =
[[HTTPMultipartUpload alloc] initWithURL:url];
[uploadParameters setObject:name forKey:@"type"];
[upload setParameters:uploadParameters];
[upload addFileContents:data name:name];
[upload send:nil];
[upload release];
}
- (void)logUploadWithID:(const char *)uploadID {
NSString *minidumpDir =
[parameters_ objectForKey:@kReporterMinidumpDirectoryKey];
NSString *logFilePath = [NSString stringWithFormat:@"%@/%s",
minidumpDir, kReporterLogFilename];
NSString *logLine = [NSString stringWithFormat:@"%0.f,%s\n",
[[NSDate date] timeIntervalSince1970], uploadID];
NSData *logData = [logLine dataUsingEncoding:NSUTF8StringEncoding];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:logFilePath]) {
NSFileHandle *logFileHandle =
[NSFileHandle fileHandleForWritingAtPath:logFilePath];
[logFileHandle seekToEndOfFile];
[logFileHandle writeData:logData];
[logFileHandle closeFile];
} else {
[fileManager createFileAtPath:logFilePath
contents:logData
attributes:nil];
}
}
//=============================================================================
- (NSMutableDictionary *)parameters {
return parameters_;
}
//=============================================================================
- (void)dealloc {
[parameters_ release];
[minidumpContents_ release];
[logFileData_ release];
[googleDictionary_ release];
[socorroDictionary_ release];
[serverDictionary_ release];
[extraServerVars_ release];
[super dealloc];
}
@end