// 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 #import #include #import #import #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]; } if (uploadCompletion_) { uploadCompletion_([NSString stringWithUTF8String:reportID], error); } // 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)setUploadCompletionBlock:(UploadCompletionBlock)uploadCompletion { uploadCompletion_ = uploadCompletion; } //============================================================================= - (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