Blob Blame History Raw
/*
    Copyright (C) 2010  ABRT team
    Copyright (C) 2010  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 <libxml/encoding.h>
#include <libxml/xmlwriter.h>
#include <curl/curl.h>
#include "internal_libreport.h"
#include "libreport_curl.h"
#include "abrt_rh_support.h"

struct reportfile {
    xmlTextWriterPtr writer;
    xmlBufferPtr     buf;
};

static void __attribute__((__noreturn__))
die_xml_oom(void)
{
    error_msg_and_die("Can't create XML attribute (out of memory?)");
}

static xmlBufferPtr
xxmlBufferCreate(void)
{
    xmlBufferPtr r = xmlBufferCreate();
    if (!r)
        die_xml_oom();
    return r;
}

static xmlTextWriterPtr
xxmlNewTextWriterMemory(xmlBufferPtr buf /*, int compression*/)
{
    xmlTextWriterPtr r = xmlNewTextWriterMemory(buf, /*compression:*/ 0);
    if (!r)
        die_xml_oom();
    return r;
}

static void
xxmlTextWriterStartDocument(xmlTextWriterPtr writer,
    const char * version,
    const char * encoding,
    const char * standalone)
{
    if (xmlTextWriterStartDocument(writer, version, encoding, standalone) < 0)
        die_xml_oom();
}

static void
xxmlTextWriterEndDocument(xmlTextWriterPtr writer)
{
    if (xmlTextWriterEndDocument(writer) < 0)
        die_xml_oom();
}

static void
xxmlTextWriterStartElement(xmlTextWriterPtr writer, const char *name)
{
    // these bright guys REDEFINED CHAR (!) to unsigned char...
    if (xmlTextWriterStartElement(writer, (unsigned char*)name) < 0)
        die_xml_oom();
}

static void
xxmlTextWriterEndElement(xmlTextWriterPtr writer)
{
    if (xmlTextWriterEndElement(writer) < 0)
        die_xml_oom();
}

static void
xxmlTextWriterWriteElement(xmlTextWriterPtr writer, const char *name, const char *content)
{
    if (xmlTextWriterWriteElement(writer, (unsigned char*)name, (unsigned char*)content) < 0)
        die_xml_oom();
}

static void
xxmlTextWriterWriteAttribute(xmlTextWriterPtr writer, const char *name, const char *content)
{
    if (xmlTextWriterWriteAttribute(writer, (unsigned char*)name, (unsigned char*)content) < 0)
        die_xml_oom();
}

#if 0 //unused
static void
xxmlTextWriterWriteString(xmlTextWriterPtr writer, const char *content)
{
    if (xmlTextWriterWriteString(writer, (unsigned char*)content) < 0)
        die_xml_oom();
}
#endif

//
// Reportfile helpers
//

// End the reportfile, and prepare it for delivery.
// No more bindings can be added after this.
static void
close_writer(reportfile_t* file)
{
    if (!file->writer)
        return;

    // close off the end of the xml file
    xxmlTextWriterEndDocument(file->writer);
    xmlFreeTextWriter(file->writer);
    file->writer = NULL;
}

// This allocates a reportfile_t structure and initializes it.
reportfile_t*
new_reportfile(void)
{
    // create a new reportfile_t
    reportfile_t* file = (reportfile_t*)xmalloc(sizeof(*file));

    // set up a libxml 'buffer' and 'writer' to that buffer
    file->buf = xxmlBufferCreate();
    file->writer = xxmlNewTextWriterMemory(file->buf);

    // start a new xml document:
    // <report xmlns="http://www.redhat.com/gss/strata">...
    xxmlTextWriterStartDocument(file->writer, /*version:*/ NULL, /*encoding:*/ NULL, /*standalone:*/ NULL);
    xxmlTextWriterStartElement(file->writer, "report");
    xxmlTextWriterWriteAttribute(file->writer, "xmlns", "http://www.redhat.com/gss/strata");

    return file;
}

