Blob Blame History Raw
/*
  Copyright (C) 2010  ABRT team

  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; either version 2 of the License, or
  (at your option) any later version.

  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, write to the Free Software Foundation, Inc.,
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "problem_api.h"
#include "abrt_glib.h"
#include "libabrt.h"

/* Maximal length of backtrace. */
#define MAX_BACKTRACE_SIZE (1024*1024)
/* Amount of data received from one client for a message before reporting error. */
#define MAX_MESSAGE_SIZE (4*MAX_BACKTRACE_SIZE)
/* Maximal number of characters read from socket at once. */
#define INPUT_BUFFER_SIZE (8*1024)
/* We exit after this many seconds */
#define TIMEOUT 10

#define ABRT_SERVER_EVENT_ENV "ABRT_SERVER_PID"

/*
Unix socket in ABRT daemon for creating new dump directories.

Why to use socket for creating dump dirs? Security. When a Python
script throws unexpected exception, ABRT handler catches it, running
as a part of that broken Python application. The application is running
with certain SELinux privileges, for example it can not execute other
programs, or to create files in /var/cache or anything else required
to properly fill a problem directory. Adding these privileges to every
application would weaken the security.
The most suitable solution is for the Python application
to open a socket where ABRT daemon is listening, write all relevant
data to that socket, and close it. ABRT daemon handles the rest.

** Protocol

Initializing new dump:
open /var/run/abrt.socket

Providing dump data (hook writes to the socket):
MANDATORY ITEMS:
-> "PID="
   number 0 - PID_MAX (/proc/sys/kernel/pid_max)
   \0
-> "EXECUTABLE="
   string
   \0
-> "BACKTRACE="
   string
   \0
-> "ANALYZER="
   string
   \0
-> "BASENAME="
   string (no slashes)
   \0
-> "REASON="
   string
   \0

You can send more messages using the same KEY=value format.
*/

static int g_signal_pipe[2];
static struct ns_ids g_ns_ids;

struct waiting_context
{
    GMainLoop *main_loop;
    const char *dirname;
    int retcode;
    enum abrt_daemon_reply
    {
        ABRT_CONTINUE,
        ABRT_INTERRUPT,
    } reply;
};

static unsigned total_bytes_read = 0;

static pid_t client_pid = (pid_t)-1L;
static uid_t client_uid = (uid_t)-1L;

static void
handle_signal(int signo)
{
    int save_errno = errno;
    uint8_t sig_caught = signo;
    if (write(g_signal_pipe[1], &sig_caught, 1))
        /* we ignore result, if () shuts up stupid compiler */;
    errno = save_errno;
}

static gboolean
handle_signal_pipe_cb(GIOChannel *gio, GIOCondition condition, gpointer user_data)
{
    gsize len = 0;
    uint8_t signals[2];

    for (;;)
    {
        GError *error = NULL;
        GIOStatus stat = g_io_channel_read_chars(gio, (void *)signals, sizeof(signals), &len, NULL);
        if (stat == G_IO_STATUS_ERROR)
            error_msg_and_die(_("Can't read from gio channel: '%s'"), error ? error->message : "");
        if (stat == G_IO_STATUS_EOF)
            return FALSE; /* Remove this GLib source */
        if (stat == G_IO_STATUS_AGAIN)
            break;

        /* G_IO_STATUS_NORMAL */
        for (unsigned signo = 0; signo < len; ++signo)
        {
            /* we did receive a signal */
            struct waiting_context *context = (struct waiting_context *)user_data;
            log_debug("Got signal %d through signal pipe", signals[signo]);
            switch (signals[signo])
            {
                case SIGUSR1: context->reply = ABRT_CONTINUE; break;
                case SIGINT:  context->reply = ABRT_INTERRUPT; break;
                default:
                {
                    error_msg("Bug - aborting - unsupported signal: %d", signals[signo]);
                    abort();
                }
            }

            g_main_loop_quit(context->main_loop);
            return FALSE; /* remove this event */
        }
    }

    return TRUE; /* "please don't remove this event" */
}

/* Remove dump dir */
static int delete_path(const char *dump_dir_name)
{
    /* If doesn't start with "g_settings_dump_location/"... */
    if (!dir_is_in_dump_location(dump_dir_name))
    {
        /* Then refuse to operate on it (someone is attacking us??) */
        error_msg("Bad problem directory name '%s', should start with: '%s'", dump_dir_name, g_settings_dump_location);
        return 400; /* Bad Request */
    }
    if (!dir_has_correct_permissions(dump_dir_name, DD_PERM_DAEMONS))
    {
        error_msg("Problem directory '%s' has wrong owner or group", dump_dir_name);
        return 400; /*  */
    }

    struct dump_dir *dd = dd_opendir(dump_dir_name, DD_OPEN_FD_ONLY);
    if (dd == NULL)
    {
        perror_msg("Can't open problem directory '%s'", dump_dir_name);
        return 400;
    }
    if (!dd_accessible_by_uid(dd, client_uid))
    {
        dd_close(dd);
        if (errno == ENOTDIR)
        {
            error_msg("Path '%s' isn't problem directory", dump_dir_name);
            return 404; /* Not Found */
        }
        error_msg("Problem directory '%s' can't be accessed by user with uid %ld", dump_dir_name, (long)client_uid);
        return 403; /* Forbidden */
    }

    dd = dd_fdopendir(dd, /*flags:*/ 0);
    if (dd)
    {
        if (dd_delete(dd) != 0)
        {
            error_msg("Failed to delete problem directory '%s'", dump_dir_name);
            dd_close(dd);
            return 400;
        }
    }

    return 0; /* success */
}

