// Copyright 2011 Google LLC
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google LLC nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#import <fcntl.h>
#include <stdio.h>
#import <sys/stat.h>
#include <TargetConditionals.h>
#import <unistd.h>

#import <SystemConfiguration/SystemConfiguration.h>

#import "common/mac/HTTPMultipartUpload.h"

#import "client/apple/Framework/BreakpadDefines.h"
#import "client/mac/sender/uploader.h"

const int kMinidumpFileLengthLimit = 2 * 1024 * 1024;  // 2MB

#define kApplePrefsSyncExcludeAllKey \
  @"com.apple.PreferenceSync.ExcludeAllSyncKeys"

NSString *const kGoogleServerType = @"google";
NSString *const kSocorroServerType = @"socorro";
NSString *const kDefaultServerType = @"google";

#pragma mark -

namespace {
// Read one line from the configuration file.
NSString *readString(int fileId) {
  NSMutableString *str = [NSMutableString stringWithCapacity:32];
  char ch[2] = { 0 };

  while (read(fileId, &ch[0], 1) == 1) {
    if (ch[0] == '\n') {
      // Break if this is the first newline after reading some other string
      // data.
      if ([str length])
        break;
    } else {
      [str appendString:[NSString stringWithUTF8String:ch]];
    }
  }

  return str;
}

//=============================================================================
// Read |length| of binary data from the configuration file. This method will
// returns |nil| in case of error.
NSData *readData(int fileId, ssize_t length) {
  NSMutableData *data = [NSMutableData dataWithLength:length];
  char *bytes = (char *)[data bytes];

  if (read(fileId, bytes, length) != length)
    return nil;

  return data;
}

//=============================================================================
// Read the configuration from the config file.
NSDictionary *readConfigurationData(const char *configFile) {
  int fileId = open(configFile, O_RDONLY, 0600);
  if (fileId == -1) {
    fprintf(stderr, "Breakpad Uploader: Couldn't open config file %s - %s",
            configFile, strerror(errno));
  }

  // we want to avoid a build-up of old config files even if they
  // have been incorrectly written by the framework
  if (unlink(configFile)) {
    fprintf(stderr, "Breakpad Uploader: Couldn't unlink config file %s - %s",
            configFile, strerror(errno));
  }

  if (fileId == -1) {
    return nil;
  }

  NSMutableDictionary *config = [NSMutableDictionary dictionary];

  while (1) {
    NSString *key = readString(fileId);

    if (![key length])
      break;

    // Read the data.  Try to convert to a UTF-8 string, or just save
    // the data
    NSString *lenStr = readString(fileId);
    ssize_t len = [lenStr intValue];
    NSData *data = readData(fileId, len);
    id value = [[NSString alloc] initWithData:data
                                     encoding:NSUTF8StringEncoding];

    [config setObject:(value ? value : data) forKey:key];
    [value release];
  }

  close(fileId);
  return config;
}
}  // namespace

#pragma mark -

@interface Uploader(PrivateMethods)

// Update |parameters_| as well as the server parameters using |config|.
- (void)translateConfigurationData:(NSDictionary *)config;

// Read the minidump referenced in |parameters_| and update |minidumpContents_|
// with its content.
- (BOOL)readMinidumpData;

// Read the log files referenced in |parameters_| and update |logFileData_|
// with their content.
- (BOOL)readLogFileData;

// Returns a unique client id (user-specific), creating a persistent
// one in the user defaults, if necessary.
- (NSString*)clientID;

// Returns a dictionary that can be used to map Breakpad parameter names to
// URL parameter names.
- (NSMutableDictionary *)dictionaryForServerType:(NSString *)serverType;

// Helper method to set HTTP parameters based on server type.  This is
// called right before the upload - crashParameters will contain, on exit,
// URL parameters that should be sent with the minidump.
- (BOOL)populateServerDictionary:(NSMutableDictionary *)crashParameters;

// Initialization helper to create dictionaries mapping Breakpad
// parameters to URL parameters
- (void)createServerParameterDictionaries;

// Accessor method for the URL parameter dictionary
- (NSMutableDictionary *)urlParameterDictionary;

// Records the uploaded crash ID to the log file.
- (void)logUploadWithID:(const char *)uploadID;

// Builds an URL parameter for a given dictionary key. Uses Uploader's
// parameters to provide its value. Returns nil if no item is stored for the
// given key.
- (NSURLQueryItem *)queryItemWithName:(NSString *)queryItemName
                          forParamKey:(NSString *)key;
@end

@implementation Uploader

