/* whoopsie
 * 
 * Copyright © 2011-2013 Canonical Ltd.
 * Author: Evan Dandrea <evan.dandrea@canonical.com>
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#define _XOPEN_SOURCE
#define _GNU_SOURCE

#include <limits.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <gio/gio.h>
#include <assert.h>
#include <curl/curl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/file.h>
#include <errno.h>
#include <signal.h>
#include <pwd.h>
#include <grp.h>
#include <sys/capability.h>
#include <sys/prctl.h>
#include <sys/mount.h>
#include <sys/time.h>
#include <sys/resource.h>

#include "bson/bson.h"
#include "whoopsie.h"
#include "utils.h"
#include "connectivity.h"
#include "monitor.h"
#include "identifier.h"
#include "logging.h"
#include "globals.h"

/* The length of time to wait before processing outstanding crashes, in seconds
 */
#define PROCESS_OUTSTANDING_TIMEOUT 7200

/* If true, we have an active Internet connection. True by default in case we
 * can't bring up GNetworkMonitor */
static gboolean online_state = TRUE;

/* The URL of the crash database. */
static char* crash_db_url = NULL;

/* Username we will run under */
static const char* username = "whoopsie";

/*  The database identifier. Either:
 *  - The android serial number, taken from sys, and SHA-512 hashed
 *  - The system UUID, taken from the DMI tables and SHA-512 hashed
 *  - The MAC address of the first non-loopback device, SHA-512 hashed */
static char* whoopsie_identifier = NULL;

/* The URL for sending the initial crash report */
static char* crash_db_submit_url = NULL;

/* The file path and descriptor for our instance lock */
static const char* lock_path = "/var/lock/whoopsie/lock";
static int lock_fd = 0;

/* The report directory */
static const char* report_dir = "/var/crash";

/* Options */
int foreground = 0;

#ifndef TEST
static int assume_online = 0;
static GOptionEntry option_entries[] = {
    { "foreground", 'f', 0, G_OPTION_ARG_NONE, &foreground, "Run in the foreground", NULL },
    { "assume-online", 'a', 0, G_OPTION_ARG_NONE, &assume_online, "Always assume there is a route to $CRASH_DB_URL.", NULL },
    { NULL }
};
#endif

/* Fields that can be larger than 1KB if present, always send them */
static const char* acceptable_fields[] = {
    "ProblemType",
    "Date",
    "Traceback",
    "Signal",
    "PythonArgs",
    "Package",
    "SourcePackage",
    "PackageArchitecture",
    "Dependencies",
    "MachineType",
    "StacktraceAddressSignature",
    "ApportVersion",
    "DuplicateSignature",
    /* add_os_info */
    "DistroRelease",
    "Uname",
    "Architecture",
    "NonfreeKernelModules",
    "LiveMediaBuild",
    /* add_user_info */
    "UserGroups",
    /* add_proc_info */
    "ExecutablePath",
    "InterpreterPath",
    "ExecutableTimestamp",
    "ProcCwd",
    "ProcEnviron",
    "ProcCmdline",
    "ProcStatus",
    "ProcMaps",
    "ProcAttrCurrent",
    "ProcCpuinfoMinimal",
    /* add_gdb_info */
    "Registers",
    "Disassembly",
    /* used to repair the StacktraceAddressSignature if it is corrupt */
    "StacktraceTop",
    "AssertionMessage",
    "ProcAttrCurrent",
    "CoreDump",
    /* add_kernel_crash_info */
    "VmCore",
    /* We use package-from-proposed tag to determine if a problem is occuring
     * in the release-proposed pocket. */
    "Tags",
    /* We need the OopsText field to be able to generate a crash signature from
     * KernelOops problems */
    "OopsText",
    /* We use the UpgradeStatus field to determine the time between upgrading
     * and the initial error report */
    "UpgradeStatus",
    /* We use the InstallationDate and InstallationMedia fields to determine
     * the time between installing and the initial error report */
    "InstallationDate",
    "InstallationMedia",
    /* Dumps for debugging iwlwifi firmware crashes */
    "IwlFwDump",
    /* System Image information from imaged systems */
    "SystemImageInfo",
    /* We always want to know why apport thought it shouldn't be reported. */
    "UnreportableReason",
    /* Package Install Failure Information */
    /* update-manager */
    "DpkgHistoryLog.txt",
    "DpkgTerminalLog.txt",
    "AptDaemon",
    "HWEunsupported",
    /* aptdaemon */
    "AptConfig",
    "SourcesList",
    "AptHistoryLog",
    "AptTermLog",
    "DpkgLog",
    "SysLog",
    /* openjdk-8 */
    "HotspotError",

    NULL,
};