static pid_t spawn_event_handler_child(const char *dump_dir_name, const char *event_name, int *fdp)
{
    char *args[9];
    args[0] = (char *) LIBEXEC_DIR"/abrt-handle-event";
    /* Do not forward ASK_* messages to parent*/
    args[1] = (char *) "-i";
    args[2] = (char *) "--nice";
    args[3] = (char *) "10";
    args[4] = (char *) "-e";
    args[5] = (char *) event_name;
    args[6] = (char *) "--";
    args[7] = (char *) dump_dir_name;
    args[8] = NULL;

    int pipeout[2];
    int flags = EXECFLG_INPUT_NUL | EXECFLG_OUTPUT | EXECFLG_QUIET | EXECFLG_ERR2OUT;
    VERB1 flags &= ~EXECFLG_QUIET;

    char *env_vec[3];
    /* Intercept ASK_* messages in Client API -> don't wait for user response */
    env_vec[0] = xstrdup("REPORT_CLIENT_NONINTERACTIVE=1");
    env_vec[1] = xasprintf("%s=%d", ABRT_SERVER_EVENT_ENV, getpid());
    env_vec[2] = NULL;

    pid_t child = fork_execv_on_steroids(flags, args, pipeout,
                                         env_vec, /*dir:*/ NULL,
                                         /*uid(unused):*/ 0);
    if (fdp)
        *fdp = pipeout[0];
    return child;
}

static int problem_dump_dir_was_provoked_by_abrt_event(struct dump_dir *dd, char  **provoker)
{
    char *env_var = NULL;
    const int r = dd_get_env_variable(dd, ABRT_SERVER_EVENT_ENV, &env_var);

    /* Dump directory doesn't contain the environ file */
    if (r == -ENOENT)
        return 0;

    if (provoker != NULL)
        *provoker = env_var;
    else
        free(env_var);

    return env_var != NULL;
}

static int
emit_new_problem_signal(gpointer data)
{
    struct waiting_context *context = (struct waiting_context *)data;

    const size_t wrote = fprintf(stderr, "NEW_PROBLEM_DETECTED: %s\n", context->dirname);
    fflush(stderr);

    if (wrote <= 0)
    {
        error_msg("Failed to communicate with the daemon");
        context->retcode = 503;
        g_main_loop_quit(context->main_loop);
    }

    log_notice("Emitted new problem signal, waiting for SIGUSR1|SIGINT");
    return FALSE;
}

struct response
{
    int code;
    char *message;
};

#define RESPONSE_SETTER(r, c, m) \
    do { if (r != NULL) { r->message = m; r->code = c; } else { free(m); }} while (0)

#define RESPONSE_RETURN(r, c ,m) \
    do { RESPONSE_SETTER(r, c, m); \
         return c; } while (0)


