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 43 const int kMinidumpFileLengthLimit = 2 * 1024 * 1024; // 2MB 44 45 #define kApplePrefsSyncExcludeAllKey \ 46 @"com.apple.PreferenceSync.ExcludeAllSyncKeys" 47 48 NSString *const kGoogleServerType = @"google"; 49 NSString *const kSocorroServerType = @"socorro"; 50 NSString *const kDefaultServerType = @"google"; 51 52 #pragma mark - 53 54 namespace { 55 // Read one line from the configuration file. 56 NSString *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. 77 NSData *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. 89 NSDictionary *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