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