static int run_post_create(const char *dirname, struct response *resp)
{
    /* If doesn't start with "g_settings_dump_location/"... */
    if (!dir_is_in_dump_location(dirname))
    {
        /* Then refuse to operate on it (someone is attacking us??) */
        error_msg("Bad problem directory name '%s', should start with: '%s'", dirname, g_settings_dump_location);
        RESPONSE_RETURN(resp, 400, NULL);
    }
    if (!dir_has_correct_permissions(dirname, DD_PERM_EVENTS))
    {
        error_msg("Problem directory '%s' has wrong owner or group", dirname);
        RESPONSE_RETURN(resp, 400, NULL);
    }
    /* Check completness */
    {
        struct dump_dir *dd = dd_opendir(dirname, DD_OPEN_READONLY);

        char *provoker = NULL;
        const bool event_dir = dd && problem_dump_dir_was_provoked_by_abrt_event(dd, &provoker);
        if (event_dir)
        {
            if (g_settings_debug_level == 0)
            {
                error_msg("Removing problem provoked by ABRT(pid:%s): '%s'", provoker, dirname);
                dd_delete(dd);
            }
            else
            {
                char *dumpdir = NULL;
                char *event   = NULL;
                char *reason  = NULL;
                char *cmdline = NULL;

                /* Ignore errors */
                dd_get_env_variable(dd, "DUMP_DIR", &dumpdir);
                dd_get_env_variable(dd, "EVENT",    &event);
                reason  = dd_load_text(dd, FILENAME_REASON);
                cmdline = dd_load_text(dd, FILENAME_CMDLINE);

                error_msg("ABRT_SERVER_PID=%s;DUMP_DIR='%s';EVENT='%s';REASON='%s';CMDLINE='%s'",
                           provoker, dumpdir, event, reason, cmdline);

            }

            free(provoker);
            return 400;
        }

        const bool complete = dd && problem_dump_dir_is_complete(dd);
        dd_close(dd);
        if (complete)
        {
            error_msg("Problem directory '%s' has already been processed", dirname);
            RESPONSE_RETURN(resp, 403, NULL);
        }
    }

    /*
     * The post-create event cannot be run concurrently for more problem
     * directories. The problem is in searching for duplicates process
     * in case when two concurrently processed directories are duplicates
     * of each other. Both of the directories are marked as duplicates
     * of each other and are deleted.
     */
    log_debug("Creating glib main loop");
    struct waiting_context context = {0};
    context.main_loop = g_main_loop_new(NULL, FALSE);
    context.dirname = strrchr(dirname, '/') + 1;

    log_debug("Setting up a signal handler");
    /* Set up signal pipe */
    xpipe(g_signal_pipe);
    close_on_exec_on(g_signal_pipe[0]);
    close_on_exec_on(g_signal_pipe[1]);
    ndelay_on(g_signal_pipe[0]);
    ndelay_on(g_signal_pipe[1]);
    signal(SIGUSR1, handle_signal);
    signal(SIGINT, handle_signal);
    GIOChannel *channel_signal = abrt_gio_channel_unix_new(g_signal_pipe[0]);
    g_io_add_watch(channel_signal, G_IO_IN | G_IO_PRI, handle_signal_pipe_cb, &context);

    g_idle_add(emit_new_problem_signal, &context);

    g_main_loop_run(context.main_loop);

    g_main_loop_unref(context.main_loop);
    g_io_channel_unref(channel_signal);
    close(g_signal_pipe[1]);

    log_notice("Waiting finished");

    if (context.retcode != 0)
        RESPONSE_RETURN(resp, context.retcode, NULL);

    if (context.reply != ABRT_CONTINUE)
        /* The only reason for the interruption is removed problem directory */
        RESPONSE_RETURN(resp, 413, NULL);
    /*
     * The post-create event synchronization done.
     */

    int child_stdout_fd;
    int child_pid = spawn_event_handler_child(dirname, "post-create", &child_stdout_fd);

    char *dup_of_dir = NULL;
    struct strbuf *cmd_output = strbuf_new();

    bool child_is_post_create = 1; /* else it is a notify child */

 read_child_output:
    //log_warning("Reading from event fd %d", child_stdout_fd);

    /* Read streamed data and split lines */
    for (;;)
    {
        char buf[250]; /* usually we get one line, no need to have big buf */
        errno = 0;
        int r = safe_read(child_stdout_fd, buf, sizeof(buf) - 1);
        if (r <= 0)
            break;
        buf[r] = '\0';

        /* split lines in the current buffer */
        char *raw = buf;
        char *newline;
        while ((newline = strchr(raw, '\n')) != NULL)
        {
            *newline = '\0';
            strbuf_append_str(cmd_output, raw);
            char *msg = cmd_output->buf;

            if (child_is_post_create
             && prefixcmp(msg, "DUP_OF_DIR: ") == 0
            ) {
                free(dup_of_dir);
                dup_of_dir = xstrdup(msg + strlen("DUP_OF_DIR: "));
            }
            else
                log_warning("%s", msg);

            strbuf_clear(cmd_output);
            /* jump to next line */
            raw = newline + 1;
        }

        /* beginning of next line. the line continues by next read */
        strbuf_append_str(cmd_output, raw);
    }

    /* EOF/error */

    /* Wait for child to actually exit, collect status */
    int status = 0;
    if (safe_waitpid(child_pid, &status, 0) <= 0)
    /* should not happen */
        perror_msg("waitpid(%d)", child_pid);

    /* If it was a "notify[-dup]" event, then we're done */
    if (!child_is_post_create)
        goto ret;

    /* exit 0 means "this is a good, non-dup dir" */
    /* exit with 1 + "DUP_OF_DIR: dir" string => dup */
    if (status != 0)
    {
        if (WIFSIGNALED(status))
        {
            log_warning("'post-create' on '%s' killed by signal %d",
                            dirname, WTERMSIG(status));
            goto delete_bad_dir;
        }
        /* else: it is WIFEXITED(status) */
        if (!dup_of_dir)
        {
            log_warning("'post-create' on '%s' exited with %d",
                            dirname, WEXITSTATUS(status));
            goto delete_bad_dir;
        }
    }

    const char *work_dir = (dup_of_dir ? dup_of_dir : dirname);

    /* Load problem_data (from the *first dir* if this one is a dup) */
    struct dump_dir *dd = dd_opendir(work_dir, /*flags:*/ 0);
    if (!dd)
        /* dd_opendir already emitted error msg */
        goto delete_bad_dir;

    /* Update count */
    char *count_str = dd_load_text_ext(dd, FILENAME_COUNT, DD_FAIL_QUIETLY_ENOENT);
    unsigned long count = strtoul(count_str, NULL, 10);

    /* Don't increase crash count if we are working with newly uploaded
     * directory (remote crash) which already has its crash count set.
     */
    if ((status != 0 && dup_of_dir) || count == 0)
    {
        count++;
        char new_count_str[sizeof(long)*3 + 2];
        sprintf(new_count_str, "%lu", count);
        dd_save_text(dd, FILENAME_COUNT, new_count_str);

        /* This condition can be simplified to either
         * (status * != 0 && * dup_of_dir) or (count == 1). But the
         * chosen form is much more reliable and safe. We must not call
         * dd_opendir() to locked dd otherwise we go into a deadlock.
         */
        if (strcmp(dd->dd_dirname, dirname) != 0)
        {
            /* Update the last occurrence file by the time file of the new problem */
            struct dump_dir *new_dd = dd_opendir(dirname, DD_OPEN_READONLY);
            char *last_ocr = NULL;
            if (new_dd)
            {
                /* TIME must exists in a valid dump directory but we don't want to die
                 * due to broken duplicated dump directory */
                last_ocr = dd_load_text_ext(new_dd, FILENAME_TIME,
                            DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE | DD_FAIL_QUIETLY_ENOENT);
                dd_close(new_dd);
            }
            else
            {   /* dd_opendir() already produced a message with good information about failure */
                error_msg("Can't read the last occurrence file from the new dump directory.");
            }

            if (!last_ocr)
            {   /* the new dump directory may lie in the dump location for some time */
                log_warning("Using current time for the last occurrence file which may be incorrect.");
                time_t t = time(NULL);
                last_ocr = xasprintf("%lu", (long)t);
            }

            dd_save_text(dd, FILENAME_LAST_OCCURRENCE, last_ocr);

            free(last_ocr);
        }
    }

    /* Reset mode/uig/gid to correct values for all files created by event run */
    dd_sanitize_mode_and_owner(dd);

    dd_close(dd);

    if (!dup_of_dir)
        log_notice("New problem directory %s, processing", work_dir);
    else
    {
        log_warning("Deleting problem directory %s (dup of %s)",
                    strrchr(dirname, '/') + 1,
                    strrchr(dup_of_dir, '/') + 1);
        delete_dump_dir(dirname);
    }

    /* Run "notify[-dup]" event */
    int fd;
    child_pid = spawn_event_handler_child(
                work_dir,
                (dup_of_dir ? "notify-dup" : "notify"),
                &fd
    );
    //log_warning("Started notify, fd %d -> %d", fd, child_stdout_fd);
    xmove_fd(fd, child_stdout_fd);
    child_is_post_create = 0;
    if (dup_of_dir)
        RESPONSE_SETTER(resp, 303, dup_of_dir);
    else
    {
        RESPONSE_SETTER(resp, 200, NULL);
        free(dup_of_dir);
    }
    dup_of_dir = NULL;
    strbuf_clear(cmd_output);
    goto read_child_output;

 delete_bad_dir:
    log_warning("Deleting problem directory '%s'", dirname);
    delete_dump_dir(dirname);
    /* TODO - better code to allow detection on client's side */
    RESPONSE_SETTER(resp, 403, NULL);

 ret:
    strbuf_free(cmd_output);
    free(dup_of_dir);
    close(child_stdout_fd);
    return 0;
}

