Blob Blame History Raw
/*
    Copyright (C) 2012,2014  ABRT team
    Copyright (C) 2012,2014  RedHat Inc

    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 <json.h>

#include <satyr/abrt.h>
#include <satyr/report.h>

#include "internal_libreport.h"
#include "client.h"
#include "ureport.h"
#include "libreport_curl.h"

#define DESTROYED_POINTER (void *)0xdeadbeef

#define BTHASH_URL_SFX "reports/bthash/"

static char *
puppet_config_print(const char *key)
{
    char *command = xasprintf("puppet config print %s", key);
    char *result = run_in_shell_and_save_output(0, command, NULL, NULL);
    free(command);

    /* run_in_shell_and_save_output always returns non-NULL */
    if (result[0] != '/')
        goto error;

    char *newline = strchrnul(result, '\n');
    if (!newline)
        goto error;

    *newline = '\0';
    return result;
error:
    free(result);
    error_msg_and_die("Unable to determine puppet %s path (puppet not installed?)", key);
}

void
ureport_server_config_set_url(struct ureport_server_config *config,
                              char *server_url)
{
    free(config->ur_url);
    config->ur_url = server_url;
}

void
ureport_server_config_set_client_auth(struct ureport_server_config *config,
                                      const char *client_auth)
{
    if (client_auth == NULL)
        return;

    if (strcmp(client_auth, "") == 0)
    {
        free(config->ur_client_cert);
        config->ur_client_cert = NULL;

        free(config->ur_client_key);
        config->ur_client_key = NULL;

        log_notice("Not using client authentication");
    }
    else if (strcmp(client_auth, "puppet") == 0)
    {
        config->ur_client_cert = puppet_config_print("hostcert");
        config->ur_client_key = puppet_config_print("hostprivkey");
    }
    else
    {
        char *scratch = xstrdup(client_auth);
        config->ur_client_cert = xstrdup(strtok(scratch, ":"));
        config->ur_client_key = xstrdup(strtok(NULL, ":"));
        free(scratch);

        if (config->ur_client_cert == NULL || config->ur_client_key == NULL)
            error_msg_and_die("Invalid client authentication specification");
    }

    if (config->ur_client_cert && config->ur_client_key)
    {
        log_notice("Using client certificate: %s", config->ur_client_cert);
        log_notice("Using client private key: %s", config->ur_client_key);

        free(config->ur_username);
        config->ur_username = NULL;

        free(config->ur_password);
        config->ur_password = NULL;
    }
}

void
ureport_server_config_set_basic_auth(struct ureport_server_config *config,
                                     const char *login, const char *password)
{
    ureport_server_config_set_client_auth(config, "");

    free(config->ur_username);
    config->ur_username = xstrdup(login);

    free(config->ur_password);
    config->ur_password = xstrdup(password);
}

void
ureport_server_config_load_basic_auth(struct ureport_server_config *config,
                                      const char *http_auth_pref)
{
    if (http_auth_pref == NULL)
        return;

    map_string_t *settings = NULL;

    char *tmp_password = NULL;
    char *tmp_username = NULL;
    const char *username = NULL;
    const char *password = NULL;

    username = tmp_username = xstrdup(http_auth_pref);
    password = strchr(tmp_username, ':');

    if (password != NULL)
        /* It is "char *", see strchr() few lines above. */
        *((char *)(password++)) = '\0';

    if (password == NULL)
    {
        char *message = xasprintf("Please provide uReport server password for user '%s':", username);
        password = tmp_password = ask_password(message);
        free(message);

        if (strcmp(password, "") == 0)
            error_msg_and_die("Cannot continue without uReport server password!");
    }

    ureport_server_config_set_basic_auth(config, username, password);

    free(tmp_password);
    free(tmp_username);
    free_map_string(settings);
}