//=============================================================================
- (id)initWithConfigFile:(const char *)configFile {
  NSDictionary *config = readConfigurationData(configFile);
  if (!config)
    return nil;

  return [self initWithConfig:config];
}

//=============================================================================
- (id)initWithConfig:(NSDictionary *)config {
  if ((self = [super init])) {
    // Because the reporter is embedded in the framework (and many copies
    // of the framework may exist) its not completely certain that the OS
    // will obey the com.apple.PreferenceSync.ExcludeAllSyncKeys in our
    // Info.plist. To make sure, also set the key directly if needed.
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    if (![ud boolForKey:kApplePrefsSyncExcludeAllKey]) {
      [ud setBool:YES forKey:kApplePrefsSyncExcludeAllKey];
    }

    [self createServerParameterDictionaries];

    [self translateConfigurationData:config];

    // Read the minidump into memory.
    [self readMinidumpData];
    [self readLogFileData];
  }
  return self;
}

//=============================================================================
+ (NSDictionary *)readConfigurationDataFromFile:(NSString *)configFile {
  return readConfigurationData([configFile fileSystemRepresentation]);
}

//=============================================================================
- (void)translateConfigurationData:(NSDictionary *)config {
  parameters_ = [[NSMutableDictionary alloc] init];

  NSEnumerator *it = [config keyEnumerator];
  while (NSString *key = [it nextObject]) {
    // If the keyname is prefixed by BREAKPAD_SERVER_PARAMETER_PREFIX
    // that indicates that it should be uploaded to the server along
    // with the minidump, so we treat it specially.
    if ([key hasPrefix:@BREAKPAD_SERVER_PARAMETER_PREFIX]) {
      NSString *urlParameterKey =
        [key substringFromIndex:[@BREAKPAD_SERVER_PARAMETER_PREFIX length]];
      if ([urlParameterKey length]) {
        id value = [config objectForKey:key];
        if ([value isKindOfClass:[NSString class]]) {
          [self addServerParameter:(NSString *)value
                            forKey:urlParameterKey];
        } else {
          [self addServerParameter:(NSData *)value
                            forKey:urlParameterKey];
        }
      }
    } else {
      [parameters_ setObject:[config objectForKey:key] forKey:key];
    }
  }

  // generate a unique client ID based on this host's MAC address
  // then add a key/value pair for it
  NSString *clientID = [self clientID];
  [parameters_ setObject:clientID forKey:@"guid"];
}

// Per user per machine
- (NSString *)clientID {
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  NSString *crashClientID = [ud stringForKey:kClientIdPreferenceKey];
  if (crashClientID) {
    return crashClientID;
  }

  // Otherwise, if we have no client id, generate one!
  srandom((int)[[NSDate date] timeIntervalSince1970]);
  long clientId1 = random();
  long clientId2 = random();
  long clientId3 = random();
  crashClientID = [NSString stringWithFormat:@"%lx%lx%lx",
                            clientId1, clientId2, clientId3];

  [ud setObject:crashClientID forKey:kClientIdPreferenceKey];
  [ud synchronize];
  return crashClientID;
}