/* Fields that we don't ever need, always */
static const char* unacceptable_fields[] = {
    /* We do not need these since we retrace with ddebs on errors */
    "Stacktrace",
    "ThreadStacktrace",

    /* We'll have our own count in the database. */
    "CrashCounter",

    /* MarkForUpload is redundant since the crash was uploaded. */
    "_MarkForUpload",

    "Title",

    NULL
};

static gboolean
is_in_field_list (const char* field, const char * field_list[])
{
    const char** p;

    g_return_val_if_fail (field, FALSE);

    p = field_list;
    while (*p) {
        if (strcmp (*p, field) == 0)
            return TRUE;
        p++;
    }
    return FALSE;
}

gboolean
append_key_value (gpointer key, gpointer value, gpointer bson_string)
{
    /* Takes a key and its value from a #GHashTable and adds it to a BSON string
     * as key and its string value. Return %FALSE on error. */

    bson* str = (bson*) bson_string;
    char* k = (char*) key;
    char* v = (char*) value;

    /* We don't send the core dump in the first upload, as the server might not
     * need it */
    if (!strcmp ("CoreDump", k))
        return TRUE;
    if (!strcmp ("VmCore", k))
        return TRUE;

    return bson_append_string (str, k, v) != BSON_ERROR;
}

size_t
server_response (char* ptr, size_t size, size_t nmemb, void* s)
{
    struct response_string* resp = (struct response_string*) s;
    grow_response_string (resp, ptr, size * nmemb);
    return size * nmemb;
}

void
split_string (char* head, char** tail)
{
    g_return_if_fail (head);
    g_return_if_fail (tail);

    *tail = strchr (head, ' ');
    if (*tail) {
        **tail = '\0';
        (*tail)++;
    }
}


gboolean
bsonify (GHashTable* report, bson* b, const char** bson_message,
         int* bson_message_len)
{
    /* Attempt to convert a #GHashTable of the report into a BSON string.
     * On error return %FALSE. */

    GHashTableIter iter;
    gpointer key, value;

    *bson_message = NULL;
    *bson_message_len = 0;

    g_return_val_if_fail (report, FALSE);

    bson_init (b);
    if (!bson_data (b))
        return FALSE;

    g_hash_table_iter_init (&iter, report);
    while (g_hash_table_iter_next (&iter, &key, &value)) {
        if (!append_key_value (key, value, b))
            return FALSE;
    }
    if (bson_finish (b) == BSON_ERROR)
        return FALSE;

    *bson_message = bson_data (b);
    *bson_message_len = bson_size (b);
    if (*bson_message_len > 0 && *bson_message)
        return TRUE;
    else
        return FALSE;
}

