crash_report_sender.m revision ed1f6e754a876f435507f5b5267341051ed3adf6
1// Copyright (c) 2006, 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 <pwd.h> 31#import <sys/stat.h> 32#import <unistd.h> 33 34#import <Cocoa/Cocoa.h> 35#import <SystemConfiguration/SystemConfiguration.h> 36 37#import "common/mac/HTTPMultipartUpload.h" 38 39#import "crash_report_sender.h" 40#import "common/mac/GTMLogger.h" 41 42 43#define kLastSubmission @"LastSubmission" 44const int kMinidumpFileLengthLimit = 800000; 45 46#define kApplePrefsSyncExcludeAllKey @"com.apple.PreferenceSync.ExcludeAllSyncKeys" 47 48NSString *const kGoogleServerType = @"google"; 49NSString *const kSocorroServerType = @"socorro"; 50NSString *const kDefaultServerType = @"google"; 51 52@interface Reporter(PrivateMethods) 53+ (uid_t)consoleUID; 54 55- (id)initWithConfigurationFD:(int)fd; 56 57- (NSString *)readString; 58- (NSData *)readData:(ssize_t)length; 59 60- (BOOL)readConfigurationData; 61- (BOOL)readMinidumpData; 62- (BOOL)readLogFileData; 63 64- (BOOL)askUserPermissionToSend:(BOOL)shouldSubmitReport; 65- (BOOL)shouldSubmitReport; 66 67// Run an alert window with the given timeout. Returns 68// NSAlertButtonDefault if the timeout is exceeded. A timeout of 0 69// queues the message immediately in the modal run loop. 70- (int)runModalWindow:(NSWindow*)window withTimeout:(NSTimeInterval)timeout; 71 72// Returns a unique client id (user-specific), creating a persistent 73// one in the user defaults, if necessary. 74- (NSString*)clientID; 75 76// Returns a dictionary that can be used to map Breakpad parameter names to 77// URL parameter names. 78- (NSDictionary *)dictionaryForServerType:(NSString *)serverType; 79@end 80 81@implementation Reporter 82//============================================================================= 83+ (uid_t)consoleUID { 84 SCDynamicStoreRef store = 85 SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("Reporter"), NULL, NULL); 86 uid_t uid = -2; // Default to "nobody" 87 if (store) { 88 CFStringRef user = SCDynamicStoreCopyConsoleUser(store, &uid, NULL); 89 90 if (user) 91 CFRelease(user); 92 else 93 uid = -2; 94 95 CFRelease(store); 96 } 97 98 return uid; 99} 100 101//============================================================================= 102- (id)initWithConfigurationFD:(int)fd { 103 if ((self = [super init])) { 104 configFile_ = fd; 105 } 106 107 // Because the reporter is embedded in the framework (and many copies 108 // of the framework may exist) its not completely certain that the OS 109 // will obey the com.apple.PreferenceSync.ExcludeAllSyncKeys in our 110 // Info.plist. To make sure, also set the key directly if needed. 111 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; 112 if (![ud boolForKey:kApplePrefsSyncExcludeAllKey]) { 113 [ud setBool:YES forKey:kApplePrefsSyncExcludeAllKey]; 114 } 115 116 [self createServerParameterDictionaries]; 117 118 return self; 119} 120 121//============================================================================= 122- (NSString *)readString { 123 NSMutableString *str = [NSMutableString stringWithCapacity:32]; 124 char ch[2] = { 0 }; 125 126 while (read(configFile_, &ch[0], 1) == 1) { 127 if (ch[0] == '\n') { 128 // Break if this is the first newline after reading some other string 129 // data. 130 if ([str length]) 131 break; 132 } else { 133 [str appendString:[NSString stringWithUTF8String:ch]]; 134 } 135 } 136 137 return str; 138} 139 140//============================================================================= 141- (NSData *)readData:(ssize_t)length { 142 NSMutableData *data = [NSMutableData dataWithLength:length]; 143 char *bytes = (char *)[data bytes]; 144 145 if (read(configFile_, bytes, length) != length) 146 return nil; 147 148 return data; 149} 150 151//============================================================================= 152- (BOOL)readConfigurationData { 153 parameters_ = [[NSMutableDictionary alloc] init]; 154 155 while (1) { 156 NSString *key = [self readString]; 157 158 if (![key length]) 159 break; 160 161 // Read the data. Try to convert to a UTF-8 string, or just save 162 // the data 163 NSString *lenStr = [self readString]; 164 ssize_t len = [lenStr intValue]; 165 NSData *data = [self readData:len]; 166 id value = [[NSString alloc] initWithData:data 167 encoding:NSUTF8StringEncoding]; 168 169 [parameters_ setObject:value ? value : data forKey:key]; 170 [value release]; 171 } 172 173 // generate a unique client ID based on this host's MAC address 174 // then add a key/value pair for it 175 NSString *clientID = [self clientID]; 176 [parameters_ setObject:clientID forKey:@"guid"]; 177 178 close(configFile_); 179 configFile_ = -1; 180 181 return YES; 182} 183 184// Per user per machine 185- (NSString *)clientID { 186 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; 187 NSString *crashClientID = [ud stringForKey:kClientIdPreferenceKey]; 188 if (crashClientID) { 189 return crashClientID; 190 } 191 192 // Otherwise, if we have no client id, generate one! 193 srandom([[NSDate date] timeIntervalSince1970]); 194 long clientId1 = random(); 195 long clientId2 = random(); 196 long clientId3 = random(); 197 crashClientID = [NSString stringWithFormat:@"%x%x%x", 198 clientId1, clientId2, clientId3]; 199 200 [ud setObject:crashClientID forKey:kClientIdPreferenceKey]; 201 [ud synchronize]; 202 return crashClientID; 203} 204 205//============================================================================= 206- (BOOL)readLogFileData { 207 unsigned int logFileCounter = 0; 208 209 NSString *logPath; 210 int logFileTailSize = [[parameters_ objectForKey:@BREAKPAD_LOGFILE_UPLOAD_SIZE] 211 intValue]; 212 213 NSMutableArray *logFilenames; // An array of NSString, one per log file 214 logFilenames = [[NSMutableArray alloc] init]; 215 216 char tmpDirTemplate[80] = "/tmp/CrashUpload-XXXXX"; 217 char *tmpDir = mkdtemp(tmpDirTemplate); 218 219 // Construct key names for the keys we expect to contain log file paths 220 for(logFileCounter = 0;; logFileCounter++) { 221 NSString *logFileKey = [NSString stringWithFormat:@"%@%d", 222 @BREAKPAD_LOGFILE_KEY_PREFIX, 223 logFileCounter]; 224 225 logPath = [parameters_ objectForKey:logFileKey]; 226 227 // They should all be consecutive, so if we don't find one, assume 228 // we're done 229 230 if (!logPath) { 231 break; 232 } 233 234 NSData *entireLogFile = [[NSData alloc] initWithContentsOfFile:logPath]; 235 236 if (entireLogFile == nil) { 237 continue; 238 } 239 240 NSRange fileRange; 241 242 // Truncate the log file, only if necessary 243 244 if ([entireLogFile length] <= logFileTailSize) { 245 fileRange = NSMakeRange(0, [entireLogFile length]); 246 } else { 247 fileRange = NSMakeRange([entireLogFile length] - logFileTailSize, 248 logFileTailSize); 249 } 250 251 char tmpFilenameTemplate[100]; 252 253 // Generate a template based on the log filename 254 sprintf(tmpFilenameTemplate,"%s/%s-XXXX", tmpDir, 255 [[logPath lastPathComponent] fileSystemRepresentation]); 256 257 char *tmpFile = mktemp(tmpFilenameTemplate); 258 259 NSData *logSubdata = [entireLogFile subdataWithRange:fileRange]; 260 NSString *tmpFileString = [NSString stringWithUTF8String:tmpFile]; 261 [logSubdata writeToFile:tmpFileString atomically:NO]; 262 263 [logFilenames addObject:[tmpFileString lastPathComponent]]; 264 [entireLogFile release]; 265 } 266 267 if ([logFilenames count] == 0) { 268 [logFilenames release]; 269 logFileData_ = nil; 270 return NO; 271 } 272 273 // now, bzip all files into one 274 NSTask *tarTask = [[NSTask alloc] init]; 275 276 [tarTask setCurrentDirectoryPath:[NSString stringWithUTF8String:tmpDir]]; 277 [tarTask setLaunchPath:@"/usr/bin/tar"]; 278 279 NSMutableArray *bzipArgs = [NSMutableArray arrayWithObjects:@"-cjvf", 280 @"log.tar.bz2",nil]; 281 [bzipArgs addObjectsFromArray:logFilenames]; 282 283 [logFilenames release]; 284 285 [tarTask setArguments:bzipArgs]; 286 [tarTask launch]; 287 [tarTask waitUntilExit]; 288 [tarTask release]; 289 290 NSString *logTarFile = [NSString stringWithFormat:@"%s/log.tar.bz2",tmpDir]; 291 logFileData_ = [[NSData alloc] initWithContentsOfFile:logTarFile]; 292 if (logFileData_ == nil) { 293 GTMLoggerDebug(@"Cannot find temp tar log file: %@", logTarFile); 294 return NO; 295 } 296 return YES; 297 298} 299 300//============================================================================= 301- (BOOL)readMinidumpData { 302 NSString *minidumpDir = [parameters_ objectForKey:@kReporterMinidumpDirectoryKey]; 303 NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey]; 304 305 if (![minidumpID length]) 306 return NO; 307 308 NSString *path = [minidumpDir stringByAppendingPathComponent:minidumpID]; 309 path = [path stringByAppendingPathExtension:@"dmp"]; 310 311 // check the size of the minidump and limit it to a reasonable size 312 // before attempting to load into memory and upload 313 const char *fileName = [path fileSystemRepresentation]; 314 struct stat fileStatus; 315 316 BOOL success = YES; 317 318 if (!stat(fileName, &fileStatus)) { 319 if (fileStatus.st_size > kMinidumpFileLengthLimit) { 320 fprintf(stderr, "Breakpad Reporter: minidump file too large " \ 321 "to upload : %d\n", (int)fileStatus.st_size); 322 success = NO; 323 } 324 } else { 325 fprintf(stderr, "Breakpad Reporter: unable to determine minidump " \ 326 "file length\n"); 327 success = NO; 328 } 329 330 if (success) { 331 minidumpContents_ = [[NSData alloc] initWithContentsOfFile:path]; 332 success = ([minidumpContents_ length] ? YES : NO); 333 } 334 335 if (!success) { 336 // something wrong with the minidump file -- delete it 337 unlink(fileName); 338 } 339 340 return success; 341} 342 343//============================================================================= 344- (BOOL)askUserPermissionToSend:(BOOL)shouldSubmitReport { 345 // Send without confirmation 346 if ([[parameters_ objectForKey:@BREAKPAD_SKIP_CONFIRM] isEqualToString:@"YES"]) { 347 GTMLoggerDebug(@"Skipping confirmation and sending report"); 348 return YES; 349 } 350 351 // Determine if we should create a text box for user feedback 352 BOOL shouldRequestComments = 353 [[parameters_ objectForKey:@BREAKPAD_REQUEST_COMMENTS] 354 isEqual:@"YES"]; 355 356 NSString *display = [parameters_ objectForKey:@BREAKPAD_PRODUCT_DISPLAY]; 357 358 if (![display length]) 359 display = [parameters_ objectForKey:@BREAKPAD_PRODUCT]; 360 361 NSString *vendor = [parameters_ objectForKey:@BREAKPAD_VENDOR]; 362 363 if (![vendor length]) 364 vendor = @"Vendor not specified"; 365 366 NSBundle *bundle = [NSBundle mainBundle]; 367 [self setHeaderMessage:[NSString stringWithFormat: 368 NSLocalizedStringFromTableInBundle(@"headerFmt", nil, 369 bundle, 370 @""), display]]; 371 NSString *defaultButtonTitle = nil; 372 NSString *otherButtonTitle = nil; 373 374 // Get the localized alert strings 375 // If we're going to submit a report, prompt the user if this is okay. 376 // Otherwise, just let them know that the app crashed. 377 378 if (shouldSubmitReport) { 379 NSString *msgFormat = NSLocalizedStringFromTableInBundle(@"msg", 380 nil, 381 bundle, @""); 382 383 [self setReportMessage:[NSString stringWithFormat:msgFormat, vendor]]; 384 385 defaultButtonTitle = NSLocalizedStringFromTableInBundle(@"sendReportButton", 386 nil, bundle, @""); 387 otherButtonTitle = NSLocalizedStringFromTableInBundle(@"cancelButton", nil, 388 bundle, @""); 389 } else { 390 [self setReportMessage:NSLocalizedStringFromTableInBundle(@"noSendMsg", nil, 391 bundle, @"")]; 392 defaultButtonTitle = NSLocalizedStringFromTableInBundle(@"noSendButton", 393 nil, bundle, @""); 394 } 395 396 // Get the timeout value for the notification. 397 NSTimeInterval timeout = [[parameters_ objectForKey:@BREAKPAD_CONFIRM_TIMEOUT] 398 floatValue]; 399 // Show the notification for at least one minute (but allow 0, since it means 400 // no timeout). 401 if (timeout > 0.001 && timeout < 60.0) { 402 timeout = 60.0; 403 } 404 405 // Initialize Cocoa, needed to display the alert 406 NSApplicationLoad(); 407 408 int buttonPressed = NSAlertAlternateReturn; 409 410 if (shouldRequestComments) { 411 BOOL didLoadNib = [NSBundle loadNibNamed:@"Breakpad" owner:self]; 412 if (didLoadNib) { 413 // Append the request for comments to the |reportMessage| string. 414 NSString *commentsMessage = 415 NSLocalizedStringFromTableInBundle(@"commentsMsg", nil, bundle, @""); 416 [self setReportMessage:[NSString stringWithFormat:@"%@\n\n%@", 417 [self reportMessage], 418 commentsMessage]]; 419 420 // Add the request for email address. 421 [self setEmailMessage: 422 NSLocalizedStringFromTableInBundle(@"emailMsg", nil, bundle, @"")]; 423 424 // Run the alert 425 buttonPressed = [self runModalWindow:alertWindow withTimeout:timeout]; 426 427 // Extract info from the user into the parameters_ dictionary 428 if ([self commentsValue]) { 429 [parameters_ setObject:[self commentsValue] 430 forKey:@BREAKPAD_COMMENTS]; 431 } 432 if ([self emailValue]) { 433 [parameters_ setObject:[self emailValue] 434 forKey:@BREAKPAD_EMAIL]; 435 } 436 } 437 } else { 438 // Create an alert panel to tell the user something happened 439 NSPanel* alert = NSGetAlertPanel([self headerMessage], 440 [self reportMessage], 441 defaultButtonTitle, 442 otherButtonTitle, nil); 443 444 // Pop the alert with an automatic timeout, and wait for the response 445 buttonPressed = [self runModalWindow:alert withTimeout:timeout]; 446 447 // Release the panel memory 448 NSReleaseAlertPanel(alert); 449 } 450 return buttonPressed == NSAlertDefaultReturn; 451} 452 453- (int)runModalWindow:(NSWindow*)window withTimeout:(NSTimeInterval)timeout { 454 // Queue a |stopModal| message to be performed in |timeout| seconds. 455 if (timeout > 0.001) { 456 [NSApp performSelector:@selector(stopModal) 457 withObject:nil 458 afterDelay:timeout]; 459 } 460 461 // Run the window modally and wait for either a |stopModal| message or a 462 // button click. 463 [NSApp activateIgnoringOtherApps:YES]; 464 int returnMethod = [NSApp runModalForWindow:window]; 465 466 // Cancel the pending |stopModal| message. 467 if (returnMethod != NSRunStoppedResponse) { 468 [NSObject cancelPreviousPerformRequestsWithTarget:NSApp 469 selector:@selector(stopModal) 470 object:nil]; 471 } 472 return returnMethod; 473} 474 475- (IBAction)sendReport:(id)sender { 476 [alertWindow orderOut:self]; 477 // Use NSAlertDefaultReturn so that the return value of |runModalWithWindow| 478 // matches the AppKit function NSRunAlertPanel() 479 [NSApp stopModalWithCode:NSAlertDefaultReturn]; 480} 481 482// UI Button Actions 483//============================================================================= 484- (IBAction)cancel:(id)sender { 485 [alertWindow orderOut:self]; 486 // Use NSAlertDefaultReturn so that the return value of |runModalWithWindow| 487 // matches the AppKit function NSRunAlertPanel() 488 [NSApp stopModalWithCode:NSAlertAlternateReturn]; 489} 490 491- (IBAction)showPrivacyPolicy:(id)sender { 492 // Get the localized privacy policy URL and open it in the default browser. 493 NSURL* privacyPolicyURL = 494 [NSURL URLWithString:NSLocalizedStringFromTableInBundle( 495 @"privacyPolicyURL", nil, [NSBundle mainBundle], @"")]; 496 [[NSWorkspace sharedWorkspace] openURL:privacyPolicyURL]; 497} 498 499// Text Field Delegate Methods 500//============================================================================= 501- (BOOL) control:(NSControl*)control 502 textView:(NSTextView*)textView 503doCommandBySelector:(SEL)commandSelector { 504 BOOL result = NO; 505 // If the user has entered text, don't end editing on "return" 506 if (commandSelector == @selector(insertNewline:) 507 && [[textView string] length] > 0) { 508 [textView insertNewlineIgnoringFieldEditor:self]; 509 result = YES; 510 } 511 return result; 512} 513 514// Accessors 515//============================================================================= 516- (NSString *)headerMessage { 517 return [[headerMessage_ retain] autorelease]; 518} 519 520- (void)setHeaderMessage:(NSString *)value { 521 if (headerMessage_ != value) { 522 [headerMessage_ autorelease]; 523 headerMessage_ = [value copy]; 524 } 525} 526 527- (NSString *)reportMessage { 528 return [[reportMessage_ retain] autorelease]; 529} 530 531- (void)setReportMessage:(NSString *)value { 532 if (reportMessage_ != value) { 533 [reportMessage_ autorelease]; 534 reportMessage_ = [value copy]; 535 } 536} 537 538- (NSString *)commentsValue { 539 return [[commentsValue_ retain] autorelease]; 540} 541 542- (void)setCommentsValue:(NSString *)value { 543 if (commentsValue_ != value) { 544 [commentsValue_ autorelease]; 545 commentsValue_ = [value copy]; 546 } 547} 548 549- (NSString *)emailMessage { 550 return [[emailMessage_ retain] autorelease]; 551} 552 553- (void)setEmailMessage:(NSString *)value { 554 if (emailMessage_ != value) { 555 [emailMessage_ autorelease]; 556 emailMessage_ = [value copy]; 557 } 558} 559 560- (NSString *)emailValue { 561 return [[emailValue_ retain] autorelease]; 562} 563 564- (void)setEmailValue:(NSString *)value { 565 if (emailValue_ != value) { 566 [emailValue_ autorelease]; 567 emailValue_ = [value copy]; 568 } 569} 570 571//============================================================================= 572- (BOOL)shouldSubmitReport { 573 float interval = [[parameters_ objectForKey:@BREAKPAD_REPORT_INTERVAL] 574 floatValue]; 575 NSString *program = [parameters_ objectForKey:@BREAKPAD_PRODUCT]; 576 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; 577 NSMutableDictionary *programDict = 578 [NSMutableDictionary dictionaryWithDictionary:[ud dictionaryForKey:program]]; 579 NSNumber *lastTimeNum = [programDict objectForKey:kLastSubmission]; 580 NSTimeInterval lastTime = lastTimeNum ? [lastTimeNum floatValue] : 0; 581 NSTimeInterval now = CFAbsoluteTimeGetCurrent(); 582 NSTimeInterval spanSeconds = (now - lastTime); 583 584 [programDict setObject:[NSNumber numberWithFloat:now] forKey:kLastSubmission]; 585 [ud setObject:programDict forKey:program]; 586 [ud synchronize]; 587 588 // If we've specified an interval and we're within that time, don't ask the 589 // user if we should report 590 GTMLoggerDebug(@"Reporter Interval: %f", interval); 591 if (interval > spanSeconds) { 592 GTMLoggerDebug(@"Within throttling interval, not sending report"); 593 return NO; 594 } 595 return YES; 596} 597 598- (void)createServerParameterDictionaries { 599 serverDictionary_ = [[NSMutableDictionary alloc] init]; 600 socorroDictionary_ = [[NSMutableDictionary alloc] init]; 601 googleDictionary_ = [[NSMutableDictionary alloc] init]; 602 603 [serverDictionary_ setObject:socorroDictionary_ forKey:kSocorroServerType]; 604 [serverDictionary_ setObject:googleDictionary_ forKey:kGoogleServerType]; 605 606 [googleDictionary_ setObject:@"ptime" forKey:@BREAKPAD_PROCESS_UP_TIME]; 607 [googleDictionary_ setObject:@"email" forKey:@BREAKPAD_EMAIL]; 608 [googleDictionary_ setObject:@"comments" forKey:@BREAKPAD_COMMENTS]; 609 [googleDictionary_ setObject:@"prod" forKey:@BREAKPAD_PRODUCT]; 610 [googleDictionary_ setObject:@"ver" forKey:@BREAKPAD_VERSION]; 611 // TODO: just for testing, google's server doesn't support it 612 [googleDictionary_ setObject:@"buildid" forKey:@BREAKPAD_BUILD_ID]; 613 614 [socorroDictionary_ setObject:@"Comments" forKey:@BREAKPAD_COMMENTS]; 615 [socorroDictionary_ setObject:@"CrashTime" 616 forKey:@BREAKPAD_PROCESS_CRASH_TIME]; 617 [socorroDictionary_ setObject:@"StartupTime" 618 forKey:@BREAKPAD_PROCESS_START_TIME]; 619 [socorroDictionary_ setObject:@"Version" 620 forKey:@BREAKPAD_VERSION]; 621 [socorroDictionary_ setObject:@"ProductName" 622 forKey:@BREAKPAD_PRODUCT]; 623 [socorroDictionary_ setObject:@"ProductName" 624 forKey:@BREAKPAD_PRODUCT]; 625 [socorroDictionary_ setObject:@"BuildID" 626 forKey:@BREAKPAD_BUILD_ID]; 627} 628 629- (NSDictionary *)dictionaryForServerType:(NSString *)serverType { 630 if (serverType == nil) { 631 return [serverDictionary_ objectForKey:kDefaultServerType]; 632 } 633 return [serverDictionary_ objectForKey:serverType]; 634} 635 636// Helper method to set HTTP parameters based on server type 637- (BOOL)setPostParametersFromDictionary:(NSMutableDictionary *)crashParameters { 638 NSString *serverType = [parameters_ objectForKey:@BREAKPAD_SERVER_TYPE]; 639 NSDictionary *urlParameterNames = [self dictionaryForServerType:serverType]; 640 641 id key; 642 NSEnumerator *enumerator = [parameters_ keyEnumerator]; 643 644 while ((key = [enumerator nextObject])) { 645 // The key from parameters_ corresponds to a key in 646 // urlParameterNames. The value in parameters_ gets stored in 647 // crashParameters with a key that is the value in 648 // urlParameterNames. 649 650 // For instance, if parameters_ has [PRODUCT_NAME => "FOOBAR"] and 651 // urlParameterNames has [PRODUCT_NAME => "pname"] the final HTTP 652 // URL parameter becomes [pname => "FOOBAR"]. 653 NSString *breakpadParameterName = (NSString *)key; 654 NSString *urlParameter = [urlParameterNames 655 objectForKey:breakpadParameterName]; 656 if (urlParameter) { 657 [crashParameters setObject:[parameters_ objectForKey:key] 658 forKey:urlParameter]; 659 } 660 } 661 return YES; 662} 663 664//============================================================================= 665- (void)report { 666 NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@BREAKPAD_URL]]; 667 HTTPMultipartUpload *upload = [[HTTPMultipartUpload alloc] initWithURL:url]; 668 NSMutableDictionary *uploadParameters = [NSMutableDictionary dictionary]; 669 670 if (![self setPostParametersFromDictionary:uploadParameters]) { 671 return; 672 } 673 674 [upload setParameters:uploadParameters]; 675 676 // Add minidump file 677 if (minidumpContents_) { 678 [upload addFileContents:minidumpContents_ name:@"upload_file_minidump"]; 679 680 // Send it 681 NSError *error = nil; 682 NSData *data = [upload send:&error]; 683 NSString *result = [[NSString alloc] initWithData:data 684 encoding:NSUTF8StringEncoding]; 685 const char *reportID = "ERR"; 686 687 if (error) { 688 fprintf(stderr, "Breakpad Reporter: Send Error: %s\n", 689 [[error description] UTF8String]); 690 } else { 691 NSCharacterSet *trimSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; 692 reportID = [[result stringByTrimmingCharactersInSet:trimSet] UTF8String]; 693 } 694 695 // rename the minidump file according to the id returned from the server 696 NSString *minidumpDir = [parameters_ objectForKey:@kReporterMinidumpDirectoryKey]; 697 NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey]; 698 699 NSString *srcString = [NSString stringWithFormat:@"%@/%@.dmp", 700 minidumpDir, minidumpID]; 701 NSString *destString = [NSString stringWithFormat:@"%@/%s.dmp", 702 minidumpDir, reportID]; 703 704 const char *src = [srcString fileSystemRepresentation]; 705 const char *dest = [destString fileSystemRepresentation]; 706 707 if (rename(src, dest) == 0) { 708 GTMLoggerInfo(@"Breakpad Reporter: Renamed %s to %s after successful " \ 709 "upload",src, dest); 710 } 711 else { 712 // can't rename - don't worry - it's not important for users 713 GTMLoggerDebug(@"Breakpad Reporter: successful upload report ID = %s\n", 714 reportID ); 715 } 716 [result release]; 717 } 718 719 if (logFileData_) { 720 HTTPMultipartUpload *logUpload = [[HTTPMultipartUpload alloc] initWithURL:url]; 721 722 [uploadParameters setObject:@"log" forKey:@"type"]; 723 [logUpload setParameters:uploadParameters]; 724 [logUpload addFileContents:logFileData_ name:@"log"]; 725 726 NSError *error = nil; 727 NSData *data = [logUpload send:&error]; 728 NSString *result = [[NSString alloc] initWithData:data 729 encoding:NSUTF8StringEncoding]; 730 [result release]; 731 [logUpload release]; 732 } 733 734 [upload release]; 735} 736 737//============================================================================= 738- (void)dealloc { 739 [parameters_ release]; 740 [minidumpContents_ release]; 741 [logFileData_ release]; 742 [googleDictionary_ release]; 743 [socorroDictionary_ release]; 744 [serverDictionary_ release]; 745 [super dealloc]; 746} 747 748@end 749 750//============================================================================= 751int main(int argc, const char *argv[]) { 752 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 753#if DEBUG 754 // Log to stderr in debug builds. 755 [GTMLogger setSharedLogger:[GTMLogger standardLoggerWithStderr]]; 756#endif 757 GTMLoggerDebug(@"Reporter Launched, argc=%d", argc); 758 // The expectation is that there will be one argument which is the path 759 // to the configuration file 760 if (argc != 2) { 761 exit(1); 762 } 763 764 // Open the file before (potentially) switching to console user 765 int configFile = open(argv[1], O_RDONLY, 0600); 766 767 if (configFile == -1) { 768 GTMLoggerDebug(@"Couldn't open config file %s - %s", 769 argv[1], 770 strerror(errno)); 771 } 772 773 // we want to avoid a build-up of old config files even if they 774 // have been incorrectly written by the framework 775 unlink(argv[1]); 776 777 if (configFile == -1) { 778 GTMLoggerDebug(@"Couldn't unlink config file %s - %s", 779 argv[1], 780 strerror(errno)); 781 exit(1); 782 } 783 784 Reporter *reporter = [[Reporter alloc] initWithConfigurationFD:configFile]; 785 786 // Gather the configuration data 787 if (![reporter readConfigurationData]) { 788 GTMLoggerDebug(@"reporter readConfigurationData failed"); 789 exit(1); 790 } 791 792 // Read the minidump into memory before we (potentially) switch from the 793 // root user 794 [reporter readMinidumpData]; 795 796 [reporter readLogFileData]; 797 798 // only submit a report if we have not recently crashed in the past 799 BOOL shouldSubmitReport = [reporter shouldSubmitReport]; 800 BOOL okayToSend = NO; 801 802 // ask user if we should send 803 if (shouldSubmitReport) { 804 okayToSend = [reporter askUserPermissionToSend:shouldSubmitReport]; 805 } 806 807 // If we're running as root, switch over to nobody 808 if (getuid() == 0 || geteuid() == 0) { 809 struct passwd *pw = getpwnam("nobody"); 810 811 // If we can't get a non-root uid, don't send the report 812 if (!pw) { 813 GTMLoggerDebug(@"!pw - %s", strerror(errno)); 814 exit(0); 815 } 816 817 if (setgid(pw->pw_gid) == -1) { 818 GTMLoggerDebug(@"setgid(pw->pw_gid) == -1 - %s", strerror(errno)); 819 exit(0); 820 } 821 822 if (setuid(pw->pw_uid) == -1) { 823 GTMLoggerDebug(@"setuid(pw->pw_uid) == -1 - %s", strerror(errno)); 824 exit(0); 825 } 826 } 827 else { 828 GTMLoggerDebug(@"getuid() !=0 || geteuid() != 0"); 829 } 830 831 if (okayToSend && shouldSubmitReport) { 832 GTMLoggerDebug(@"Sending Report"); 833 [reporter report]; 834 GTMLoggerDebug(@"Report Sent!"); 835 } else { 836 GTMLoggerDebug(@"Not sending crash report okayToSend=%d, "\ 837 "shouldSubmitReport=%d", okayToSend, shouldSubmitReport); 838 } 839 840 GTMLoggerDebug(@"Exiting with no errors"); 841 // Cleanup 842 [reporter release]; 843 [pool release]; 844 return 0; 845} 846