void
ureport_server_config_load(struct ureport_server_config *config,
                           map_string_t *settings)
{
    UREPORT_OPTION_VALUE_FROM_CONF(settings, "URL", config->ur_url, xstrdup);
    UREPORT_OPTION_VALUE_FROM_CONF(settings, "SSLVerify", config->ur_ssl_verify, string_to_bool);

    const char *http_auth_pref = NULL;
    UREPORT_OPTION_VALUE_FROM_CONF(settings, "HTTPAuth", http_auth_pref, (const char *));
    ureport_server_config_load_basic_auth(config, http_auth_pref);

    const char *client_auth = NULL;
    if (http_auth_pref == NULL)
    {
        UREPORT_OPTION_VALUE_FROM_CONF(settings, "SSLClientAuth", client_auth, (const char *));
        ureport_server_config_set_client_auth(config, client_auth);
    }

    /* If SSLClientAuth is configured, include the auth items by default. */
    bool include_auth = config->ur_client_cert != NULL || config->ur_username != NULL;
    UREPORT_OPTION_VALUE_FROM_CONF(settings, "IncludeAuthData", include_auth, string_to_bool);

    if (include_auth)
    {
        const char *auth_items = NULL;
        UREPORT_OPTION_VALUE_FROM_CONF(settings, "AuthDataItems", auth_items, (const char *));
        config->ur_prefs.urp_auth_items = parse_list(auth_items);

        if (config->ur_prefs.urp_auth_items == NULL)
            log_warning("IncludeAuthData set to 'yes' but AuthDataItems is empty.");
    }
}

void
ureport_server_config_init(struct ureport_server_config *config)
{
    config->ur_url = NULL;
    config->ur_ssl_verify = true;
    config->ur_client_cert = NULL;
    config->ur_client_key = NULL;
    config->ur_cert_authority_cert = NULL;
    config->ur_username = NULL;
    config->ur_password = NULL;
    config->ur_http_headers = new_map_string();

    config->ur_prefs.urp_auth_items = NULL;
    config->ur_prefs.urp_flags = 0;
}

void
ureport_server_config_destroy(struct ureport_server_config *config)
{
    free(config->ur_url);
    config->ur_url = DESTROYED_POINTER;

    free(config->ur_client_cert);
    config->ur_client_cert = DESTROYED_POINTER;

    free(config->ur_client_key);
    config->ur_client_key = DESTROYED_POINTER;

    free(config->ur_cert_authority_cert);
    config->ur_cert_authority_cert = DESTROYED_POINTER;

    free(config->ur_username);
    config->ur_username = DESTROYED_POINTER;

    free(config->ur_password);
    config->ur_password = DESTROYED_POINTER;

    g_list_free_full(config->ur_prefs.urp_auth_items, free);
    config->ur_prefs.urp_auth_items = DESTROYED_POINTER;

    free_map_string(config->ur_http_headers);
    config->ur_http_headers = DESTROYED_POINTER;
}

void
ureport_server_response_free(struct ureport_server_response *resp)
{
    if (!resp)
        return;

    free(resp->urr_solution);
    resp->urr_solution = DESTROYED_POINTER;

    g_list_free_full(resp->urr_reported_to_list, g_free);
    resp->urr_reported_to_list = DESTROYED_POINTER;

    free(resp->urr_bthash);
    resp->urr_bthash = DESTROYED_POINTER;

    free(resp->urr_message);
    resp->urr_message = DESTROYED_POINTER;

    free(resp->urr_value);
    resp->urr_value = DESTROYED_POINTER;

    free(resp);
}

static char *
parse_solution_from_json_list(struct json_object *list,
                              GList **reported_to)
{
    json_object *list_elem, *struct_elem;
    const char *cause, *note, *url;
    struct strbuf *solution_buf = strbuf_new();

    const unsigned length = json_object_array_length(list);

    const char *one_format = _("Your problem seems to be caused by %s\n\n%s\n");
    if (length > 1)
    {
        strbuf_append_str(solution_buf, _("Your problem seems to be caused by one of the following:\n"));
        one_format = "\n* %s\n\n%s\n";
    }

    bool empty = true;
    for (unsigned i = 0; i < length; ++i)
    {
        list_elem = json_object_array_get_idx(list, i);
        if (!list_elem)
            continue;

        if (!json_object_object_get_ex(list_elem, "cause", &struct_elem))
            continue;

        cause = json_object_get_string(struct_elem);
        if (!cause)
            continue;

        if (!json_object_object_get_ex(list_elem, "note", &struct_elem))
            continue;

        note = json_object_get_string(struct_elem);
        if (!note)
            continue;

        empty = false;
        strbuf_append_strf(solution_buf, one_format, cause, note);

        if (!json_object_object_get_ex(list_elem, "url", &struct_elem))
            continue;

        url = json_object_get_string(struct_elem);
        if (url)
        {
            char *reported_to_line = xasprintf("%s: URL=%s", cause, url);
            *reported_to = g_list_append(*reported_to, reported_to_line);
        }
    }

    if (empty)
    {
        strbuf_free(solution_buf);
        return NULL;
    }

    return strbuf_free_nobuf(solution_buf);
}