static void
internal_reportfile_start_binding(reportfile_t* file, const char* name, int isbinary, const char* filename)
{
    // <binding name=NAME [fileName=FILENAME] type=text/binary...
    xxmlTextWriterStartElement(file->writer, "binding");
    xxmlTextWriterWriteAttribute(file->writer, "name", name);
    if (filename)
        xxmlTextWriterWriteAttribute(file->writer, "fileName", filename);
    if (isbinary)
        xxmlTextWriterWriteAttribute(file->writer, "type", "binary");
    else
        xxmlTextWriterWriteAttribute(file->writer, "type", "text");
}

// Add a new text binding
void
reportfile_add_binding_from_string(reportfile_t* file, const char* name, const char* value)
{
    // <binding name=NAME type=text value=VALUE>
    internal_reportfile_start_binding(file, name, /*isbinary:*/ 0, /*filename:*/ NULL);
    xxmlTextWriterWriteAttribute(file->writer, "value", value);
    xxmlTextWriterEndElement(file->writer);
}

// Add a new binding to a report whose value is represented as a file.
void
reportfile_add_binding_from_namedfile(reportfile_t* file,
                const char* on_disk_filename, /* unused so far */
                const char* binding_name,
                const char* recorded_filename,
                int isbinary)
{
    // <binding name=NAME fileName=FILENAME type=text/binary...
    internal_reportfile_start_binding(file, binding_name, isbinary, recorded_filename);
    // ... href=content/NAME>
    char *href_name = concat_path_file("content", binding_name);
    xxmlTextWriterWriteAttribute(file->writer, "href", href_name);
    free(href_name);
}

// Return the contents of the reportfile as a string.
const char*
reportfile_as_string(reportfile_t* file)
{
    close_writer(file);
    // unsigned char -> char
    return (char*)file->buf->content;
}

void
free_reportfile(reportfile_t* file)
{
    if (!file)
        return;
    close_writer(file);
    xmlBufferFree(file->buf);
    free(file);
}


void free_rhts_result(rhts_result_t *p)
{
    if (!p)
        return;
    free(p->url);
    free(p->msg);
    free(p->body);
    free(p);
}

//
// Common
//
static const char *const text_plain_header[] = {
    "Accept: text/plain",
    NULL
};

//
// Creating new case
// See
// https://access.redhat.com/knowledge/docs/Red_Hat_Customer_Portal/integration_guide.html
//
// $ curl -X POST -H 'Content-Type: application/xml' --data
//  '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
//   <case xmlns="http://www.redhat.com/gss/strata">
//   <summary>Example Case</summary>
//   <description>Example created with cURL</description>
//   <product>Red Hat Enterprise Linux</product><version>6.0</version>
//   </case>'
//   https://api.access.redhat.com/rs/cases
//
static char*
make_case_data(const char* summary, const char* description,
               const char* product, const char* version,
               const char* component)
{
    char* retval;
    xmlTextWriterPtr writer;
    xmlBufferPtr buf;

    buf = xxmlBufferCreate();
    writer = xxmlNewTextWriterMemory(buf);

    xxmlTextWriterStartDocument(writer, NULL, "UTF-8", "yes");
    xxmlTextWriterStartElement(writer, "case");
    xxmlTextWriterWriteAttribute(writer, "xmlns",
                                   "http://www.redhat.com/gss/strata");

    xxmlTextWriterWriteElement(writer, "summary", summary);
    xxmlTextWriterWriteElement(writer, "description", description);
    if (product) {
        xxmlTextWriterWriteElement(writer, "product", product);
    }
    if (version) {
        xxmlTextWriterWriteElement(writer, "version", version);
    }
    if (component) {
        xxmlTextWriterWriteElement(writer, "component", component);
    }

    xxmlTextWriterEndDocument(writer);
    retval = xstrdup((const char*)buf->content);
    xmlFreeTextWriter(writer);
    xmlBufferFree(buf);
    return retval;
}