/* Create a new problem directory from client session.
 * Caller must ensure that all fields in struct client
 * are properly filled.
 */
static int create_problem_dir(GHashTable *problem_info, unsigned pid)
{
    /* Exit if free space is less than 1/4 of MaxCrashReportsSize */
    if (g_settings_nMaxCrashReportsSize > 0)
    {
        if (low_free_space(g_settings_nMaxCrashReportsSize, g_settings_dump_location))
            exit(1);
    }

    /* Create temp directory with the problem data.
     * This directory is renamed to final directory name after
     * all files have been stored into it.
     */

    gchar *dir_basename = g_hash_table_lookup(problem_info, "basename");
    if (!dir_basename)
        dir_basename = g_hash_table_lookup(problem_info, FILENAME_TYPE);

    char *path = xasprintf("%s/%s-%s-%u.new",
                           g_settings_dump_location,
                           dir_basename,
                           iso_date_string(NULL),
                           pid);

    /* This item is useless, don't save it */
    g_hash_table_remove(problem_info, "basename");

    /* No need to check the path length, as all variables used are limited,
     * and dd_create() fails if the path is too long.
     */
    struct dump_dir *dd = dd_create(path, /*fs owner*/0, DEFAULT_DUMP_DIR_MODE);
    if (!dd)
    {
        error_msg_and_die("Error creating problem directory '%s'", path);
    }

    const int proc_dir_fd = open_proc_pid_dir(pid);
    char *rootdir = NULL;

    if (proc_dir_fd < 0)
    {
        pwarn_msg("Cannot open /proc/%d:", pid);
    }
    else if (process_has_own_root_at(proc_dir_fd))
    {
        /* Obtain the root directory path only if process' root directory is
         * not the same as the init's root directory
         */
        rootdir = get_rootdir_at(proc_dir_fd);
    }

    /* Reading data from an arbitrary root directory is not secure. */
    if (proc_dir_fd >= 0 && g_settings_explorechroots)
    {
        char proc_pid_root[sizeof("/proc/[pid]/root") + sizeof(pid_t) * 3];
        const size_t w = snprintf(proc_pid_root, sizeof(proc_pid_root), "/proc/%d/root", pid);
        assert(sizeof(proc_pid_root) > w);

        /* Yes, test 'rootdir' but use 'source_filename' because 'rootdir' can
         * be '/' for a process with own namespace. 'source_filename' is /proc/[pid]/root. */
        dd_create_basic_files(dd, client_uid, (rootdir != NULL) ? proc_pid_root : NULL);
    }
    else
    {
        dd_create_basic_files(dd, client_uid, NULL);
    }

    if (proc_dir_fd >= 0)
    {
        /* Obtain and save the command line. */
        char *cmdline = get_cmdline_at(proc_dir_fd);
        if (cmdline)
        {
            dd_save_text(dd, FILENAME_CMDLINE, cmdline);
            free(cmdline);
        }

        /* Obtain and save the environment variables. */
        char *environ = get_environ_at(proc_dir_fd);
        if (environ)
        {
            dd_save_text(dd, FILENAME_ENVIRON, environ);
            free(environ);
        }

        dd_copy_file_at(dd, FILENAME_CGROUP,    proc_dir_fd, "cgroup");
        dd_copy_file_at(dd, FILENAME_MOUNTINFO, proc_dir_fd, "mountinfo");

        FILE *open_fds = dd_open_item_file(dd, FILENAME_OPEN_FDS, O_RDWR);
        if (open_fds)
        {
            if (dump_fd_info_at(proc_dir_fd, open_fds) < 0)
                dd_delete_item(dd, FILENAME_OPEN_FDS);
            fclose(open_fds);
        }

        const int init_proc_dir_fd = open_proc_pid_dir(1);
        FILE *namespaces = dd_open_item_file(dd, FILENAME_NAMESPACES, O_RDWR);
        if (namespaces && init_proc_dir_fd >= 0)
        {
            if (dump_namespace_diff_at(init_proc_dir_fd, proc_dir_fd, namespaces) < 0)
                dd_delete_item(dd, FILENAME_NAMESPACES);
        }
        if (init_proc_dir_fd >= 0)
            close(init_proc_dir_fd);
        if (namespaces)
            fclose(namespaces);

        /* The process's root directory isn't the same as the init's root
         * directory. */
        if (rootdir)
        {
            if (strcmp(rootdir, "/") ==  0)
            {   /* We are dealing containerized process because root's
                 * directory path is '/' and that means that the process
                 * has mounted its own root.
                 * Seriously, it is possible if the process is running in its
                 * own MOUNT namespaces.
                 */
                log_debug("Process %d is considered to be containerized", pid);
                pid_t container_pid;
                if (get_pid_of_container_at(proc_dir_fd, &container_pid) == 0)
                {
                    char *container_cmdline = get_cmdline(container_pid);
                    dd_save_text(dd, FILENAME_CONTAINER_CMDLINE, container_cmdline);
                    free(container_cmdline);
                }
            }
            else
            {   /* We are dealing chrooted process. */
                dd_save_text(dd, FILENAME_ROOTDIR, rootdir);
            }
        }
        close(proc_dir_fd);
    }
    free(rootdir);

    /* Store id of the user whose application crashed. */
    char uid_str[sizeof(long) * 3 + 2];
    sprintf(uid_str, "%lu", (long)client_uid);
    dd_save_text(dd, FILENAME_UID, uid_str);

    GHashTableIter iter;
    gpointer gpkey;
    gpointer gpvalue;
    g_hash_table_iter_init(&iter, problem_info);
    while (g_hash_table_iter_next(&iter, &gpkey, &gpvalue))
    {
        dd_save_text(dd, (gchar *) gpkey, (gchar *) gpvalue);
    }

    dd_save_text(dd, FILENAME_ABRT_VERSION, VERSION);

    dd_close(dd);

    /* Not needing it anymore */
    g_hash_table_destroy(problem_info);

    /* Move the completely created problem directory
     * to final directory.
     */
    char *newpath = xstrndup(path, strlen(path) - strlen(".new"));
    if (rename(path, newpath) == 0)
        strcpy(path, newpath);
    free(newpath);

    log_notice("Saved problem directory of pid %u to '%s'", pid, path);

    /* We let the peer know that problem dir was created successfully
     * _before_ we run potentially long-running post-create.
     */
    printf("HTTP/1.1 201 Created\r\n\r\n");
    fflush(NULL);

    /* Closing STDIN_FILENO (abrtd duped the socket to stdin and stdout) and
     * not-replacing it with something else to let abrt-server die on reading
     * from invalid stdin - to catch bugs. */
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    xdup2(STDERR_FILENO, STDOUT_FILENO); /* paranoia: don't leave stdout fd closed */

    /* Trim old problem directories if necessary */
    if (g_settings_nMaxCrashReportsSize > 0)
    {
        trim_problem_dirs(g_settings_dump_location, g_settings_nMaxCrashReportsSize * (double)(1024*1024), path);
    }

    run_post_create(path, NULL);

    /* free(path); */
    exit(0);
}