/* reported_to json element should be a list of structures
   {
     "reporter": "Bugzilla",
     "type": "url",
     "value": "https://bugzilla.redhat.com/show_bug.cgi?id=XYZ"
   }
 */
static GList *
parse_reported_to_from_json_list(struct json_object *list)
{
    int i;
    json_object *list_elem, *struct_elem;
    const char *reporter, *value, *type;
    char *reported_to_line, *prefix;
    GList *result = NULL;

    for (i = 0; i < json_object_array_length(list); ++i)
    {
        prefix = NULL;
        list_elem = json_object_array_get_idx(list, i);
        if (!list_elem)
            continue;

        if (!json_object_object_get_ex(list_elem, "reporter", &struct_elem))
            continue;

        reporter = json_object_get_string(struct_elem);
        if (!reporter)
            continue;

        if (!json_object_object_get_ex(list_elem, "value", &struct_elem))
            continue;

        value = json_object_get_string(struct_elem);
        if (!value)
            continue;

        if (!json_object_object_get_ex(list_elem, "type", &struct_elem))
            continue;

        type = json_object_get_string(struct_elem);
        if (type)
        {
            if (strcasecmp("url", type) == 0)
                prefix = xstrdup("URL=");
            else if (strcasecmp("bthash", type) == 0)
                prefix = xstrdup("BTHASH=");
        }

        if (!prefix)
            prefix = xstrdup("");

        reported_to_line = xasprintf("%s: %s%s", reporter, prefix, value);
        free(prefix);

        result = g_list_append(result, reported_to_line);
    }

    return result;
}

/*
 * Reponse samples:
 * {"error":"field 'foo' is required"}
 * {"response":"true"}
 * {"response":"false"}
 */
static struct ureport_server_response *
ureport_server_parse_json(json_object *json)
{
    json_object *obj = NULL;
    if (json_object_object_get_ex(json, "error", &obj))
    {
        struct ureport_server_response *out_response = xzalloc(sizeof(*out_response));
        out_response->urr_is_error = true;
        /*
         * Used to use json_object_to_json_string(obj), but it returns
         * the string in quote marks (") - IOW, json-formatted string.
         */
        out_response->urr_value = xstrdup(json_object_get_string(obj));
        return out_response;
    }

    if (json_object_object_get_ex(json, "result", &obj))
    {
        struct ureport_server_response *out_response = xzalloc(sizeof(*out_response));
        out_response->urr_value = xstrdup(json_object_get_string(obj));

        json_object *message = NULL;
        if (json_object_object_get_ex(json, "message", &message))
            out_response->urr_message = xstrdup(json_object_get_string(message));

        json_object *bthash = NULL;
        if (json_object_object_get_ex(json, "bthash", &bthash))
            out_response->urr_bthash = xstrdup(json_object_get_string(bthash));

        json_object *reported_to_list = NULL;
        if (json_object_object_get_ex(json, "reported_to", &reported_to_list))
            out_response->urr_reported_to_list =
                parse_reported_to_from_json_list(reported_to_list);

        json_object *solutions = NULL;
        if (json_object_object_get_ex(json, "solutions", &solutions))
            out_response->urr_solution =
                parse_solution_from_json_list(solutions, &(out_response->urr_reported_to_list));

        return out_response;
    }

    return NULL;
}

struct ureport_server_response *
ureport_server_response_from_reply(post_state_t *post_state,
                                   struct ureport_server_config *config)
{
    /* Previously, the condition here was (post_state->errmsg[0] != '\0')
     * however when the server asks for optional client authentication and we do not have the certificates,
     * then post_state->errmsg contains "NSS: client certificate not found (nickname not specified)" even though
     * the request succeeded.
     */
    if (post_state->curl_result != CURLE_OK)
    {
        if (strcmp(post_state->errmsg, "") != 0)
            error_msg(_("Failed to upload uReport to the server '%s' with curl: %s"),
                                                                    config->ur_url,
                                                                    post_state->errmsg);
        else
            error_msg(_("Failed to upload uReport to the server '%s'"), config->ur_url);

        if (post_state->curl_error_msg != NULL && strcmp(post_state->curl_error_msg, "") != 0)
            error_msg(_("Error: %s"), post_state->curl_error_msg);

        return NULL;
    }

    if (post_state->http_resp_code == 404)
    {
        error_msg(_("The URL '%s' does not exist (got error 404 from server)"), config->ur_url);
        return NULL;
    }

    if (post_state->http_resp_code == 500)
    {
        error_msg(_("The server at '%s' encountered an internal error (got error 500)"), config->ur_url);
        return NULL;
    }

