- add third_party src.
[platform/framework/web/crosswalk.git] / src / breakpad / src / client / mac / sender / uploader.mm
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 - (void)translateConfigurationData:(NSDictionary *)config {
209   parameters_ = [[NSMutableDictionary alloc] init];
210
211   NSEnumerator *it = [config keyEnumerator];
212   while (NSString *key = [it nextObject]) {
213     // If the keyname is prefixed by BREAKPAD_SERVER_PARAMETER_PREFIX
214     // that indicates that it should be uploaded to the server along
215     // with the minidump, so we treat it specially.
216     if ([key hasPrefix:@BREAKPAD_SERVER_PARAMETER_PREFIX]) {
217       NSString *urlParameterKey =
218         [key substringFromIndex:[@BREAKPAD_SERVER_PARAMETER_PREFIX length]];
219       if ([urlParameterKey length]) {
220         id value = [config objectForKey:key];
221         if ([value isKindOfClass:[NSString class]]) {
222           [self addServerParameter:(NSString *)value
223                             forKey:urlParameterKey];
224         } else {
225           [self addServerParameter:(NSData *)value
226                             forKey:urlParameterKey];
227         }
228       }
229     } else {
230       [parameters_ setObject:[config objectForKey:key] forKey:key];
231     }
232   }
233
234   // generate a unique client ID based on this host's MAC address
235   // then add a key/value pair for it
236   NSString *clientID = [self clientID];
237   [parameters_ setObject:clientID forKey:@"guid"];
238 }
239
240 // Per user per machine
241 - (NSString *)clientID {
242   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
243   NSString *crashClientID = [ud stringForKey:kClientIdPreferenceKey];
244   if (crashClientID) {
245     return crashClientID;
246   }
247
248   // Otherwise, if we have no client id, generate one!
249   srandom((int)[[NSDate date] timeIntervalSince1970]);
250   long clientId1 = random();
251   long clientId2 = random();
252   long clientId3 = random();
253   crashClientID = [NSString stringWithFormat:@"%lx%lx%lx",
254                             clientId1, clientId2, clientId3];
255
256   [ud setObject:crashClientID forKey:kClientIdPreferenceKey];
257   [ud synchronize];
258   return crashClientID;
259 }
260
261 //=============================================================================
262 - (BOOL)readLogFileData {
263 #if TARGET_OS_IPHONE
264   return NO;
265 #else
266   unsigned int logFileCounter = 0;
267
268   NSString *logPath;
269   size_t logFileTailSize =
270       [[parameters_ objectForKey:@BREAKPAD_LOGFILE_UPLOAD_SIZE] intValue];
271
272   NSMutableArray *logFilenames; // An array of NSString, one per log file
273   logFilenames = [[NSMutableArray alloc] init];
274
275   char tmpDirTemplate[80] = "/tmp/CrashUpload-XXXXX";
276   char *tmpDir = mkdtemp(tmpDirTemplate);
277
278   // Construct key names for the keys we expect to contain log file paths
279   for(logFileCounter = 0;; logFileCounter++) {
280     NSString *logFileKey = [NSString stringWithFormat:@"%@%d",
281                                      @BREAKPAD_LOGFILE_KEY_PREFIX,
282                                      logFileCounter];
283
284     logPath = [parameters_ objectForKey:logFileKey];
285
286     // They should all be consecutive, so if we don't find one, assume
287     // we're done
288
289     if (!logPath) {
290       break;
291     }
292
293     NSData *entireLogFile = [[NSData alloc] initWithContentsOfFile:logPath];
294
295     if (entireLogFile == nil) {
296       continue;
297     }
298
299     NSRange fileRange;
300
301     // Truncate the log file, only if necessary
302
303     if ([entireLogFile length] <= logFileTailSize) {
304       fileRange = NSMakeRange(0, [entireLogFile length]);
305     } else {
306       fileRange = NSMakeRange([entireLogFile length] - logFileTailSize,
307                               logFileTailSize);
308     }
309
310     char tmpFilenameTemplate[100];
311
312     // Generate a template based on the log filename
313     sprintf(tmpFilenameTemplate,"%s/%s-XXXX", tmpDir,
314             [[logPath lastPathComponent] fileSystemRepresentation]);
315
316     char *tmpFile = mktemp(tmpFilenameTemplate);
317
318     NSData *logSubdata = [entireLogFile subdataWithRange:fileRange];
319     NSString *tmpFileString = [NSString stringWithUTF8String:tmpFile];
320     [logSubdata writeToFile:tmpFileString atomically:NO];
321
322     [logFilenames addObject:[tmpFileString lastPathComponent]];
323     [entireLogFile release];
324   }
325
326   if ([logFilenames count] == 0) {
327     [logFilenames release];
328     logFileData_ =  nil;
329     return NO;
330   }
331
332   // now, bzip all files into one
333   NSTask *tarTask = [[NSTask alloc] init];
334
335   [tarTask setCurrentDirectoryPath:[NSString stringWithUTF8String:tmpDir]];
336   [tarTask setLaunchPath:@"/usr/bin/tar"];
337
338   NSMutableArray *bzipArgs = [NSMutableArray arrayWithObjects:@"-cjvf",
339                                              @"log.tar.bz2",nil];
340   [bzipArgs addObjectsFromArray:logFilenames];
341
342   [logFilenames release];
343
344   [tarTask setArguments:bzipArgs];
345   [tarTask launch];
346   [tarTask waitUntilExit];
347   [tarTask release];
348
349   NSString *logTarFile = [NSString stringWithFormat:@"%s/log.tar.bz2",tmpDir];
350   logFileData_ = [[NSData alloc] initWithContentsOfFile:logTarFile];
351   if (logFileData_ == nil) {
352     GTMLoggerDebug(@"Cannot find temp tar log file: %@", logTarFile);
353     return NO;
354   }
355   return YES;
356 #endif  // TARGET_OS_IPHONE
357 }
358
359 //=============================================================================
360 - (BOOL)readMinidumpData {
361   NSString *minidumpDir =
362       [parameters_ objectForKey:@kReporterMinidumpDirectoryKey];
363   NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey];
364
365   if (![minidumpID length])
366     return NO;
367
368   NSString *path = [minidumpDir stringByAppendingPathComponent:minidumpID];
369   path = [path stringByAppendingPathExtension:@"dmp"];
370
371   // check the size of the minidump and limit it to a reasonable size
372   // before attempting to load into memory and upload
373   const char *fileName = [path fileSystemRepresentation];
374   struct stat fileStatus;
375
376   BOOL success = YES;
377
378   if (!stat(fileName, &fileStatus)) {
379     if (fileStatus.st_size > kMinidumpFileLengthLimit) {
380       fprintf(stderr, "Breakpad Uploader: minidump file too large " \
381               "to upload : %d\n", (int)fileStatus.st_size);
382       success = NO;
383     }
384   } else {
385       fprintf(stderr, "Breakpad Uploader: unable to determine minidump " \
386               "file length\n");
387       success = NO;
388   }
389
390   if (success) {
391     minidumpContents_ = [[NSData alloc] initWithContentsOfFile:path];
392     success = ([minidumpContents_ length] ? YES : NO);
393   }
394
395   if (!success) {
396     // something wrong with the minidump file -- delete it
397     unlink(fileName);
398   }
399
400   return success;
401 }
402
403 #pragma mark -
404 //=============================================================================
405
406 - (void)createServerParameterDictionaries {
407   serverDictionary_ = [[NSMutableDictionary alloc] init];
408   socorroDictionary_ = [[NSMutableDictionary alloc] init];
409   googleDictionary_ = [[NSMutableDictionary alloc] init];
410   extraServerVars_ = [[NSMutableDictionary alloc] init];
411
412   [serverDictionary_ setObject:socorroDictionary_ forKey:kSocorroServerType];
413   [serverDictionary_ setObject:googleDictionary_ forKey:kGoogleServerType];
414
415   [googleDictionary_ setObject:@"ptime" forKey:@BREAKPAD_PROCESS_UP_TIME];
416   [googleDictionary_ setObject:@"email" forKey:@BREAKPAD_EMAIL];
417   [googleDictionary_ setObject:@"comments" forKey:@BREAKPAD_COMMENTS];
418   [googleDictionary_ setObject:@"prod" forKey:@BREAKPAD_PRODUCT];
419   [googleDictionary_ setObject:@"ver" forKey:@BREAKPAD_VERSION];
420   [googleDictionary_ setObject:@"guid" forKey:@"guid"];
421
422   [socorroDictionary_ setObject:@"Comments" forKey:@BREAKPAD_COMMENTS];
423   [socorroDictionary_ setObject:@"CrashTime"
424                          forKey:@BREAKPAD_PROCESS_CRASH_TIME];
425   [socorroDictionary_ setObject:@"StartupTime"
426                          forKey:@BREAKPAD_PROCESS_START_TIME];
427   [socorroDictionary_ setObject:@"Version"
428                          forKey:@BREAKPAD_VERSION];
429   [socorroDictionary_ setObject:@"ProductName"
430                          forKey:@BREAKPAD_PRODUCT];
431   [socorroDictionary_ setObject:@"Email"
432                          forKey:@BREAKPAD_EMAIL];
433 }
434
435 - (NSMutableDictionary *)dictionaryForServerType:(NSString *)serverType {
436   if (serverType == nil || [serverType length] == 0) {
437     return [serverDictionary_ objectForKey:kDefaultServerType];
438   }
439   return [serverDictionary_ objectForKey:serverType];
440 }
441
442 - (NSMutableDictionary *)urlParameterDictionary {
443   NSString *serverType = [parameters_ objectForKey:@BREAKPAD_SERVER_TYPE];
444   return [self dictionaryForServerType:serverType];
445
446 }
447
448 - (BOOL)populateServerDictionary:(NSMutableDictionary *)crashParameters {
449   NSDictionary *urlParameterNames = [self urlParameterDictionary];
450
451   id key;
452   NSEnumerator *enumerator = [parameters_ keyEnumerator];
453
454   while ((key = [enumerator nextObject])) {
455     // The key from parameters_ corresponds to a key in
456     // urlParameterNames.  The value in parameters_ gets stored in
457     // crashParameters with a key that is the value in
458     // urlParameterNames.
459
460     // For instance, if parameters_ has [PRODUCT_NAME => "FOOBAR"] and
461     // urlParameterNames has [PRODUCT_NAME => "pname"] the final HTTP
462     // URL parameter becomes [pname => "FOOBAR"].
463     NSString *breakpadParameterName = (NSString *)key;
464     NSString *urlParameter = [urlParameterNames
465                                    objectForKey:breakpadParameterName];
466     if (urlParameter) {
467       [crashParameters setObject:[parameters_ objectForKey:key]
468                           forKey:urlParameter];
469     }
470   }
471
472   // Now, add the parameters that were added by the application.
473   enumerator = [extraServerVars_ keyEnumerator];
474
475   while ((key = [enumerator nextObject])) {
476     NSString *urlParameterName = (NSString *)key;
477     NSString *urlParameterValue =
478       [extraServerVars_ objectForKey:urlParameterName];
479     [crashParameters setObject:urlParameterValue
480                         forKey:urlParameterName];
481   }
482   return YES;
483 }
484
485 - (void)addServerParameter:(id)value forKey:(NSString *)key {
486   [extraServerVars_ setObject:value forKey:key];
487 }
488
489 //=============================================================================
490 - (void)report {
491   NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@BREAKPAD_URL]];
492   HTTPMultipartUpload *upload = [[HTTPMultipartUpload alloc] initWithURL:url];
493   NSMutableDictionary *uploadParameters = [NSMutableDictionary dictionary];
494
495   if (![self populateServerDictionary:uploadParameters]) {
496     [upload release];
497     return;
498   }
499
500   [upload setParameters:uploadParameters];
501
502   // Add minidump file
503   if (minidumpContents_) {
504     [upload addFileContents:minidumpContents_ name:@"upload_file_minidump"];
505
506     // If there is a log file, upload it together with the minidump.
507     if (logFileData_) {
508       [upload addFileContents:logFileData_ name:@"log"];
509     }
510
511     // Send it
512     NSError *error = nil;
513     NSData *data = [upload send:&error];
514     NSString *result = [[NSString alloc] initWithData:data
515                                          encoding:NSUTF8StringEncoding];
516     const char *reportID = "ERR";
517
518     if (error) {
519       fprintf(stderr, "Breakpad Uploader: Send Error: %s\n",
520               [[error description] UTF8String]);
521     } else {
522       NSCharacterSet *trimSet =
523           [NSCharacterSet whitespaceAndNewlineCharacterSet];
524       reportID = [[result stringByTrimmingCharactersInSet:trimSet] UTF8String];
525       [self logUploadWithID:reportID];
526     }
527
528     // rename the minidump file according to the id returned from the server
529     NSString *minidumpDir =
530         [parameters_ objectForKey:@kReporterMinidumpDirectoryKey];
531     NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey];
532
533     NSString *srcString = [NSString stringWithFormat:@"%@/%@.dmp",
534                                     minidumpDir, minidumpID];
535     NSString *destString = [NSString stringWithFormat:@"%@/%s.dmp",
536                                      minidumpDir, reportID];
537
538     const char *src = [srcString fileSystemRepresentation];
539     const char *dest = [destString fileSystemRepresentation];
540
541     if (rename(src, dest) == 0) {
542       GTMLoggerInfo(@"Breakpad Uploader: Renamed %s to %s after successful " \
543                     "upload",src, dest);
544     }
545     else {
546       // can't rename - don't worry - it's not important for users
547       GTMLoggerDebug(@"Breakpad Uploader: successful upload report ID = %s\n",
548                      reportID );
549     }
550     [result release];
551   } else {
552     // Minidump is missing -- upload just the log file.
553     if (logFileData_) {
554       [self uploadData:logFileData_ name:@"log"];
555     }
556   }
557   [upload release];
558 }
559
560 - (void)uploadData:(NSData *)data name:(NSString *)name {
561   NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@BREAKPAD_URL]];
562   NSMutableDictionary *uploadParameters = [NSMutableDictionary dictionary];
563
564   if (![self populateServerDictionary:uploadParameters])
565     return;
566
567   HTTPMultipartUpload *upload =
568       [[HTTPMultipartUpload alloc] initWithURL:url];
569
570   [uploadParameters setObject:name forKey:@"type"];
571   [upload setParameters:uploadParameters];
572   [upload addFileContents:data name:name];
573
574   [upload send:nil];
575   [upload release];
576 }
577
578 - (void)logUploadWithID:(const char *)uploadID {
579   NSString *minidumpDir =
580       [parameters_ objectForKey:@kReporterMinidumpDirectoryKey];
581   NSString *logFilePath = [NSString stringWithFormat:@"%@/%s",
582       minidumpDir, kReporterLogFilename];
583   NSString *logLine = [NSString stringWithFormat:@"%0.f,%s\n",
584       [[NSDate date] timeIntervalSince1970], uploadID];
585   NSData *logData = [logLine dataUsingEncoding:NSUTF8StringEncoding];
586
587   NSFileManager *fileManager = [NSFileManager defaultManager];
588   if ([fileManager fileExistsAtPath:logFilePath]) {
589     NSFileHandle *logFileHandle =
590        [NSFileHandle fileHandleForWritingAtPath:logFilePath];
591     [logFileHandle seekToEndOfFile];
592     [logFileHandle writeData:logData];
593     [logFileHandle closeFile];
594   } else {
595     [fileManager createFileAtPath:logFilePath
596                          contents:logData
597                        attributes:nil];
598   }
599 }
600
601 //=============================================================================
602 - (NSMutableDictionary *)parameters {
603   return parameters_;
604 }
605
606 //=============================================================================
607 - (void)dealloc {
608   [parameters_ release];
609   [minidumpContents_ release];
610   [logFileData_ release];
611   [googleDictionary_ release];
612   [socorroDictionary_ release];
613   [serverDictionary_ release];
614   [extraServerVars_ release];
615   [super dealloc];
616 }
617
618 @end