static gboolean key_value_ok(gchar *key, gchar *value)
{
    char *i;

    /* check key, it has to be valid filename and will end up in the
     * bugzilla */
    for (i = key; *i != 0; i++)
    {
        if (!isalpha(*i) && (*i != '-') && (*i != '_') && (*i != ' '))
            return FALSE;
    }

    /* check value of 'basename', it has to be valid non-hidden directory
     * name */
    if (strcmp(key, "basename") == 0
     || strcmp(key, FILENAME_TYPE) == 0
    )
    {
        if (!str_is_correct_filename(value))
        {
            error_msg("Value of '%s' ('%s') is not a valid directory name",
                      key, value);
            return FALSE;
        }
    }

    return allowed_new_user_problem_entry(client_uid, key, value);
}

/* Handles a message received from client over socket. */
static void process_message(GHashTable *problem_info, char *message)
{
    gchar *key, *value;

    value = strchr(message, '=');
    if (value)
    {
        key = g_ascii_strdown(message, value - message); /* result is malloced */
//TODO: is it ok? it uses g_malloc, not malloc!

        value++;
        if (key_value_ok(key, value))
        {
            if (strcmp(key, FILENAME_UID) == 0)
            {
                error_msg("Ignoring value of %s, will be determined later",
                          FILENAME_UID);
            }
            else
            {
                g_hash_table_insert(problem_info, key, xstrdup(value));
                /* Prevent freeing key later: */
                key = NULL;
            }
        }
        else
        {
            /* should use error_msg_and_die() here? */
            error_msg("Invalid key or value format: %s", message);
        }
        free(key);
    }
    else
    {
        /* should use error_msg_and_die() here? */
        error_msg("Invalid message format: '%s'", message);
    }
}