static rhts_result_t*
post_case_to_url(const char* url,
                const char* username,
                const char* password,
                bool ssl_verify,
                const char **additional_headers,
                const char* product,
                const char* version,
                const char* summary,
                const char* description,
                const char* component)
{
    rhts_result_t *result = xzalloc(sizeof(*result));
    char *url_copy = NULL;

    char *case_data = make_case_data(summary, description,
                                         product, version,
                                         component);

    int redirect_count = 0;
    char *errmsg;
    post_state_t *post_state;

 redirect:
    post_state = new_post_state(0
            + POST_WANT_HEADERS
            + POST_WANT_BODY
            + POST_WANT_ERROR_MSG
            + (ssl_verify ? POST_WANT_SSL_VERIFY : 0)
    );
    post_state->username = username;
    post_state->password = password;

    post_string(post_state, url, "application/xml", additional_headers, case_data);

    char *location = find_header_in_post_state(post_state, "Location:");

    switch (post_state->http_resp_code)
    {
    case 404:
        /* Not strictly necessary (default branch would deal with it too),
         * but makes this typical error less cryptic:
         * instead of returning html-encoded body, we show short concise message,
         * and show offending URL (typos in which is a typical cause) */
        result->error = -1;
        result->msg = xasprintf("Error in HTTP POST, "
                        "HTTP code: 404 (Not found), URL:'%s'", url);
        break;

    case 301: /* "301 Moved Permanently" (for example, used to move http:// to https://) */
    case 302: /* "302 Found" (just in case) */
    case 305: /* "305 Use Proxy" */
        if (++redirect_count < 10 && location)
        {
            free(url_copy);
            url = url_copy = xstrdup(location);
            free_post_state(post_state);
            goto redirect;
        }
        /* fall through */

    default:
        // TODO: error messages in headers
        // are observed to be more informative than the body:
        //
        // 'HTTP/1.1 400 Bad Request'
        // 'Date: Mon, 10 Oct 2011 13:31:56 GMT^M'
        // 'Server: Apache^M'
        // 'Strata-Message: The supplied parameter Fedora value  can not be processed^M'
        // ^^^^^^^^^^^^^^^^^^^^^^^^^ useful message
        // 'Strata-Code: BAD_PARAMETER^M'
        // 'Content-Length: 1^M'
        // 'Content-Type: text/plain; charset=UTF-8^M'
        // 'Connection: close^M'
        // '^M'
        // ' '  <------ body is useless
        result->error = -1;
        errmsg = post_state->curl_error_msg;
        if (errmsg && errmsg[0])
        {
            result->msg = xasprintf(_("Error in case creation at '%s': %s"),
                    url, errmsg);
        }
        else
        {
            errmsg = find_header_in_post_state(post_state, "Strata-Message:");
            if (!errmsg)
                errmsg = post_state->body;
            if (errmsg && errmsg[0])
                result->msg = xasprintf(_("Error in case creation at '%s',"
                        " HTTP code: %d, server says: '%s'"),
                        url, post_state->http_resp_code, errmsg);
            else
                result->msg = xasprintf(_("Error in case creation at '%s',"
                        " HTTP code: %d"),
                        url, post_state->http_resp_code);
        }
        break;

    case 200:
    case 201:
        /* Created successfully */
        result->url = xstrdup(location); /* note: xstrdup(NULL) returns NULL */
    } /* switch (HTTP code) */

    result->http_resp_code = post_state->http_resp_code;
    result->body = post_state->body;
    post_state->body = NULL;

    free_post_state(post_state);
    free(case_data);
    free(url_copy);
    return result;
}

rhts_result_t*
create_new_case(const char* base_url,
                const char* username,
                const char* password,
                bool ssl_verify,
                const char* product,
                const char* version,
                const char* summary,
                const char* description,
                const char* component)
{
    char *url = concat_path_file(base_url, "cases");
    rhts_result_t *result = post_case_to_url(url,
                username,
                password,
                ssl_verify,
                (const char **)text_plain_header,
                product,
                version,
                summary,
                description,
                component
    );

    if (!result->error && !result->url)
    {
        /* Case Creation returned valid code, but no location */
        result->error = -1;
        free(result->msg);
        result->msg = xasprintf(_("Error in case creation at '%s':"
                " no Location URL, HTTP code: %d"),
                url, result->http_resp_code
        );
    }
    free(url);

    return result;
}