//=============================================================================
- (BOOL)readLogFileData {
#if TARGET_OS_IPHONE
  return NO;
#else
  unsigned int logFileCounter = 0;

  NSString *logPath;
  size_t logFileTailSize =
      [[parameters_ objectForKey:@BREAKPAD_LOGFILE_UPLOAD_SIZE] intValue];

  NSMutableArray *logFilenames; // An array of NSString, one per log file
  logFilenames = [[NSMutableArray alloc] init];

  char tmpDirTemplate[80] = "/tmp/CrashUpload-XXXXX";
  char *tmpDir = mkdtemp(tmpDirTemplate);

  // Construct key names for the keys we expect to contain log file paths
  for(logFileCounter = 0;; logFileCounter++) {
    NSString *logFileKey = [NSString stringWithFormat:@"%@%d",
                                     @BREAKPAD_LOGFILE_KEY_PREFIX,
                                     logFileCounter];

    logPath = [parameters_ objectForKey:logFileKey];

    // They should all be consecutive, so if we don't find one, assume
    // we're done

    if (!logPath) {
      break;
    }

    NSData *entireLogFile = [[NSData alloc] initWithContentsOfFile:logPath];

    if (entireLogFile == nil) {
      continue;
    }

    NSRange fileRange;

    // Truncate the log file, only if necessary

    if ([entireLogFile length] <= logFileTailSize) {
      fileRange = NSMakeRange(0, [entireLogFile length]);
    } else {
      fileRange = NSMakeRange([entireLogFile length] - logFileTailSize,
                              logFileTailSize);
    }

    char tmpFilenameTemplate[100];

    // Generate a template based on the log filename
    sprintf(tmpFilenameTemplate,"%s/%s-XXXX", tmpDir,
            [[logPath lastPathComponent] fileSystemRepresentation]);

    char *tmpFile = mktemp(tmpFilenameTemplate);

    NSData *logSubdata = [entireLogFile subdataWithRange:fileRange];
    NSString *tmpFileString = [NSString stringWithUTF8String:tmpFile];
    [logSubdata writeToFile:tmpFileString atomically:NO];

    [logFilenames addObject:[tmpFileString lastPathComponent]];
    [entireLogFile release];
  }

  if ([logFilenames count] == 0) {
    [logFilenames release];
    logFileData_ =  nil;
    return NO;
  }

  // now, bzip all files into one
  NSTask *tarTask = [[NSTask alloc] init];

  [tarTask setCurrentDirectoryPath:[NSString stringWithUTF8String:tmpDir]];
  [tarTask setLaunchPath:@"/usr/bin/tar"];

  NSMutableArray *bzipArgs = [NSMutableArray arrayWithObjects:@"-cjvf",
                                             @"log.tar.bz2",nil];
  [bzipArgs addObjectsFromArray:logFilenames];

  [logFilenames release];

  [tarTask setArguments:bzipArgs];
  [tarTask launch];
  [tarTask waitUntilExit];
  [tarTask release];

  NSString *logTarFile = [NSString stringWithFormat:@"%s/log.tar.bz2",tmpDir];
  logFileData_ = [[NSData alloc] initWithContentsOfFile:logTarFile];
  if (logFileData_ == nil) {
    fprintf(stderr, "Breakpad Uploader: Cannot find temp tar log file: %s",
            [logTarFile UTF8String]);
    return NO;
  }
  return YES;
#endif  // TARGET_OS_IPHONE
}

//=============================================================================
- (BOOL)readMinidumpData {
  NSString *minidumpDir =
      [parameters_ objectForKey:@kReporterMinidumpDirectoryKey];
  NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey];

  if (![minidumpID length])
    return NO;

  NSString *path = [minidumpDir stringByAppendingPathComponent:minidumpID];
  path = [path stringByAppendingPathExtension:@"dmp"];

  // check the size of the minidump and limit it to a reasonable size
  // before attempting to load into memory and upload
  const char *fileName = [path fileSystemRepresentation];
  struct stat fileStatus;

  BOOL success = YES;

  if (!stat(fileName, &fileStatus)) {
    if (fileStatus.st_size > kMinidumpFileLengthLimit) {
      fprintf(stderr, "Breakpad Uploader: minidump file too large " \
              "to upload : %d\n", (int)fileStatus.st_size);
      success = NO;
    }
  } else {
      fprintf(stderr, "Breakpad Uploader: unable to determine minidump " \
              "file length\n");
      success = NO;
  }

  if (success) {
    minidumpContents_ = [[NSData alloc] initWithContentsOfFile:path];
    success = ([minidumpContents_ length] ? YES : NO);
  }

  if (!success) {
    // something wrong with the minidump file -- delete it
    unlink(fileName);
  }

  return success;
}

#pragma mark -
//=============================================================================

- (void)createServerParameterDictionaries {
  serverDictionary_ = [[NSMutableDictionary alloc] init];
  socorroDictionary_ = [[NSMutableDictionary alloc] init];
  googleDictionary_ = [[NSMutableDictionary alloc] init];
  extraServerVars_ = [[NSMutableDictionary alloc] init];

  [serverDictionary_ setObject:socorroDictionary_ forKey:kSocorroServerType];
  [serverDictionary_ setObject:googleDictionary_ forKey:kGoogleServerType];

  [googleDictionary_ setObject:@"ptime" forKey:@BREAKPAD_PROCESS_UP_TIME];
  [googleDictionary_ setObject:@"email" forKey:@BREAKPAD_EMAIL];
  [googleDictionary_ setObject:@"comments" forKey:@BREAKPAD_COMMENTS];
  [googleDictionary_ setObject:@"prod" forKey:@BREAKPAD_PRODUCT];
  [googleDictionary_ setObject:@"ver" forKey:@BREAKPAD_VERSION];
  [googleDictionary_ setObject:@"guid" forKey:@"guid"];

  [socorroDictionary_ setObject:@"Comments" forKey:@BREAKPAD_COMMENTS];
  [socorroDictionary_ setObject:@"CrashTime"
                         forKey:@BREAKPAD_PROCESS_CRASH_TIME];
  [socorroDictionary_ setObject:@"StartupTime"
                         forKey:@BREAKPAD_PROCESS_START_TIME];
  [socorroDictionary_ setObject:@"Version"
                         forKey:@BREAKPAD_VERSION];
  [socorroDictionary_ setObject:@"ProductName"
                         forKey:@BREAKPAD_PRODUCT];
  [socorroDictionary_ setObject:@"Email"
                         forKey:@BREAKPAD_EMAIL];
}