int
upload_report (const char* message_data, int message_len, struct response_string* s)
{
    CURL* curl = NULL;
    CURLcode result_code = 0;
    long response_code = 0;
    struct curl_slist* list = NULL;

    g_return_val_if_fail (message_data, -1);

    /* TODO use curl_share for DNS caching. */
    /* Repeated calls to curl_global_init will have no effect. */
    if (curl_global_init (CURL_GLOBAL_SSL)) {
        log_msg ("Unable to initialize curl.\n\n");
        exit (EXIT_FAILURE);
    }

    if ((curl = curl_easy_init ()) == NULL) {
        log_msg ("Couldn't init curl.\n");
        return FALSE;
    }
    curl_easy_setopt (curl, CURLOPT_POST, 1);
    curl_easy_setopt (curl, CURLOPT_NOPROGRESS, 1);
    list = curl_slist_append (list, "Content-Type: application/octet-stream");
    list = curl_slist_append (list, "X-Whoopsie-Version: " VERSION);
    curl_easy_setopt (curl, CURLOPT_URL, crash_db_submit_url);
    curl_easy_setopt (curl, CURLOPT_HTTPHEADER, list);
    curl_easy_setopt (curl, CURLOPT_POSTFIELDSIZE, message_len);
    curl_easy_setopt (curl, CURLOPT_POSTFIELDS, (void*)message_data);
    curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, server_response);
    curl_easy_setopt (curl, CURLOPT_WRITEDATA, s);
    curl_easy_setopt (curl, CURLOPT_VERBOSE, 0L);

    result_code = curl_easy_perform (curl);
    curl_slist_free_all(list);
    curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &response_code);

    log_msg ("Sent; server replied with: %s\n",
        curl_easy_strerror (result_code));
    log_msg ("Response code: %ld\n", response_code);
    curl_easy_cleanup (curl);

    if (result_code != CURLE_OK)
        return result_code;
    else
        return response_code;
}

void
destroy_key_and_value (gpointer key, gpointer value, gpointer user_data)
{
    if (key)
        g_free (key);
    /* The value may be "", which is allocated on the stack. */
    if (value && *(char*)value != '\0')
        g_free (value);
}