static void die_if_data_is_missing(GHashTable *problem_info)
{
    gboolean missing_data = FALSE;
    gchar **pstring;
    static const gchar *const needed[] = {
        FILENAME_TYPE,
        FILENAME_REASON,
        /* FILENAME_BACKTRACE, - ECC errors have no such elements */
        /* FILENAME_EXECUTABLE, */
        NULL
    };

    for (pstring = (gchar**) needed; *pstring; pstring++)
    {
        if (!g_hash_table_lookup(problem_info, *pstring))
        {
            error_msg("Element '%s' is missing", *pstring);
            missing_data = TRUE;
        }
    }

    if (missing_data)
        error_msg_and_die("Some data is missing, aborting");
}

/*
 * Takes hash table, looks for key FILENAME_PID and tries to convert its value
 * to int.
 */
unsigned convert_pid(GHashTable *problem_info)
{
    long ret;
    gchar *pid_str = (gchar *) g_hash_table_lookup(problem_info, FILENAME_PID);
    char *err_pos;

    if (!pid_str)
        error_msg_and_die("PID data is missing, aborting");

    errno = 0;
    ret = strtol(pid_str, &err_pos, 10);
    if (errno || pid_str == err_pos || *err_pos != '\0'
        || ret > UINT_MAX || ret < 1)
        error_msg_and_die("Malformed or out-of-range PID number: '%s'", pid_str);

    return (unsigned) ret;
}