//
// Add case comment
//
// $ curl -X POST -H 'Content-Type: application/xml' --data
//  '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
//   <comment xmlns="http://www.redhat.com/gss/strata">
//   <text>Test comment!  This can contain lots of information, etc.</text>
//   </comment>'
//   https://api.access.redhat.com/rs/cases/NNNNNNN/comments
//
static char*
make_comment_data(const char *comment_text)
{
    char *retval;
    xmlTextWriterPtr writer;
    xmlBufferPtr buf;

    buf = xxmlBufferCreate();
    writer = xxmlNewTextWriterMemory(buf);

    xxmlTextWriterStartDocument(writer, NULL, "UTF-8", "yes");
    xxmlTextWriterStartElement(writer, "comment");
    xxmlTextWriterWriteAttribute(writer, "xmlns",
                                   "http://www.redhat.com/gss/strata");

    xxmlTextWriterWriteElement(writer, "text", comment_text);

    xxmlTextWriterEndDocument(writer);
    retval = xstrdup((const char*)buf->content);
    xmlFreeTextWriter(writer);
    xmlBufferFree(buf);
    return retval;
}

static rhts_result_t*
post_comment_to_url(const char *url,
                const char *username,
                const char *password,
                bool ssl_verify,
                const char **additional_headers,
                const char *comment_text)
{
    rhts_result_t *result = xzalloc(sizeof(*result));
    char *url_copy = NULL;

    char *xml = make_comment_data(comment_text);

    int redirect_count = 0;
    char *errmsg;
    post_state_t *post_state;

 redirect:
    post_state = new_post_state(0
            + POST_WANT_HEADERS
            + POST_WANT_BODY
            + POST_WANT_ERROR_MSG
            + (ssl_verify ? POST_WANT_SSL_VERIFY : 0)
    );
    post_state->username = username;
    post_state->password = password;

    post_string(post_state, url, "application/xml", additional_headers, xml);

    char *location = find_header_in_post_state(post_state, "Location:");

    switch (post_state->http_resp_code)
    {
    case 404:
        /* Not strictly necessary (default branch would deal with it too),
         * but makes this typical error less cryptic:
         * instead of returning html-encoded body, we show short concise message,
         * and show offending URL (typos in which is a typical cause) */
        result->error = -1;
        result->msg = xasprintf("Error in HTTP POST, "
                        "HTTP code: 404 (Not found), URL:'%s'", url);
        break;

    case 301: /* "301 Moved Permanently" (for example, used to move http:// to https://) */
    case 302: /* "302 Found" (just in case) */
    case 305: /* "305 Use Proxy" */
        if (++redirect_count < 10 && location)
        {
            free(url_copy);
            url = url_copy = xstrdup(location);
            free_post_state(post_state);
            goto redirect;
        }
        /* fall through */

    default:
        result->error = -1;
        errmsg = post_state->curl_error_msg;
        if (errmsg && errmsg[0])
        {
            result->msg = xasprintf(_("Error in comment creation at '%s': %s"),
                        url, errmsg);
        }
        else
        {
            errmsg = find_header_in_post_state(post_state, "Strata-Message:");
            if (!errmsg)
                errmsg = post_state->body;
            if (errmsg && errmsg[0])
                result->msg = xasprintf(_("Error in comment creation at '%s',"
                        " HTTP code: %d, server says: '%s'"),
                        url, post_state->http_resp_code, errmsg);
            else
                result->msg = xasprintf(_("Error in comment creation at '%s',"
                        " HTTP code: %d"),
                        url, post_state->http_resp_code);
        }
        break;

    case 200:
    case 201:
        /* Created successfully */
        result->url = xstrdup(location); /* note: xstrdup(NULL) returns NULL */
    } /* switch (HTTP code) */

    result->http_resp_code = post_state->http_resp_code;
    result->body = post_state->body;
    post_state->body = NULL;

    free_post_state(post_state);
    free(xml);
    free(url_copy);
    return result;
}

rhts_result_t*
add_comment_to_case(const char* base_url,
                const char* username,
                const char* password,
                bool ssl_verify,
                const char* comment_text)
{
    char *url = concat_path_file(base_url, "comments");
    rhts_result_t *result = post_comment_to_url(url,
                username,
                password,
                ssl_verify,
    // NB! text_plain_header here was causing error 404 instead of 201 (Created)!
    // NULL makes curl use "Accept: */*" instead and creation works.
    // Likely a bug on the server!
                (const char **) NULL, //text_plain_header,
                comment_text
    );

    if (!result->error && !result->url)
    {
        /* Creation returned valid code, but no location */
        result->error = -1;
        free(result->msg);
        result->msg = xasprintf(_("Error in comment creation at '%s':"
                " no Location URL, HTTP code: %d"),
                url, result->http_resp_code
        );
    }
    free(url);

    return result;
}