- (NSMutableDictionary *)dictionaryForServerType:(NSString *)serverType {
  if (serverType == nil || [serverType length] == 0) {
    return [serverDictionary_ objectForKey:kDefaultServerType];
  }
  return [serverDictionary_ objectForKey:serverType];
}

- (NSMutableDictionary *)urlParameterDictionary {
  NSString *serverType = [parameters_ objectForKey:@BREAKPAD_SERVER_TYPE];
  return [self dictionaryForServerType:serverType];

}

- (BOOL)populateServerDictionary:(NSMutableDictionary *)crashParameters {
  NSDictionary *urlParameterNames = [self urlParameterDictionary];

  id key;
  NSEnumerator *enumerator = [parameters_ keyEnumerator];

  while ((key = [enumerator nextObject])) {
    // The key from parameters_ corresponds to a key in
    // urlParameterNames.  The value in parameters_ gets stored in
    // crashParameters with a key that is the value in
    // urlParameterNames.

    // For instance, if parameters_ has [PRODUCT_NAME => "FOOBAR"] and
    // urlParameterNames has [PRODUCT_NAME => "pname"] the final HTTP
    // URL parameter becomes [pname => "FOOBAR"].
    NSString *breakpadParameterName = (NSString *)key;
    NSString *urlParameter = [urlParameterNames
                                   objectForKey:breakpadParameterName];
    if (urlParameter) {
      [crashParameters setObject:[parameters_ objectForKey:key]
                          forKey:urlParameter];
    }
  }

  // Now, add the parameters that were added by the application.
  enumerator = [extraServerVars_ keyEnumerator];

  while ((key = [enumerator nextObject])) {
    NSString *urlParameterName = (NSString *)key;
    NSString *urlParameterValue =
      [extraServerVars_ objectForKey:urlParameterName];
    [crashParameters setObject:urlParameterValue
                        forKey:urlParameterName];
  }
  return YES;
}

- (void)addServerParameter:(id)value forKey:(NSString *)key {
  [extraServerVars_ setObject:value forKey:key];
}

//=============================================================================
- (void)handleNetworkResponse:(NSData *)data withError:(NSError *)error {
  NSString *result = [[NSString alloc] initWithData:data
                                           encoding:NSUTF8StringEncoding];
  const char *reportID = "ERR";
  if (error) {
    fprintf(stderr, "Breakpad Uploader: Send Error: %s\n",
            [[error description] UTF8String]);
  } else {
    NSCharacterSet *trimSet =
        [NSCharacterSet whitespaceAndNewlineCharacterSet];
    reportID = [[result stringByTrimmingCharactersInSet:trimSet] UTF8String];
    [self logUploadWithID:reportID];
  }
  if (uploadCompletion_) {
    uploadCompletion_([NSString stringWithUTF8String:reportID], error);
  }

  // rename the minidump file according to the id returned from the server
  NSString *minidumpDir =
      [parameters_ objectForKey:@kReporterMinidumpDirectoryKey];
  NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey];

  NSString *srcString = [NSString stringWithFormat:@"%@/%@.dmp",
                                  minidumpDir, minidumpID];
  NSString *destString = [NSString stringWithFormat:@"%@/%s.dmp",
                                   minidumpDir, reportID];

  const char *src = [srcString fileSystemRepresentation];
  const char *dest = [destString fileSystemRepresentation];

  if (rename(src, dest) == 0) {
    fprintf(stderr,
            "Breakpad Uploader: Renamed %s to %s after successful upload", src,
            dest);
  }
  else {
    // can't rename - don't worry - it's not important for users
    fprintf(stderr, "Breakpad Uploader: successful upload report ID = %s\n",
            reportID);
  }
  [result release];
}

//=============================================================================
- (NSURLQueryItem *)queryItemWithName:(NSString *)queryItemName
                          forParamKey:(NSString *)key {
  NSString *value = [parameters_ objectForKey:key];
  NSString *escapedValue =
    [value stringByAddingPercentEncodingWithAllowedCharacters:
      [NSCharacterSet URLQueryAllowedCharacterSet]];
  return [NSURLQueryItem queryItemWithName:queryItemName value:escapedValue];
}

//=============================================================================
- (void)setUploadCompletionBlock:(UploadCompletionBlock)uploadCompletion {
  uploadCompletion_ = uploadCompletion;
}