GHashTable*
parse_report (const char* report_path, gboolean full_report, GError** error)
{
    /* We'll eventually modify the contents of the report, rather than sending
     * it as-is, to make it more amenable to what the server has to stick in
     * the database, and thus creating less work server-side.
     */

    GMappedFile* fp = NULL;
    GHashTable* hash_table = NULL;
    gchar* contents = NULL;
    gsize file_len = 0;
    /* Our position in the file. */
    gchar* p = NULL;
    /* The end or length of the token. */
    gchar* token_p = NULL;
    char* key = NULL;
    char* value = NULL;
    gchar* value_p = NULL;
    GError* err = NULL;
    gchar* end = NULL;
    int value_length;
    int value_pos;

    g_return_val_if_fail (report_path, NULL);

    if (g_file_test (report_path, G_FILE_TEST_IS_SYMLINK) ||
        !g_file_test (report_path, G_FILE_TEST_IS_REGULAR)) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "%s is a symlink or is not a regular file.", report_path);
        return NULL;
    }
    /* TODO handle the file being modified underneath us. */
    fp = g_mapped_file_new (report_path, FALSE, &err);
    if (err) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "Unable to map report: %s", err->message);
        g_error_free (err);
        goto error;
    }

    contents = g_mapped_file_get_contents (fp);
    file_len = g_mapped_file_get_length (fp);
    end = contents + file_len;
    hash_table = g_hash_table_new (g_str_hash, g_str_equal);
    p = contents;

    while (p < end) {
        /* We're either at the beginning of the file or the start of a line,
         * otherwise this report is corrupted. */
        if (!(p == contents || *(p-1) == '\n')) {
            g_set_error (error, g_quark_from_static_string ("whoopsie-quark"),
                         0, "Malformed report.");
            goto error;
        }
        if (*p == ' ') {
            if (!key) {
                g_set_error (error,
                             g_quark_from_static_string ("whoopsie-quark"), 0,
                             "Report may not start with a value.");
                goto error;
            }
            /* Skip the space. */
            p++;
            token_p = p;
            while (token_p < end && *token_p != '\n')
                token_p++;

            /* The length of this value string */
            value_length = token_p - p;
            if (value) {
                /* Space for the leading newline too. */
                value_pos = value_p - value;
                if (INT_MAX - (1 + value_length + 1) < value_pos) {
                    g_set_error (error,
                                 g_quark_from_static_string ("whoopsie-quark"),
                                 0, "Report value too long.");
                    goto error;
                }
                value = g_realloc (value, value_pos + 1 + value_length + 1);
                value_p = value + value_pos;
                *value_p = '\n';
                value_p++;
            } else {
                value = g_realloc (value, value_length + 1);
                value_p = value;
            }
            memcpy (value_p, p, value_length);
            value_p[value_length] = '\0';
            for (char *c = value_p; c < value_p + value_length; c++)
                /* If c is a control character but not TAB. */
                if (*c != '\t' && *c >= '\0' && *c < ' ')
                    *c = '?';
            value_p += value_length;
            g_hash_table_insert (hash_table, key, value ? value : "");
            p = token_p + 1;
        } else {
            /* Reset the value pointer. */
            value = NULL;
            /* Key. */
            token_p = p;
            while (token_p < end) {
                if (*token_p != ':') {
                    if (*token_p == '\n') {
                        /* No colon character found on this line */
                        g_set_error (error,
                                g_quark_from_static_string ("whoopsie-quark"),
                                0, "Report key must have a value.");
                        goto error;
                    }
                    token_p++;
                } else if ((*(token_p + 1) == '\n' &&
                            *(token_p + 2) != ' ')) {
                        /* The next line doesn't start with a value */
                        g_set_error (error,
                                g_quark_from_static_string ("whoopsie-quark"),
                                0, "Report key must have a value.");
                        goto error;
                } else {
                    break;
                }
            }
            key = g_malloc ((token_p - p) + 1);
            memcpy (key, p, (token_p - p));
            key[(token_p - p)] = '\0';

            /* Replace any embedded NUL bytes. */
            for (char *c = key; c < key + (token_p - p); c++)
                if (*c >= '\0' && *c < ' ')
                    *c = '?';

            /* Eat the semicolon. */
            token_p++;

            /* Skip any leading spaces. */
            while (token_p < end && *token_p == ' ')
                token_p++;

            /* Start of the value. */
            p = token_p;

            while (token_p < end && *token_p != '\n')
                token_p++;
            if ((token_p - p) == 0) {
                /* Empty value. The key likely has a child. */
                value = NULL;
            } else {
                if (!strncmp ("base64", p, 6)) {
                    /* Just a marker that the following lines are base64
                     * encoded. Don't include it in the value. */
                    value = NULL;
                } else {
                    /* Value. */
                    value = g_malloc ((token_p - p) + 1);
                    memcpy (value, p, (token_p - p));
                    value[(token_p - p)] = '\0';
                    for (char *c = value; c < value + (token_p - p); c++)
                        if (*c >= '\0' && *c < ' ')
                            *c = '?';
                    value_p = value + (token_p - p);
                }
            }
            p = token_p + 1;

            g_hash_table_insert (hash_table, key, value ? value : "");
        }
    }
    g_mapped_file_unref (fp);

    /* Remove entries that we don't want to send */
    if (!full_report) {
        GHashTableIter iter;
        gpointer key, value;
        g_hash_table_iter_init(&iter, hash_table);

        /* We want everything that is in our white list or less than
           1 KB so that we don't end up DoSing our database. */
        while (g_hash_table_iter_next(&iter, &key, &value)) {
            if (!is_in_field_list((const char *)key, unacceptable_fields)) {
                if (is_in_field_list((const char *)key, acceptable_fields))
                    continue;
                if (strlen((const char *)value) < 1024)
                    continue;
            }

            g_hash_table_iter_steal(&iter);
            destroy_key_and_value(key, value, NULL);
        }
    }

    return hash_table;