    if (post_state->http_resp_code == 503)
    {
        error_msg(_("The server at '%s' currently can't handle the request (got error 503)"), config->ur_url);
        return NULL;
    }

    if (post_state->http_resp_code != 202
            && post_state->http_resp_code != 400
            && post_state->http_resp_code != 413)
    {
        /* can't print better error message */
        error_msg(_("Unexpected HTTP response from '%s': %d"), config->ur_url, post_state->http_resp_code);
        log_notice("%s", post_state->body);
        return NULL;
    }

    json_object *const json = json_tokener_parse(post_state->body);

    if (json == NULL)
    {
        error_msg(_("Unable to parse response from ureport server at '%s'"), config->ur_url);
        log_notice("%s", post_state->body);
        json_object_put(json);
        return NULL;
    }

    struct ureport_server_response *response = ureport_server_parse_json(json);
    json_object_put(json);

    if (!response)
        error_msg(_("The response from '%s' has invalid format"), config->ur_url);
    else if ((post_state->http_resp_code == 202 && response->urr_is_error)
                || (post_state->http_resp_code != 202 && !response->urr_is_error))
    {
        /* HTTP CODE 202 means that call was successful but the response */
        /* has an error message */
        error_msg(_("Type mismatch has been detected in the response from '%s'"), config->ur_url);
    }

    return response;
}

bool
ureport_server_response_save_in_dump_dir(struct ureport_server_response *resp,
                                         const char *dump_dir_path,
                                         struct ureport_server_config *config)
{
    struct dump_dir *dd = dd_opendir(dump_dir_path, /* flags */ 0);
    if (!dd)
        return false;

    if (resp->urr_bthash)
    {
        {
            report_result_t rr = { .label = (char *)"uReport" };
            rr.bthash = resp->urr_bthash;
            add_reported_to_entry(dd, &rr);
        }

        {
            report_result_t rr = { .label = (char *)"ABRT Server" };
            rr.url = ureport_server_response_get_report_url(resp, config);
            add_reported_to_entry(dd, &rr);
            free(rr.url);
        }
    }

    if (resp->urr_reported_to_list)
    {
        for (GList *e = resp->urr_reported_to_list; e; e = g_list_next(e))
            add_reported_to(dd, e->data);
    }

    if (resp->urr_solution)
        dd_save_text(dd, FILENAME_NOT_REPORTABLE, resp->urr_solution);

    dd_close(dd);
    return true;
}

char *
ureport_server_response_get_report_url(struct ureport_server_response *resp,
                                       struct ureport_server_config *config)
{
    char *bthash_url = concat_path_file(config->ur_url, BTHASH_URL_SFX);
    char *report_url = concat_path_file(bthash_url, resp->urr_bthash);
    free(bthash_url);
    return report_url;
}

static void
ureport_add_str(struct json_object *ur, const char *key, const char *s)
{
    struct json_object *jstring = json_object_new_string(s);
    if (!jstring)
        die_out_of_memory();

    json_object_object_add(ur, key, jstring);
}

char *
ureport_from_dump_dir_ext(const char *dump_dir_path, const struct ureport_preferences *preferences)
{
    char *error_message;
    struct sr_report *report = sr_abrt_report_from_dir(dump_dir_path,
                                                       &error_message);

    if (!report)
    {
        if (NULL == preferences || !(preferences->urp_flags & UREPORT_PREF_FLAG_RETURN_ON_FAILURE))
            error_msg_and_die("%s", error_message);

        log_notice("%s", error_message);
        return NULL;
    }

    if (preferences != NULL && preferences->urp_auth_items != NULL)
    {
        struct dump_dir *dd = dd_opendir(dump_dir_path, DD_OPEN_READONLY);
        if (!dd)
            xfunc_die(); /* dd_opendir() already printed an error message */

        GList *iter = preferences->urp_auth_items;
        for ( ; iter != NULL; iter = g_list_next(iter))
        {
            const char *key = (const char *)iter->data;
            char *value = dd_load_text_ext(dd, key,
                    DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE | DD_FAIL_QUIETLY_ENOENT);

            if (value == NULL)
            {
                perror_msg("Cannot include '%s' in 'auth'", key);
                continue;
            }

            sr_report_add_auth(report, key, value);
            free(value);
        }

        dd_close(dd);
    }

    char *json_ureport = sr_report_to_json(report);
    sr_report_free(report);

    return json_ureport;
}