static int perform_http_xact(struct response *rsp)
{
    /* use free instead of g_free so that we can use xstr* functions from
     * libreport/lib/xfuncs.c
     */
    GHashTable *problem_info = g_hash_table_new_full(g_str_hash, g_str_equal,
                                     free, free);
    /* Read header */
    char *body_start = NULL;
    char *messagebuf_data = NULL;
    unsigned messagebuf_len = 0;
    /* Loop until EOF/error/timeout/end_of_header */
    while (1)
    {
        messagebuf_data = xrealloc(messagebuf_data, messagebuf_len + INPUT_BUFFER_SIZE);
        char *p = messagebuf_data + messagebuf_len;
        int rd = read(STDIN_FILENO, p, INPUT_BUFFER_SIZE);
        if (rd < 0)
        {
            if (errno == EINTR) /* SIGALRM? */
                error_msg_and_die("Timed out");
            perror_msg_and_die("read");
        }
        if (rd == 0)
            break;

        log_debug("Received %u bytes of data", rd);
        messagebuf_len += rd;
        total_bytes_read += rd;
        if (total_bytes_read > MAX_MESSAGE_SIZE)
            error_msg_and_die("Message is too long, aborting");

        /* Check whether we see end of header */
        /* Note: we support both [\r]\n\r\n and \n\n */
        char *past_end = messagebuf_data + messagebuf_len;
        if (p > messagebuf_data+1)
            p -= 2; /* start search from two last bytes in last read - they might be '\n\r' */
        while (p < past_end)
        {
            p = memchr(p, '\n', past_end - p);
            if (!p)
                break;
            p++;
            if (p >= past_end)
                break;
            if (*p == '\n'
             || (*p == '\r' && p+1 < past_end && p[1] == '\n')
            ) {
                body_start = p + 1 + (*p == '\r');
                *p = '\0';
                goto found_end_of_header;
            }
        }
    } /* while (read) */
 found_end_of_header: ;
    log_debug("Request: %s", messagebuf_data);

    /* Sanitize and analyze header.
     * Header now is in messagebuf_data, NUL terminated string,
     * with last empty line deleted (by placement of NUL).
     * \r\n are not (yet) converted to \n, multi-line headers also
     * not converted.
     */
    /* First line must be "op<space>[http://host]/path<space>HTTP/n.n".
     * <space> is exactly one space char.
     */
    if (prefixcmp(messagebuf_data, "DELETE ") == 0)
    {
        messagebuf_data += strlen("DELETE ");
        char *space = strchr(messagebuf_data, ' ');
        if (!space || prefixcmp(space+1, "HTTP/") != 0)
            return 400; /* Bad Request */
        *space = '\0';
        //decode_url(messagebuf_data); %20 => ' '
        alarm(0);
        return delete_path(messagebuf_data);
    }

    /* We erroneously used "PUT /" to create new problems.
     * POST is the correct request in this case:
     * "PUT /" implies creation or replace of resource named "/"!
     * Delete PUT in 2014.
     */
    if (prefixcmp(messagebuf_data, "PUT ") != 0
     && prefixcmp(messagebuf_data, "POST ") != 0
    ) {
        return 400; /* Bad Request */
    }

    enum {
        CREATION_NOTIFICATION,
        CREATION_REQUEST,
    };
    int url_type;
    char *url = skip_non_whitespace(messagebuf_data) + 1; /* skip "POST " */
    if (prefixcmp(url, "/creation_notification ") == 0)
        url_type = CREATION_NOTIFICATION;
    else if (prefixcmp(url, "/ ") == 0)
        url_type = CREATION_REQUEST;
    else
        return 400; /* Bad Request */

    /* Read body */
    if (!body_start)
    {
        log_warning("Premature EOF detected, exiting");
        return 400; /* Bad Request */
    }

    messagebuf_len -= (body_start - messagebuf_data);
    memmove(messagebuf_data, body_start, messagebuf_len);
    log_debug("Body so far: %u bytes, '%s'", messagebuf_len, messagebuf_data);

    /* Loop until EOF/error/timeout */
    while (1)
    {
        if (url_type == CREATION_REQUEST)
        {
            while (1)
            {
                unsigned len = strnlen(messagebuf_data, messagebuf_len);
                if (len >= messagebuf_len)
                    break;
                /* messagebuf has at least one NUL - process the line */
                process_message(problem_info, messagebuf_data);
                messagebuf_len -= (len + 1);
                memmove(messagebuf_data, messagebuf_data + len + 1, messagebuf_len);
            }
        }

        messagebuf_data = xrealloc(messagebuf_data, messagebuf_len + INPUT_BUFFER_SIZE + 1);
        int rd = read(STDIN_FILENO, messagebuf_data + messagebuf_len, INPUT_BUFFER_SIZE);
        if (rd < 0)
        {
            if (errno == EINTR) /* SIGALRM? */
                error_msg_and_die("Timed out");
            perror_msg_and_die("read");
        }
        if (rd == 0)
            break;

        log_debug("Received %u bytes of data", rd);
        messagebuf_len += rd;
        total_bytes_read += rd;
        if (total_bytes_read > MAX_MESSAGE_SIZE)
            error_msg_and_die("Message is too long, aborting");
    }

    /* Body received, EOF was seen. Don't let alarm to interrupt after this. */
    alarm(0);

    int ret = 0;
    if (url_type == CREATION_NOTIFICATION)
    {
        if (client_uid != 0)
        {
            error_msg("UID=%ld is not authorized to trigger post-create processing", (long)client_uid);
            ret = 403; /* Forbidden */
            goto out;
        }

        messagebuf_data[messagebuf_len] = '\0';
        return run_post_create(messagebuf_data, rsp);
    }

    die_if_data_is_missing(problem_info);

    /* Save problem dir */
    char *executable = g_hash_table_lookup(problem_info, FILENAME_EXECUTABLE);
    if (executable)
    {
        char *last_file = concat_path_file(g_settings_dump_location, "last-via-server");
        int repeating_crash = check_recent_crash_file(last_file, executable);
        free(last_file);
        if (repeating_crash) /* Only pretend that we saved it */
        {
            error_msg("Not saving repeating crash in '%s'", executable);
            goto out; /* ret is 0: "success" */
        }
    }

#if 0
//TODO:
    /* At least it should generate local problem identifier UUID */
    problem_data_add_basics(problem_info);
//...the problem being that problem_info here is not a problem_data_t!
#endif
    unsigned pid = convert_pid(problem_info);
    struct ns_ids client_ids;
    if (get_ns_ids(client_pid, &client_ids) < 0)
        error_msg_and_die("Cannot get peer's Namespaces from /proc/%d/ns", client_pid);

    if (client_ids.nsi_ids[PROC_NS_ID_PID] != g_ns_ids.nsi_ids[PROC_NS_ID_PID])
    {
        log_notice("Client is running in own PID Namespace, using PID %d instead of %d", client_pid, pid);
        pid = client_pid;
    }

    create_problem_dir(problem_info, pid);
    /* does not return */

 out:
    g_hash_table_destroy(problem_info);
    return ret; /* Used as HTTP response code */
}