error:
    if (hash_table) {
        g_hash_table_foreach (hash_table, destroy_key_and_value, NULL);
        g_hash_table_destroy (hash_table);
    }
    g_mapped_file_unref (fp);
    return NULL;
}

gboolean
upload_core (const char* uuid, const char* arch, const char* core_data) {

    CURL* curl = NULL;
    CURLcode result_code = 0;
    long response_code = 0;
    struct curl_slist* list = NULL;
    char* crash_db_core_url = NULL;
    struct response_string s;


    g_return_val_if_fail (uuid, FALSE);
    g_return_val_if_fail (arch, FALSE);
    g_return_val_if_fail (core_data, FALSE);

    crash_db_core_url = g_strdup_printf ("%s/%s/submit-core/%s/%s",
                                         crash_db_url, uuid, arch,
                                         whoopsie_identifier);

    /* TODO use CURLOPT_READFUNCTION to transparently compress data with
     * Snappy. */
    if ((curl = curl_easy_init ()) == NULL) {
        log_msg ("Couldn't init curl.\n");
        g_free (crash_db_core_url);
        return FALSE;
    }
    init_response_string (&s);
    curl_easy_setopt (curl, CURLOPT_POST, 1);
    curl_easy_setopt (curl, CURLOPT_NOPROGRESS, 1);
    list = curl_slist_append (list, "Content-Type: application/octet-stream");
    list = curl_slist_append (list, "X-Whoopsie-Version: " VERSION);
    curl_easy_setopt (curl, CURLOPT_URL, crash_db_core_url);
    curl_easy_setopt (curl, CURLOPT_HTTPHEADER, list);
    curl_easy_setopt (curl, CURLOPT_POSTFIELDS, (void*)core_data);
    curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, server_response);
    curl_easy_setopt (curl, CURLOPT_WRITEDATA, &s);
    curl_easy_setopt (curl, CURLOPT_VERBOSE, 0L);

    result_code = curl_easy_perform (curl);
    curl_slist_free_all(list);

    curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &response_code);
    /* this actually what curl replied with */
    log_msg ("Sent; server replied with: %s\n",
        curl_easy_strerror (result_code));
    log_msg ("Response code: %ld\n", response_code);
    curl_easy_cleanup (curl);
    g_free (crash_db_core_url);
    destroy_response_string (&s);

    return result_code == CURLE_OK && response_code == 200;
}

void
handle_response (GHashTable* report, char* response_data)
{
    char* command = NULL;
    char* core = NULL;
    char* arch = NULL;

    g_return_if_fail (report);

    /* Command could be CORE, which requests the core dump, BUG ######, if in a
     * development release, which points to the bug report, or UPDATE, if this
     * is fixed in an update. */
    split_string (response_data, &command);
    if (command) {
        if (strcmp (command, "CORE") == 0) {
            log_msg ("Reported OOPS ID %.36s\n", response_data);
            core = g_hash_table_lookup (report, "CoreDump");
            arch = g_hash_table_lookup (report, "Architecture");
            if (core && arch) {
                if (!upload_core (response_data, arch, core))
                    /* We do not retry the upload. Once is a big enough hit to
                     * their Internet connection, and we can always count on
                     * the next person in line to send it. */
                    log_msg ("Upload of the core dump failed.\n");
            } else if (strcmp (command, "OOPSID") == 0) {
                log_msg ("Reported OOPS ID %.36s\n", response_data);
            } else
                log_msg ("Asked for a core dump that we don't have.\n");
        } else if (strcmp (command, "OOPSID") == 0) {
            log_msg ("Reported OOPS ID %.36s\n", response_data);
        } else
            log_msg ("Got command: %s\n", command);
    }
}