//=============================================================================
- (void)report {
  NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@BREAKPAD_URL]];

  NSString *serverType = [parameters_ objectForKey:@BREAKPAD_SERVER_TYPE];
  if ([serverType length] == 0 ||
      [serverType isEqualToString:kGoogleServerType]) {
    // when communicating to Google's crash collecting service, add URL params
    // which identify the product
    NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url
                                                resolvingAgainstBaseURL:false];
    NSMutableArray *queryItemsToAdd = [urlComponents.queryItems mutableCopy];
    if (queryItemsToAdd == nil) {
      queryItemsToAdd = [[NSMutableArray alloc] init];
    }

    NSURLQueryItem *queryItemProduct =
      [self queryItemWithName:@"product" forParamKey:@BREAKPAD_PRODUCT];
    NSURLQueryItem *queryItemVersion =
      [self queryItemWithName:@"version" forParamKey:@BREAKPAD_VERSION];
    NSURLQueryItem *queryItemGuid =
      [self queryItemWithName:@"guid" forParamKey:@"guid"];

    if (queryItemProduct != nil) [queryItemsToAdd addObject:queryItemProduct];
    if (queryItemVersion != nil) [queryItemsToAdd addObject:queryItemVersion];
    if (queryItemGuid != nil) [queryItemsToAdd addObject:queryItemGuid];

    urlComponents.queryItems = queryItemsToAdd;
    url = [urlComponents URL];
  }

  HTTPMultipartUpload *upload = [[HTTPMultipartUpload alloc] initWithURL:url];
  NSMutableDictionary *uploadParameters = [NSMutableDictionary dictionary];

  if (![self populateServerDictionary:uploadParameters]) {
    [upload release];
    return;
  }

  [upload setParameters:uploadParameters];

  // Add minidump file
  if (minidumpContents_) {
    [upload addFileContents:minidumpContents_ name:@"upload_file_minidump"];

    // If there is a log file, upload it together with the minidump.
    if (logFileData_) {
      [upload addFileContents:logFileData_ name:@"log"];
    }

    // Send it
    NSError *error = nil;
    NSData *data = [upload send:&error];

    if (![url isFileURL]) {
      [self handleNetworkResponse:data withError:error];
    } else {
      if (error) {
        fprintf(stderr, "Breakpad Uploader: Error writing request file: %s\n",
                [[error description] UTF8String]);
      }
    }

  } else {
    // Minidump is missing -- upload just the log file.
    if (logFileData_) {
      [self uploadData:logFileData_ name:@"log"];
    }
  }
  [upload release];
}

- (void)uploadData:(NSData *)data name:(NSString *)name {
  NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@BREAKPAD_URL]];
  NSMutableDictionary *uploadParameters = [NSMutableDictionary dictionary];

  if (![self populateServerDictionary:uploadParameters])
    return;

  HTTPMultipartUpload *upload =
      [[HTTPMultipartUpload alloc] initWithURL:url];

  [uploadParameters setObject:name forKey:@"type"];
  [upload setParameters:uploadParameters];
  [upload addFileContents:data name:name];

  [upload send:nil];
  [upload release];
}

- (void)logUploadWithID:(const char *)uploadID {
  NSString *minidumpDir =
      [parameters_ objectForKey:@kReporterMinidumpDirectoryKey];
  NSString *logFilePath = [NSString stringWithFormat:@"%@/%s",
      minidumpDir, kReporterLogFilename];
  NSString *logLine = [NSString stringWithFormat:@"%0.f,%s\n",
      [[NSDate date] timeIntervalSince1970], uploadID];
  NSData *logData = [logLine dataUsingEncoding:NSUTF8StringEncoding];

  NSFileManager *fileManager = [NSFileManager defaultManager];
  if ([fileManager fileExistsAtPath:logFilePath]) {
    NSFileHandle *logFileHandle =
       [NSFileHandle fileHandleForWritingAtPath:logFilePath];
    [logFileHandle seekToEndOfFile];
    [logFileHandle writeData:logData];
    [logFileHandle closeFile];
  } else {
    [fileManager createFileAtPath:logFilePath
                         contents:logData
                       attributes:nil];
  }
}

//=============================================================================
- (NSMutableDictionary *)parameters {
  return parameters_;
}

//=============================================================================
- (void)dealloc {
  [parameters_ release];
  [minidumpContents_ release];
  [logFileData_ release];
  [googleDictionary_ release];
  [socorroDictionary_ release];
  [serverDictionary_ release];
  [extraServerVars_ release];
  [super dealloc];
}

@end