//
// Attach file to case
//
static rhts_result_t*
post_file_to_url(const char* url,
                const char* username,
                const char* password,
                bool ssl_verify,
                bool post_as_form,
                const char **additional_headers,
                const char *file_name)
{
    rhts_result_t *result = xzalloc(sizeof(*result));
    char *url_copy = NULL;

    int redirect_count = 0;
    char *errmsg;
    post_state_t *atch_state;

 redirect_attach:
    atch_state = new_post_state(0
            + POST_WANT_HEADERS
            + POST_WANT_BODY
            + POST_WANT_ERROR_MSG
            + (ssl_verify ? POST_WANT_SSL_VERIFY : 0)
    );
    atch_state->username = username;
    atch_state->password = password;
    if (post_as_form)
    {
        /* Sends data in multipart/mixed document. One detail is that
	 * file *name* is also sent to the server.
	 */
        post_file_as_form(atch_state,
            url,
            "application/octet-stream",
            additional_headers,
            file_name
        );
    }
    else
    {
        /* Sends file's raw contents */
        post_file(atch_state,
            url,
            "application/octet-stream",
            additional_headers,
            file_name
        );
    }

    char *atch_location = find_header_in_post_state(atch_state, "Location:");

    switch (atch_state->http_resp_code)
    {
    case 305: /* "305 Use Proxy" */
        if (++redirect_count < 10 && atch_location)
        {
            free(url_copy);
            url = url_copy = xstrdup(atch_location);
            free_post_state(atch_state);
            goto redirect_attach;
        }
        /* fall through */

    default:
        /* Error */
        result->error = -1;
        errmsg = atch_state->curl_error_msg;
        if (errmsg && errmsg[0])
        {
            result->msg = xasprintf("Error in file upload at '%s': %s",
                    url, errmsg);
        }
        else
        {
            errmsg = atch_state->body;
            if (errmsg && errmsg[0])
                result->msg = xasprintf("Error in file upload at '%s',"
                        " HTTP code: %d, server says: '%s'",
                        url, atch_state->http_resp_code, errmsg);
            else
                result->msg = xasprintf("Error in file upload at '%s',"
                        " HTTP code: %d",
                        url, atch_state->http_resp_code);
        }
        break;

    case 200:
    case 201:
        result->url = xstrdup(atch_location); /* note: xstrdup(NULL) returns NULL */
        //result->msg = xstrdup("File uploaded successfully");
    } /* switch (HTTP code) */

    result->http_resp_code = atch_state->http_resp_code;
    result->body = atch_state->body;
    atch_state->body = NULL;

    free_post_state(atch_state);
    free(url_copy);
    return result;
}

rhts_result_t*
attach_file_to_case(const char* base_url,
                const char* username,
                const char* password,
                bool ssl_verify,
                const char *file_name)
{
    char *url = concat_path_file(base_url, "attachments");
    rhts_result_t *result = post_file_to_url(url,
                username,
                password,
                ssl_verify,
                /*post_as_form:*/ true,
                (const char **) text_plain_header,
                file_name
    );
    free(url);
    return result;
}

//
// Get hint
//
rhts_result_t*
get_rhts_hints(const char* base_url,
                const char* username,
                const char* password,
                bool ssl_verify,
                const char* file_name)
{
    char *url = concat_path_file(base_url, "problems");
//    rhts_result_t *result = post_case_to_url(url,
//                username,
//                password,
//                ssl_verify,
//                NULL,
//                release,
//                summary,
//                description,
//                component
//    );
    rhts_result_t *result = post_file_to_url(url,
                username,
                password,
                ssl_verify,
                /*post_as_form:*/ false,
                /*headers:*/ NULL,
                file_name
    );
    free(url);
    return result;
}