1// Copyright (c) 2011, Google Inc. 2// All rights reserved. 3// 4// Redistribution and use in source and binary forms, with or without 5// modification, are permitted provided that the following conditions are 6// met: 7// 8// * Redistributions of source code must retain the above copyright 9// notice, this list of conditions and the following disclaimer. 10// * Redistributions in binary form must reproduce the above 11// copyright notice, this list of conditions and the following disclaimer 12// in the documentation and/or other materials provided with the 13// distribution. 14// * Neither the name of Google Inc. nor the names of its 15// contributors may be used to endorse or promote products derived from 16// this software without specific prior written permission. 17// 18// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30#import <fcntl.h> 31#import <sys/stat.h> 32#include <TargetConditionals.h> 33#import <unistd.h> 34 35#import <SystemConfiguration/SystemConfiguration.h> 36 37#import "common/mac/HTTPMultipartUpload.h" 38 39#import "client/apple/Framework/BreakpadDefines.h" 40#import "client/mac/sender/uploader.h" 41#import "common/mac/GTMLogger.h" 42 43const int kMinidumpFileLengthLimit = 2 * 1024 * 1024; // 2MB 44 45#define kApplePrefsSyncExcludeAllKey \ 46 @"com.apple.PreferenceSync.ExcludeAllSyncKeys" 47 48NSString *const kGoogleServerType = @"google"; 49NSString *const kSocorroServerType = @"socorro"; 50NSString *const kDefaultServerType = @"google"; 51 52#pragma mark - 53 54namespace { 55// Read one line from the configuration file. 56NSString *readString(int fileId) { 57 NSMutableString *str = [NSMutableString stringWithCapacity:32]; 58 char ch[2] = { 0 }; 59 60 while (read(fileId, &ch[0], 1) == 1) { 61 if (ch[0] == '\n') { 62 // Break if this is the first newline after reading some other string 63 // data. 64 if ([str length]) 65 break; 66 } else { 67 [str appendString:[NSString stringWithUTF8String:ch]]; 68 } 69 } 70 71 return str; 72} 73 74//============================================================================= 75// Read |length| of binary data from the configuration file. This method will 76// returns |nil| in case of error. 77NSData *readData(int fileId, ssize_t length) { 78 NSMutableData *data = [NSMutableData dataWithLength:length]; 79 char *bytes = (char *)[data bytes]; 80 81 if (read(fileId, bytes, length) != length) 82 return nil; 83 84 return data; 85} 86 87//============================================================================= 88// Read the configuration from the config file. 89NSDictionary *readConfigurationData(const char *configFile) { 90 int fileId = open(configFile, O_RDONLY, 0600); 91 if (fileId == -1) { 92 GTMLoggerDebug(@"Couldn't open config file %s - %s", 93 configFile, 94 strerror(errno)); 95 } 96 97 // we want to avoid a build-up of old config files even if they 98 // have been incorrectly written by the framework 99 if (unlink(configFile)) { 100 GTMLoggerDebug(@"Couldn't unlink config file %s - %s", 101 configFile, 102 strerror(errno)); 103 } 104 105 if (fileId == -1) { 106 return nil; 107 } 108 109 NSMutableDictionary *config = [NSMutableDictionary dictionary]; 110 111 while (1) { 112 NSString *key = readString(fileId); 113 114 if (![key length]) 115 break; 116 117 // Read the data. Try to convert to a UTF-8 string, or just save 118 // the data 119 NSString *lenStr = readString(fileId); 120 ssize_t len = [lenStr intValue]; 121 NSData *data = readData(fileId, len); 122 id value = [[NSString alloc] initWithData:data 123 encoding:NSUTF8StringEncoding]; 124 125 [config setObject:(value ? value : data) forKey:key]; 126 [value release]; 127 } 128 129 close(fileId); 130 return config; 131} 132} // namespace 133 134#pragma mark - 135 136@interface Uploader(PrivateMethods) 137 138// Update |parameters_| as well as the server parameters using |config|. 139- (void)translateConfigurationData:(NSDictionary *)config; 140 141// Read the minidump referenced in |parameters_| and update |minidumpContents_| 142// with its content. 143- (BOOL)readMinidumpData; 144 145// Read the log files referenced in |parameters_| and update |logFileData_| 146// with their content. 147- (BOOL)readLogFileData; 148 149// Returns a unique client id (user-specific), creating a persistent 150// one in the user defaults, if necessary. 151- (NSString*)clientID; 152 153// Returns a dictionary that can be used to map Breakpad parameter names to 154// URL parameter names. 155- (NSMutableDictionary *)dictionaryForServerType:(NSString *)serverType; 156 157// Helper method to set HTTP parameters based on server type. This is 158// called right before the upload - crashParameters will contain, on exit, 159// URL parameters that should be sent with the minidump. 160- (BOOL)populateServerDictionary:(NSMutableDictionary *)crashParameters; 161 162// Initialization helper to create dictionaries mapping Breakpad 163// parameters to URL parameters 164- (void)createServerParameterDictionaries; 165 166// Accessor method for the URL parameter dictionary 167- (NSMutableDictionary *)urlParameterDictionary; 168 169// Records the uploaded crash ID to the log file. 170- (void)logUploadWithID:(const char *)uploadID; 171@end 172 173@implementation Uploader 174 175//============================================================================= 176- (id)initWithConfigFile:(const char *)configFile { 177 NSDictionary *config = readConfigurationData(configFile); 178 if (!config) 179 return nil; 180 181 return [self initWithConfig:config]; 182} 183 184//============================================================================= 185- (id)initWithConfig:(NSDictionary *)config { 186 if ((self = [super init])) { 187 // Because the reporter is embedded in the framework (and many copies 188 // of the framework may exist) its not completely certain that the OS 189 // will obey the com.apple.PreferenceSync.ExcludeAllSyncKeys in our 190 // Info.plist. To make sure, also set the key directly if needed. 191 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; 192 if (![ud boolForKey:kApplePrefsSyncExcludeAllKey]) { 193 [ud setBool:YES forKey:kApplePrefsSyncExcludeAllKey]; 194 } 195 196 [self createServerParameterDictionaries]; 197 198 [self translateConfigurationData:config]; 199 200 // Read the minidump into memory. 201 [self readMinidumpData]; 202 [self readLogFileData]; 203 } 204 return self; 205} 206 207//============================================================================= 208+ (NSDictionary *)readConfigurationDataFromFile:(NSString *)configFile { 209 return readConfigurationData([configFile fileSystemRepresentation]); 210} 211 212//============================================================================= 213- (void)translateConfigurationData:(NSDictionary *)config { 214 parameters_ = [[NSMutableDictionary alloc] init]; 215 216 NSEnumerator *it = [config keyEnumerator]; 217 while (NSString *key = [it nextObject]) { 218 // If the keyname is prefixed by BREAKPAD_SERVER_PARAMETER_PREFIX 219 // that indicates that it should be uploaded to the server along 220 // with the minidump, so we treat it specially. 221 if ([key hasPrefix:@BREAKPAD_SERVER_PARAMETER_PREFIX]) { 222 NSString *urlParameterKey = 223 [key substringFromIndex:[@BREAKPAD_SERVER_PARAMETER_PREFIX length]]; 224 if ([urlParameterKey length]) { 225 id value = [config objectForKey:key]; 226 if ([value isKindOfClass:[NSString class]]) { 227 [self addServerParameter:(NSString *)value 228 forKey:urlParameterKey]; 229 } else { 230 [self addServerParameter:(NSData *)value 231 forKey:urlParameterKey]; 232 } 233 } 234 } else { 235 [parameters_ setObject:[config objectForKey:key] forKey:key]; 236 } 237 } 238 239 // generate a unique client ID based on this host's MAC address 240 // then add a key/value pair for it 241 NSString *clientID = [self clientID]; 242 [parameters_ setObject:clientID forKey:@"guid"]; 243} 244 245// Per user per machine 246- (NSString *)clientID { 247 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; 248 NSString *crashClientID = [ud stringForKey:kClientIdPreferenceKey]; 249 if (crashClientID) { 250 return crashClientID; 251 } 252 253 // Otherwise, if we have no client id, generate one! 254 srandom((int)[[NSDate date] timeIntervalSince1970]); 255 long clientId1 = random(); 256 long clientId2 = random(); 257 long clientId3 = random(); 258 crashClientID = [NSString stringWithFormat:@"%lx%lx%lx", 259 clientId1, clientId2, clientId3]; 260 261 [ud setObject:crashClientID forKey:kClientIdPreferenceKey]; 262 [ud synchronize]; 263 return crashClientID; 264} 265 266//============================================================================= 267- (BOOL)readLogFileData { 268#if TARGET_OS_IPHONE 269 return NO; 270#else 271 unsigned int logFileCounter = 0; 272 273 NSString *logPath; 274 size_t logFileTailSize = 275 [[parameters_ objectForKey:@BREAKPAD_LOGFILE_UPLOAD_SIZE] intValue]; 276 277 NSMutableArray *logFilenames; // An array of NSString, one per log file 278 logFilenames = [[NSMutableArray alloc] init]; 279 280 char tmpDirTemplate[80] = "/tmp/CrashUpload-XXXXX"; 281 char *tmpDir = mkdtemp(tmpDirTemplate); 282 283 // Construct key names for the keys we expect to contain log file paths 284 for(logFileCounter = 0;; logFileCounter++) { 285 NSString *logFileKey = [NSString stringWithFormat:@"%@%d", 286 @BREAKPAD_LOGFILE_KEY_PREFIX, 287 logFileCounter]; 288 289 logPath = [parameters_ objectForKey:logFileKey]; 290 291 // They should all be consecutive, so if we don't find one, assume 292 // we're done 293 294 if (!logPath) { 295 break; 296 } 297 298 NSData *entireLogFile = [[NSData alloc] initWithContentsOfFile:logPath]; 299 300 if (entireLogFile == nil) { 301 continue; 302 } 303 304 NSRange fileRange; 305 306 // Truncate the log file, only if necessary 307 308 if ([entireLogFile length] <= logFileTailSize) { 309 fileRange = NSMakeRange(0, [entireLogFile length]); 310 } else { 311 fileRange = NSMakeRange([entireLogFile length] - logFileTailSize, 312 logFileTailSize); 313 } 314 315 char tmpFilenameTemplate[100]; 316 317 // Generate a template based on the log filename 318 sprintf(tmpFilenameTemplate,"%s/%s-XXXX", tmpDir, 319 [[logPath lastPathComponent] fileSystemRepresentation]); 320 321 char *tmpFile = mktemp(tmpFilenameTemplate); 322 323 NSData *logSubdata = [entireLogFile subdataWithRange:fileRange]; 324 NSString *tmpFileString = [NSString stringWithUTF8String:tmpFile]; 325 [logSubdata writeToFile:tmpFileString atomically:NO]; 326 327 [logFilenames addObject:[tmpFileString lastPathComponent]]; 328 [entireLogFile release]; 329 } 330 331 if ([logFilenames count] == 0) { 332 [logFilenames release]; 333 logFileData_ = nil; 334 return NO; 335 } 336 337 // now, bzip all files into one 338 NSTask *tarTask = [[NSTask alloc] init]; 339 340 [tarTask setCurrentDirectoryPath:[NSString stringWithUTF8String:tmpDir]]; 341 [tarTask setLaunchPath:@"/usr/bin/tar"]; 342 343 NSMutableArray *bzipArgs = [NSMutableArray arrayWithObjects:@"-cjvf", 344 @"log.tar.bz2",nil]; 345 [bzipArgs addObjectsFromArray:logFilenames]; 346 347 [logFilenames release]; 348 349 [tarTask setArguments:bzipArgs]; 350 [tarTask launch]; 351 [tarTask waitUntilExit]; 352 [tarTask release]; 353 354 NSString *logTarFile = [NSString stringWithFormat:@"%s/log.tar.bz2",tmpDir]; 355 logFileData_ = [[NSData alloc] initWithContentsOfFile:logTarFile]; 356 if (logFileData_ == nil) { 357 GTMLoggerDebug(@"Cannot find temp tar log file: %@", logTarFile); 358 return NO; 359 } 360 return YES; 361#endif // TARGET_OS_IPHONE 362} 363 364//============================================================================= 365- (BOOL)readMinidumpData { 366 NSString *minidumpDir = 367 [parameters_ objectForKey:@kReporterMinidumpDirectoryKey]; 368 NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey]; 369 370 if (![minidumpID length]) 371 return NO; 372 373 NSString *path = [minidumpDir stringByAppendingPathComponent:minidumpID]; 374 path = [path stringByAppendingPathExtension:@"dmp"]; 375 376 // check the size of the minidump and limit it to a reasonable size 377 // before attempting to load into memory and upload 378 const char *fileName = [path fileSystemRepresentation]; 379 struct stat fileStatus; 380 381 BOOL success = YES; 382 383 if (!stat(fileName, &fileStatus)) { 384 if (fileStatus.st_size > kMinidumpFileLengthLimit) { 385 fprintf(stderr, "Breakpad Uploader: minidump file too large " \ 386 "to upload : %d\n", (int)fileStatus.st_size); 387 success = NO; 388 } 389 } else { 390 fprintf(stderr, "Breakpad Uploader: unable to determine minidump " \ 391 "file length\n"); 392 success = NO; 393 } 394 395 if (success) { 396 minidumpContents_ = [[NSData alloc] initWithContentsOfFile:path]; 397 success = ([minidumpContents_ length] ? YES : NO); 398 } 399 400 if (!success) { 401 // something wrong with the minidump file -- delete it 402 unlink(fileName); 403 } 404 405 return success; 406} 407 408#pragma mark - 409//============================================================================= 410 411- (void)createServerParameterDictionaries { 412 serverDictionary_ = [[NSMutableDictionary alloc] init]; 413 socorroDictionary_ = [[NSMutableDictionary alloc] init]; 414 googleDictionary_ = [[NSMutableDictionary alloc] init]; 415 extraServerVars_ = [[NSMutableDictionary alloc] init]; 416 417 [serverDictionary_ setObject:socorroDictionary_ forKey:kSocorroServerType]; 418 [serverDictionary_ setObject:googleDictionary_ forKey:kGoogleServerType]; 419 420 [googleDictionary_ setObject:@"ptime" forKey:@BREAKPAD_PROCESS_UP_TIME]; 421 [googleDictionary_ setObject:@"email" forKey:@BREAKPAD_EMAIL]; 422 [googleDictionary_ setObject:@"comments" forKey:@BREAKPAD_COMMENTS]; 423 [googleDictionary_ setObject:@"prod" forKey:@BREAKPAD_PRODUCT]; 424 [googleDictionary_ setObject:@"ver" forKey:@BREAKPAD_VERSION]; 425 [googleDictionary_ setObject:@"guid" forKey:@"guid"]; 426 427 [socorroDictionary_ setObject:@"Comments" forKey:@BREAKPAD_COMMENTS]; 428 [socorroDictionary_ setObject:@"CrashTime" 429 forKey:@BREAKPAD_PROCESS_CRASH_TIME]; 430 [socorroDictionary_ setObject:@"StartupTime" 431 forKey:@BREAKPAD_PROCESS_START_TIME]; 432 [socorroDictionary_ setObject:@"Version" 433 forKey:@BREAKPAD_VERSION]; 434 [socorroDictionary_ setObject:@"ProductName" 435 forKey:@BREAKPAD_PRODUCT]; 436 [socorroDictionary_ setObject:@"Email" 437 forKey:@BREAKPAD_EMAIL]; 438} 439 440- (NSMutableDictionary *)dictionaryForServerType:(NSString *)serverType { 441 if (serverType == nil || [serverType length] == 0) { 442 return [serverDictionary_ objectForKey:kDefaultServerType]; 443 } 444 return [serverDictionary_ objectForKey:serverType]; 445} 446 447- (NSMutableDictionary *)urlParameterDictionary { 448 NSString *serverType = [parameters_ objectForKey:@BREAKPAD_SERVER_TYPE]; 449 return [self dictionaryForServerType:serverType]; 450 451} 452 453- (BOOL)populateServerDictionary:(NSMutableDictionary *)crashParameters { 454 NSDictionary *urlParameterNames = [self urlParameterDictionary]; 455 456 id key; 457 NSEnumerator *enumerator = [parameters_ keyEnumerator]; 458 459 while ((key = [enumerator nextObject])) { 460 // The key from parameters_ corresponds to a key in 461 // urlParameterNames. The value in parameters_ gets stored in 462 // crashParameters with a key that is the value in 463 // urlParameterNames. 464 465 // For instance, if parameters_ has [PRODUCT_NAME => "FOOBAR"] and 466 // urlParameterNames has [PRODUCT_NAME => "pname"] the final HTTP 467 // URL parameter becomes [pname => "FOOBAR"]. 468 NSString *breakpadParameterName = (NSString *)key; 469 NSString *urlParameter = [urlParameterNames 470 objectForKey:breakpadParameterName]; 471 if (urlParameter) { 472 [crashParameters setObject:[parameters_ objectForKey:key] 473 forKey:urlParameter]; 474 } 475 } 476 477 // Now, add the parameters that were added by the application. 478 enumerator = [extraServerVars_ keyEnumerator]; 479 480 while ((key = [enumerator nextObject])) { 481 NSString *urlParameterName = (NSString *)key; 482 NSString *urlParameterValue = 483 [extraServerVars_ objectForKey:urlParameterName]; 484 [crashParameters setObject:urlParameterValue 485 forKey:urlParameterName]; 486 } 487 return YES; 488} 489 490- (void)addServerParameter:(id)value forKey:(NSString *)key { 491 [extraServerVars_ setObject:value forKey:key]; 492} 493 494//============================================================================= 495- (void)handleNetworkResponse:(NSData *)data withError:(NSError *)error { 496 NSString *result = [[NSString alloc] initWithData:data 497 encoding:NSUTF8StringEncoding]; 498 const char *reportID = "ERR"; 499 if (error) { 500 fprintf(stderr, "Breakpad Uploader: Send Error: %s\n", 501 [[error description] UTF8String]); 502 } else { 503 NSCharacterSet *trimSet = 504 [NSCharacterSet whitespaceAndNewlineCharacterSet]; 505 reportID = [[result stringByTrimmingCharactersInSet:trimSet] UTF8String]; 506 [self logUploadWithID:reportID]; 507 } 508 509 // rename the minidump file according to the id returned from the server 510 NSString *minidumpDir = 511 [parameters_ objectForKey:@kReporterMinidumpDirectoryKey]; 512 NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey]; 513 514 NSString *srcString = [NSString stringWithFormat:@"%@/%@.dmp", 515 minidumpDir, minidumpID]; 516 NSString *destString = [NSString stringWithFormat:@"%@/%s.dmp", 517 minidumpDir, reportID]; 518 519 const char *src = [srcString fileSystemRepresentation]; 520 const char *dest = [destString fileSystemRepresentation]; 521 522 if (rename(src, dest) == 0) { 523 GTMLoggerInfo(@"Breakpad Uploader: Renamed %s to %s after successful " \ 524 "upload",src, dest); 525 } 526 else { 527 // can't rename - don't worry - it's not important for users 528 GTMLoggerDebug(@"Breakpad Uploader: successful upload report ID = %s\n", 529 reportID ); 530 } 531 [result release]; 532} 533 534//============================================================================= 535- (void)report { 536 NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@BREAKPAD_URL]]; 537 HTTPMultipartUpload *upload = [[HTTPMultipartUpload alloc] initWithURL:url]; 538 NSMutableDictionary *uploadParameters = [NSMutableDictionary dictionary]; 539 540 if (![self populateServerDictionary:uploadParameters]) { 541 [upload release]; 542 return; 543 } 544 545 [upload setParameters:uploadParameters]; 546 547 // Add minidump file 548 if (minidumpContents_) { 549 [upload addFileContents:minidumpContents_ name:@"upload_file_minidump"]; 550 551 // If there is a log file, upload it together with the minidump. 552 if (logFileData_) { 553 [upload addFileContents:logFileData_ name:@"log"]; 554 } 555 556 // Send it 557 NSError *error = nil; 558 NSData *data = [upload send:&error]; 559 560 if (![url isFileURL]) { 561 [self handleNetworkResponse:data withError:error]; 562 } else { 563 if (error) { 564 fprintf(stderr, "Breakpad Uploader: Error writing request file: %s\n", 565 [[error description] UTF8String]); 566 } 567 } 568 569 } else { 570 // Minidump is missing -- upload just the log file. 571 if (logFileData_) { 572 [self uploadData:logFileData_ name:@"log"]; 573 } 574 } 575 [upload release]; 576} 577 578- (void)uploadData:(NSData *)data name:(NSString *)name { 579 NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@BREAKPAD_URL]]; 580 NSMutableDictionary *uploadParameters = [NSMutableDictionary dictionary]; 581 582 if (![self populateServerDictionary:uploadParameters]) 583 return; 584 585 HTTPMultipartUpload *upload = 586 [[HTTPMultipartUpload alloc] initWithURL:url]; 587 588 [uploadParameters setObject:name forKey:@"type"]; 589 [upload setParameters:uploadParameters]; 590 [upload addFileContents:data name:name]; 591 592 [upload send:nil]; 593 [upload release]; 594} 595 596- (void)logUploadWithID:(const char *)uploadID { 597 NSString *minidumpDir = 598 [parameters_ objectForKey:@kReporterMinidumpDirectoryKey]; 599 NSString *logFilePath = [NSString stringWithFormat:@"%@/%s", 600 minidumpDir, kReporterLogFilename]; 601 NSString *logLine = [NSString stringWithFormat:@"%0.f,%s\n", 602 [[NSDate date] timeIntervalSince1970], uploadID]; 603 NSData *logData = [logLine dataUsingEncoding:NSUTF8StringEncoding]; 604 605 NSFileManager *fileManager = [NSFileManager defaultManager]; 606 if ([fileManager fileExistsAtPath:logFilePath]) { 607 NSFileHandle *logFileHandle = 608 [NSFileHandle fileHandleForWritingAtPath:logFilePath]; 609 [logFileHandle seekToEndOfFile]; 610 [logFileHandle writeData:logData]; 611 [logFileHandle closeFile]; 612 } else { 613 [fileManager createFileAtPath:logFilePath 614 contents:logData 615 attributes:nil]; 616 } 617} 618 619//============================================================================= 620- (NSMutableDictionary *)parameters { 621 return parameters_; 622} 623 624//============================================================================= 625- (void)dealloc { 626 [parameters_ release]; 627 [minidumpContents_ release]; 628 [logFileData_ release]; 629 [googleDictionary_ release]; 630 [socorroDictionary_ release]; 631 [serverDictionary_ release]; 632 [extraServerVars_ release]; 633 [super dealloc]; 634} 635 636@end 637