static void dummy_handler(int sig_unused) {}

int main(int argc, char **argv)
{
    /* I18n */
    setlocale(LC_ALL, "");
#if ENABLE_NLS
    bindtextdomain(PACKAGE, LOCALEDIR);
    textdomain(PACKAGE);
#endif

    abrt_init(argv);

    /* Can't keep these strings/structs static: _() doesn't support that */
    const char *program_usage_string = _(
        "& [options]"
    );
    enum {
        OPT_v = 1 << 0,
        OPT_u = 1 << 1,
        OPT_s = 1 << 2,
        OPT_p = 1 << 3,
    };
    /* Keep enum above and order of options below in sync! */
    struct options program_options[] = {
        OPT__VERBOSE(&g_verbose),
        OPT_INTEGER('u', NULL, &client_uid, _("Use NUM as client uid")),
        OPT_BOOL(   's', NULL, NULL       , _("Log to syslog")),
        OPT_BOOL(   'p', NULL, NULL       , _("Add program names to log")),
        OPT_END()
    };
    unsigned opts = parse_opts(argc, argv, program_options, program_usage_string);

    export_abrt_envvars(opts & OPT_p);

    msg_prefix = xasprintf("%s[%u]", g_progname, getpid());
    if (opts & OPT_s)
    {
        logmode = LOGMODE_JOURNAL;
    }

    /* Set up timeout handling */
    /* Part 1 - need this to make SIGALRM interrupt syscalls
     * (as opposed to restarting them): I want read syscall to be interrupted
     */
    struct sigaction sa;
    /* sa.sa_flags.SA_RESTART bit is clear: make signal interrupt syscalls */
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = dummy_handler; /* pity, SIG_DFL won't do */
    sigaction(SIGALRM, &sa, NULL);
    /* Part 2 - set the timeout per se */
    alarm(TIMEOUT);

    /* Get uid of the connected client */
    struct ucred cr;
    socklen_t crlen = sizeof(cr);
    if (0 != getsockopt(STDIN_FILENO, SOL_SOCKET, SO_PEERCRED, &cr, &crlen))
        perror_msg_and_die("getsockopt(SO_PEERCRED)");
    if (crlen != sizeof(cr))
        error_msg_and_die("%s: bad crlen %d", "getsockopt(SO_PEERCRED)", (int)crlen);

    if (client_uid == (uid_t)-1L)
        client_uid = cr.uid;

    client_pid = cr.pid;

    pid_t pid = getpid();
    if (get_ns_ids(getpid(), &g_ns_ids) < 0)
        error_msg_and_die("Cannot get own Namespaces from /proc/%d/ns", pid);

    load_abrt_conf();

    struct response rsp = { 0 };
    int r = perform_http_xact(&rsp);
    if (r == 0)
        r = 200;

    if (rsp.code == 0)
        rsp.code = r;

    free_abrt_conf_data();

    printf("HTTP/1.1 %u \r\n\r\n", rsp.code);
    if (rsp.message != NULL)
    {
        printf("%s", rsp.message);
        fflush(stdout);
        free(rsp.message);
    }

    return (r >= 400); /* Error if 400+ */
}