char *
ureport_from_dump_dir(const char *dump_dir_path)
{
    return ureport_from_dump_dir_ext(dump_dir_path, /*no preferences*/NULL);
}

struct post_state *
ureport_do_post(const char *json, struct ureport_server_config *config,
                const char *url_sfx)
{
    int flags = POST_WANT_BODY | POST_WANT_ERROR_MSG;

    if (config->ur_ssl_verify)
        flags |= POST_WANT_SSL_VERIFY;

    struct post_state *post_state = new_post_state(flags);

    if (config->ur_client_cert && config->ur_client_key)
    {
        post_state->client_cert_path = config->ur_client_cert;
        post_state->client_key_path = config->ur_client_key;
        post_state->cert_authority_cert_path = config->ur_cert_authority_cert;
    }
    else if (config->ur_username && config->ur_password)
    {
        post_state->username = config->ur_username;
        post_state->password = config->ur_password;
    }

    char **headers = xmalloc(sizeof(char *) * (3 + size_map_string(config->ur_http_headers)));
    headers[0] = (char *)"Accept: application/json";
    headers[1] = (char *)"Connection: close";
    headers[2] = NULL;

    if (config->ur_http_headers != NULL)
    {
        unsigned i = 2;
        const char *header;
        const char *value;
        map_string_iter_t iter;
        init_map_string_iter(&iter, config->ur_http_headers);
        while (next_map_string_iter(&iter, &header, &value))
            headers[i++] = xasprintf("%s: %s", header, value);
        headers[i] = NULL;
    }

    char *dest_url = concat_path_file(config->ur_url, url_sfx);

    post_string_as_form_data(post_state, dest_url, "application/json",
                     (const char **)headers, json);

    /* Client authentication failed. Try again without client auth.
     * CURLE_SSL_CONNECT_ERROR - cert not found/server doesnt trust the CA
     * CURLE_SSL_CERTPROBLEM - malformed certificate/no permission
     */
    if ((post_state->curl_result == CURLE_SSL_CONNECT_ERROR
         || post_state->curl_result == CURLE_SSL_CERTPROBLEM)
            && config->ur_client_cert && config->ur_client_key)
    {
        warn_msg("Authentication failed. Retrying unauthenticated.");
        free_post_state(post_state);
        post_state = new_post_state(flags);

        post_string_as_form_data(post_state, dest_url, "application/json",
                         (const char **)headers, json);

    }

    free(dest_url);

    for (unsigned i = size_map_string(config->ur_http_headers); i != 0; --i)
        free(headers[i + 1]);
    free(headers);

    return post_state;
}

struct ureport_server_response *
ureport_submit(const char *json, struct ureport_server_config *config)
{
    struct post_state *post_state = ureport_do_post(json, config, UREPORT_SUBMIT_ACTION);

    if (post_state == NULL)
    {
        error_msg(_("Failed on submitting the problem"));
        return NULL;
    }

    struct ureport_server_response *resp = ureport_server_response_from_reply(post_state, config);
    free(post_state);

    return resp;
}

char *
ureport_json_attachment_new(const char *bthash, const char *type, const char *data)
{
    struct json_object *attachment = json_object_new_object();
    if (!attachment)
        die_out_of_memory();

    ureport_add_str(attachment, "bthash", bthash);
    ureport_add_str(attachment, "type", type);
    ureport_add_str(attachment, "data", data);

    char *result = xstrdup(json_object_to_json_string(attachment));
    json_object_put(attachment);

    return result;
}

bool
ureport_attach_string(const char *bthash, const char *type, const char *data,
               struct ureport_server_config *config)
{
    char *json = ureport_json_attachment_new(bthash, type, data);
    post_state_t *post_state = ureport_do_post(json, config, UREPORT_ATTACH_ACTION);
    free(json);

    struct ureport_server_response *resp =
        ureport_server_response_from_reply(post_state, config);
    free_post_state(post_state);
    /* don't use str_bo_bool() because we require "true" string */
    const int result = !resp || resp->urr_is_error || strcmp(resp->urr_value, "true") != 0;

    if (resp && resp->urr_is_error)
        error_msg(_("The server at '%s' responded with an error: '%s'"),
                config->ur_url, resp->urr_value);

    ureport_server_response_free(resp);

    return result;
}

bool
ureport_attach_int(const char *bthash, const char *type, int data,
                    struct ureport_server_config *config)
{
    char *data_str = xasprintf("%d", data);
    const bool result = ureport_attach_string(bthash, type, data_str, config);
    free(data_str);

    return result;
}