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