gboolean
parse_and_upload_report (const char* crash_file)
{
    GHashTable* report = NULL;
    gboolean success = FALSE;
    int message_len = 0;
    const char* message_data = NULL;
    struct response_string s;
    GError* error = NULL;
    bson b[1];
    int response = 0;

    log_msg ("Parsing %s.\n", crash_file);
    report = parse_report (crash_file, FALSE, &error);
    if (!report) {
        if (error) {
            log_msg ("Unable to parse report (%s): %s\n", crash_file,
                       error->message);
            g_error_free (error);
        } else {
            log_msg ("Unable to parse report (%s)\n", crash_file);
        }
        /* Do not keep trying to parse and upload this */
        return TRUE;
    }

    if (!bsonify (report, b, &message_data, &message_len)) {
        log_msg ("Unable to bsonify report (%s)\n", crash_file);
        if (bson_data (b))
            bson_destroy (b);
        /* Do not keep trying to parse and upload this */
        success = TRUE;
    } else {
        log_msg ("Uploading %s.\n", crash_file);
        init_response_string (&s);
        response = upload_report (message_data, message_len, &s);
        if (bson_data (b))
            bson_destroy (b);

        /* If the response code is 400, the server did not like what we sent it.
         * Sending the same thing again is not likely to change that */
        /* TODO check that there aren't 400 responses that we care about seeing
         * again, such as a transient database failure. */
        if (response == 200 || response == 400)
            success = TRUE;
        else
            success = FALSE;

        if (response > 200) {
            log_msg ("Server replied with:\n");
            log_msg ("%s\n", s.p);
        }

        if (response == 200 && s.length > 0)
            handle_response (report, s.p);
        destroy_response_string (&s);
    }

    g_hash_table_foreach (report, destroy_key_and_value, NULL);
    g_hash_table_destroy (report);

    return success;
}

gboolean
process_existing_files (const char* report_dir)
{
    GDir* dir = NULL;
    const gchar* file = NULL;
    const gchar* ext = NULL;
    char* upload_file = NULL;
    char* crash_file = NULL;

    dir = g_dir_open (report_dir, 0, NULL);
    while ((file = g_dir_read_name (dir)) != NULL) {

        upload_file = g_build_filename (report_dir, file, NULL);
        if (!upload_file)
            continue;

        ext = strrchr (upload_file, '.');
        if (ext && strcmp(++ext, "upload") != 0) {
            g_free (upload_file);
            continue;
        }

        crash_file = change_file_extension (upload_file, ".crash");
        if (!crash_file) {
            g_free (upload_file);
            continue;
        }

        if (already_handled_report (crash_file)) {
            g_free (upload_file);
            g_free (crash_file);
            continue;
        } else if (online_state && parse_and_upload_report (crash_file)) {
            if (!mark_handled (crash_file)) {
                log_msg ("Unable to mark report as seen (%s) removing it.\n", crash_file);
                g_unlink (crash_file);
            }
        }

        g_free (upload_file);
        g_free (crash_file);
    }
    g_dir_close (dir);

    return G_SOURCE_CONTINUE;
}

void daemonize (void)
{
    pid_t pid, sid;
    int i;
    struct rlimit rl = {0};

    if (getrlimit (RLIMIT_NOFILE, &rl) < 0) {
        log_msg ("Could not get resource limits.\n");
        exit (EXIT_FAILURE);
    }

    umask (0);
    pid = fork();
    if (pid < 0)
        exit (EXIT_FAILURE);
    if (pid > 0)
        exit (EXIT_SUCCESS);
    sid = setsid ();
    if (sid < 0)
        exit (EXIT_FAILURE);

    if ((chdir ("/")) < 0)
        exit (EXIT_FAILURE);

    for (i = 0; i < rl.rlim_max && i < 1024; i++) {
        if (i != lock_fd)
            close (i);
    }
    if ((open ("/dev/null", O_RDWR) != 0) ||
        (dup (0) != 1) ||
        (dup (0) != 2)) {
        log_msg ("Could not redirect file descriptors to /dev/null.\n");
        exit (EXIT_FAILURE);
    }
}

