aboutsummaryrefslogtreecommitdiff
path: root/src/client/mac/sender/uploader.m
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/mac/sender/uploader.m')
-rw-r--r--src/client/mac/sender/uploader.m579
1 files changed, 579 insertions, 0 deletions
diff --git a/src/client/mac/sender/uploader.m b/src/client/mac/sender/uploader.m
new file mode 100644
index 00000000..45d98a54
--- /dev/null
+++ b/src/client/mac/sender/uploader.m
@@ -0,0 +1,579 @@
+// 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 <pwd.h>
+#import <sys/stat.h>
+#import <unistd.h>
+
+#import <SystemConfiguration/SystemConfiguration.h>
+
+#import "common/mac/HTTPMultipartUpload.h"
+
+#import "client/mac/sender/uploader.h"
+#import "common/mac/GTMLogger.h"
+
+
+const int kMinidumpFileLengthLimit = 800000;
+
+#define kApplePrefsSyncExcludeAllKey \
+ @"com.apple.PreferenceSync.ExcludeAllSyncKeys"
+
+NSString *const kGoogleServerType = @"google";
+NSString *const kSocorroServerType = @"socorro";
+NSString *const kDefaultServerType = @"google";
+
+#define GTMLoggerDebug NSLog
+
+#pragma mark -
+
+@interface Uploader(PrivateMethods)
+- (NSString *)readString;
+- (NSData *)readData:(ssize_t)length;
+
+- (BOOL)readConfigurationData;
+- (BOOL)readMinidumpData;
+- (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;
+
+// This method adds a key/value pair to the dictionary that
+// will be uploaded to the crash server.
+- (void)addServerParameter:(id)value forKey:(NSString *)key;
+
+// Records the uploaded crash ID to the log file.
+- (void)logUploadWithID:(const char *)uploadID;
+
+@end
+
+@implementation Uploader
+
+//=============================================================================
+- (id)initWithConfigFile:(const char *)configFile {
+ if ((self = [super init])) {
+
+ configFile_ = open(configFile, O_RDONLY, 0600);
+ if (configFile_ == -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 (configFile_ == -1) {
+ [self release];
+ return nil;
+ }
+
+ // 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];
+
+ if (![self readConfigurationData]) {
+ GTMLoggerDebug(@"uploader readConfigurationData failed");
+ [self release];
+ return nil;
+ }
+
+ // Read the minidump into memory.
+ [self readMinidumpData];
+ [self readLogFileData];
+
+ }
+ return self;
+}
+
+//=============================================================================
+- (NSString *)readString {
+ NSMutableString *str = [NSMutableString stringWithCapacity:32];
+ char ch[2] = { 0 };
+
+ while (read(configFile_, &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;
+}
+
+//=============================================================================
+- (NSData *)readData:(ssize_t)length {
+ NSMutableData *data = [NSMutableData dataWithLength:length];
+ char *bytes = (char *)[data bytes];
+
+ if (read(configFile_, bytes, length) != length)
+ return nil;
+
+ return data;
+}
+
+//=============================================================================
+- (BOOL)readConfigurationData {
+ parameters_ = [[NSMutableDictionary alloc] init];
+
+ while (1) {
+ NSString *key = [self readString];
+
+ if (![key length])
+ break;
+
+ // Read the data. Try to convert to a UTF-8 string, or just save
+ // the data
+ NSString *lenStr = [self readString];
+ ssize_t len = [lenStr intValue];
+ NSData *data = [self readData:len];
+ id value = [[NSString alloc] initWithData:data
+ encoding:NSUTF8StringEncoding];
+
+ // 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]) {
+ if (value) {
+ [self addServerParameter:value
+ forKey:urlParameterKey];
+ } else {
+ [self addServerParameter:data
+ forKey:urlParameterKey];
+ }
+ }
+ } else {
+ [parameters_ setObject:(value ? value : data) forKey:key];
+ }
+ [value release];
+ }
+
+ // 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"];
+
+ close(configFile_);
+ configFile_ = -1;
+
+ return YES;
+}
+
+// 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:@"%x%x%x",
+ clientId1, clientId2, clientId3];
+
+ [ud setObject:crashClientID forKey:kClientIdPreferenceKey];
+ [ud synchronize];
+ return crashClientID;
+}
+
+//=============================================================================
+- (BOOL)readLogFileData {
+ 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;
+}
+
+//=============================================================================
+- (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];
+
+ [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)report {
+ NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@BREAKPAD_URL]];
+ HTTPMultipartUpload *upload = [[HTTPMultipartUpload alloc] initWithURL:url];
+ NSMutableDictionary *uploadParameters = [NSMutableDictionary dictionary];
+
+ if (![self populateServerDictionary:uploadParameters]) {
+ return;
+ }
+
+ [upload setParameters:uploadParameters];
+
+ // Add minidump file
+ if (minidumpContents_) {
+ [upload addFileContents:minidumpContents_ name:@"upload_file_minidump"];
+
+ // Send it
+ NSError *error = nil;
+ NSData *data = [upload send:&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];
+ }
+
+ if (logFileData_) {
+ HTTPMultipartUpload *logUpload =
+ [[HTTPMultipartUpload alloc] initWithURL:url];
+
+ [uploadParameters setObject:@"log" forKey:@"type"];
+ [logUpload setParameters:uploadParameters];
+ [logUpload addFileContents:logFileData_ name:@"log"];
+
+ NSError *error = nil;
+ NSData *data = [logUpload send:&error];
+ NSString *result = [[NSString alloc] initWithData:data
+ encoding:NSUTF8StringEncoding];
+ [result release];
+ [logUpload release];
+ }
+
+ [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];
+ }
+}
+
+//=============================================================================
+- (void)dealloc {
+ [parameters_ release];
+ [minidumpContents_ release];
+ [logFileData_ release];
+ [googleDictionary_ release];
+ [socorroDictionary_ release];
+ [serverDictionary_ release];
+ [extraServerVars_ release];
+ [super dealloc];
+}
+
+@end