void
exit_if_already_running (void)
{
    int rc = 0;

    if (g_getenv ("APPORT_REPORT_DIR")) {
        /* keep lock file in custom report directory */
        lock_path = g_build_filename (report_dir, "whoopsie_lock", NULL);
    } else {
        /* use system directory */
        if (mkdir ("/var/lock/whoopsie", 0755) < 0) {
            if (errno != EEXIST) {
                log_msg ("Could not create lock directory.\n");
            }
        }
    }

    log_msg ("Using lock path: %s\n", lock_path);

    lock_fd = open (lock_path, O_CREAT | O_RDWR, 0600);
    rc = flock (lock_fd, LOCK_EX | LOCK_NB);
    if (rc) {
        if (EWOULDBLOCK == errno) {
            log_msg ("Another instance is already running.\n");
            exit (1);
        } else {
            log_msg ("Could not create lock file: %s\n", strerror (errno));
        }
    }
}

char*
get_crash_db_url (void)
{
    const char* url = NULL;

    url = g_getenv ("CRASH_DB_URL");
    if (url == NULL)
        return NULL;

    if ((strncasecmp ("http://", url, 7) || url[7] == '\0') &&
        (strncasecmp ("https://", url, 8) || url[8] == '\0'))
        return NULL;
    return g_strdup (url);
}

void
drop_privileges (GError** error)
{
    struct passwd *pw = NULL;

    if (getuid () != 0) {
        if (g_getenv ("CRASH_DB_IDENTIFIER") == NULL) {
            g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                         "You must be root to run this program, or set $CRASH_DB_IDENTIFIER.");
        }
        return;
    }
    if (!(pw = getpwnam (username))) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "Failed to find user: %s", username);
        return;
    }

    /* Drop privileges */
    if (setgroups (1, &pw->pw_gid) < 0 ||
        setresgid (pw->pw_gid, pw->pw_gid, pw->pw_gid) < 0 ||
        setresuid (pw->pw_uid, pw->pw_uid, pw->pw_uid) < 0) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "Failed to become user: %s", username);
        return;
    }

    if (prctl (PR_SET_DUMPABLE, 1))
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "Failed to ensure core dump production.");

    if ((setenv ("USER", username, 1) < 0) ||
        (setenv ("USERNAME", username, 1) < 0)) {
        g_set_error (error, g_quark_from_static_string ("whoopsie-quark"), 0,
                     "Failed to set user environment variables.");
        return;
    }
}

void
network_changed (gboolean available)
{
    if (online_state != available)
        log_msg (available ? "online\n" : "offline\n");

    if (!available) {
        online_state = FALSE;
        return;
    }

    if (online_state && available)
        return;

    online_state = available;

    if (online_state)
        process_existing_files (report_dir);
}

gboolean
check_online_then_upload (const char* crash_file) {

    if (!online_state) {
        log_msg ("Not online; processing later (%s).\n", crash_file);
        return FALSE;
    }

    if (!parse_and_upload_report (crash_file)) {
        log_msg ("Could not upload; processing later (%s).\n", crash_file);
        return FALSE;
    }

    return TRUE;
}

void
create_crash_directory (void)
{
    struct passwd *pw = NULL;

    if (mkdir (report_dir, 0755) < 0) {
        if (errno != EEXIST) {
            log_msg ("Could not create non-existent report_directory to monitor (%d): %s.\n", errno, report_dir);
            exit (EXIT_FAILURE);
        }
    } else {
        /* Only change the permissions if we've just created it */
        if (!(pw = getpwnam (username))) {
            log_msg ("Could not find user, %s.\n", username);
            exit (EXIT_FAILURE);
        }
        if (chown (report_dir, -1, pw->pw_gid) < 0) {
            log_msg ("Could not change ownership of %s.\n", report_dir);
            exit (EXIT_FAILURE);
        }
        if (chmod (report_dir, 03777) < 0) {
            log_msg ("Could not change permissions on %s.\n", report_dir);
            exit (EXIT_FAILURE);
        }
    }
}

#ifndef TEST
static GMainLoop* loop = NULL;

static void
parse_arguments (int* argc, char** argv[])
{
    GError* err = NULL;
    GOptionContext* context;

    context = g_option_context_new (NULL);
    g_option_context_add_main_entries (context, option_entries, NULL);
    if (!g_option_context_parse (context, argc, argv, &err)) {
        log_msg ("whoopsie: %s\n", err->message);
        g_error_free (err);
        exit (EXIT_FAILURE);
    }
    g_option_context_free (context);
}

static void
handle_signals (int signo)
{
    if (loop)
        g_main_loop_quit (loop);
    else
        exit (0);
}

static void
setup_signals (void)
{
    struct sigaction action;
    sigset_t mask;

    sigemptyset (&mask);
    action.sa_handler = handle_signals;
    action.sa_mask = mask;
    action.sa_flags = 0;
    sigaction (SIGTERM, &action, NULL);
    sigaction (SIGINT, &action, NULL);
}

int
main (int argc, char** argv)
{
    GError* err = NULL;
    const gchar* env;
    GFileMonitor* monitor;

    setup_signals ();
    parse_arguments (&argc, &argv);

    if (!foreground) {
        open_log ();
        log_msg ("whoopsie " VERSION " starting up.\n");
    }

    if ((crash_db_url = get_crash_db_url ()) == NULL) {
        log_msg ("Could not get crash database location.\n");
        exit (EXIT_FAILURE);
    }

    /* environment might change report directory and identifier */
    env = g_getenv ("APPORT_REPORT_DIR");
    if (env != NULL && *env != '\0')
        report_dir = g_strdup (env);

    env = g_getenv ("CRASH_DB_IDENTIFIER");
    if (env != NULL)
        whoopsie_identifier = g_strdup (env);
    else
        whoopsie_identifier_generate (&whoopsie_identifier, &err);

    if (err) {
        log_msg ("%s\n", err->message);
        g_error_free (err);
        err = NULL;
        crash_db_submit_url = strdup (crash_db_url);
    } else {
        crash_db_submit_url = g_strdup_printf ("%s/%s", crash_db_url,
                                               whoopsie_identifier);
    }

    /* Only publish whoopsie-id if it was generated. LP: #1389357 */
    if (env == NULL) {
        g_file_set_contents (WHOOPSIE_ID_PATH, whoopsie_identifier, -1, NULL);
        chmod (WHOOPSIE_ID_PATH, 00600);
    }

    create_crash_directory ();

    drop_privileges (&err);
    if (err) {
        log_msg ("%s\n", err->message);
        g_error_free (err);
        exit (EXIT_FAILURE);
    }
    exit_if_already_running ();

    if (!foreground) {
        close_log ();
        daemonize ();
        open_log();
    }

#if GLIB_MAJOR_VERSION <= 2 && GLIB_MINOR_VERSION < 35
    /* Deprecated in glib 2.35/2.36. */
    g_type_init ();
#endif

    monitor = monitor_directory (report_dir, check_online_then_upload);
    if (!monitor)
        exit (EXIT_FAILURE);

    if (!assume_online)
        monitor_connectivity (crash_db_url, network_changed);

    process_existing_files (report_dir);
    g_timeout_add_seconds (PROCESS_OUTSTANDING_TIMEOUT,
                           (GSourceFunc) process_existing_files, (gpointer) report_dir);

    loop = g_main_loop_new (NULL, FALSE);
    g_main_loop_run (loop);

    unmonitor_directory (monitor, check_online_then_upload);
    if (!assume_online)
        unmonitor_connectivity ();

    close_log ();

    g_unlink (lock_path);
    close (lock_fd);
    curl_global_cleanup ();

    g_free (crash_db_url);
    g_free (crash_db_submit_url);
    return 0;
}
#endif
