/* 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 #include "ureport.h" #include "internal_libreport.h" #include "client.h" #include "libreport_curl.h" #include "abrt_rh_support.h" #include "reporter-rhtsupport.h" #include "problem_report.h" /* problem report format template */ #define PROBLEM_REPORT_TEMPLATE \ "%summary:: [abrt] [[%pkg_name%]][[: %crash_function%()]][[: %reason%]][[: TAINTED %tainted_short%]]\n" \ "\n" \ "Description of problem:: %bare_comment\n" \ "\n" \ "Additional info::" \ " count,reason,package,pkg_vendor,cmdline,executable,%reporter\n" \ "\n" \ "How reproducible:: %bare_reproducible\n" \ "\n" \ "Steps to reproduce:: %bare_reproducer\n" \ "\n" \ "Truncated backtrace:: %bare_%short_backtrace\n" \ "\n" \ "Other report identifiers:: %bare_reported_to\n" #define ABRT_ELEMENTS_KB_ARTICLE "https://access.redhat.com/articles/2134281" #define RHTSUPPORT_CASE_URL_PATH "cases" #define QUERY_HINTS_IF_SMALLER_THAN (8*1024*1024) static void ask_rh_credentials(char **login, char **password); #define INVALID_CREDENTIALS_LOOP(l, p, r, fncall) \ do {\ r = fncall;\ if (r->error == 0 || r->http_resp_code != 401 ) { break; }\ ask_rh_credentials(&l, &p);\ free_rhts_result(r);\ } while (1) #define STRCPY_IF_NOT_EQUAL(dest, src) \ do { if (strcmp(dest, src) != 0 ) { \ free(dest); \ dest = xstrdup(src); \ } } while (0) static report_result_t *get_reported_to(const char *dump_dir_name) { struct dump_dir *dd = dd_opendir(dump_dir_name, /*flags:*/ 0); if (!dd) xfunc_die(); report_result_t *reported_to = find_in_reported_to(dd, "RHTSupport"); dd_close(dd); return reported_to; } static int create_tarball(const char *tempfile, struct dump_dir *dd, problem_data_t *problem_data) { reportfile_t *file = NULL; int retval = 0; /* everything is ok so far .. */ int pipe_from_parent_to_child[2]; xpipe(pipe_from_parent_to_child); pid_t child = fork(); if (child == 0) { /* child */ close(pipe_from_parent_to_child[1]); xmove_fd(xopen3(tempfile, O_WRONLY | O_CREAT | O_EXCL, 0600), 1); xmove_fd(pipe_from_parent_to_child[0], 0); execlp("gzip", "gzip", NULL); perror_msg_and_die("Can't execute '%s'", "gzip"); } close(pipe_from_parent_to_child[0]); TAR *tar = NULL; if (tar_fdopen(&tar, pipe_from_parent_to_child[1], (char*)tempfile, /*fileops:(standard)*/ NULL, O_WRONLY | O_CREAT, 0644, TAR_GNU) != 0) { goto ret_fail; } file = new_reportfile(); { GHashTableIter iter; char *name; struct problem_item *value; g_hash_table_iter_init(&iter, problem_data); while (g_hash_table_iter_next(&iter, (void**)&name, (void**)&value)) { const char *content = value->content; if (value->flags & CD_FLAG_TXT) { reportfile_add_binding_from_string(file, name, content); } else if (value->flags & CD_FLAG_BIN) { const char *basename = strrchr(content, '/'); if (basename) basename++; else basename = content; char *xml_name = concat_path_file("content", basename); reportfile_add_binding_from_namedfile(file, /*on_disk_filename */ content, /*binding_name */ name, /*recorded_filename*/ xml_name, /*binary */ !(value->flags & CD_FLAG_BIGTXT) ); free(xml_name); } } } /* append all files from dump dir */ dd_init_next_file(dd); char *short_name, *full_name; while (dd_get_next_file(dd, &short_name, &full_name)) { char *uploaded_name = concat_path_file("content", short_name); free(short_name); if (tar_append_file(tar, full_name, uploaded_name) != 0) { free(full_name); goto ret_fail; } free(full_name); } const char *signature = reportfile_as_string(file); /* * Note: this pointer points to string which is owned by * "file" object, can't free "file" just yet. */ /* Write out content.xml in the tarball's root */ { unsigned len = strlen(signature); unsigned len512 = (len + 511) & ~511; char *block = (char*)memcpy(xzalloc(len512), signature, len); th_set_type(tar, S_IFREG | 0644); th_set_mode(tar, S_IFREG | 0644); //th_set_link(tar, char *linkname); //th_set_device(tar, dev_t device); //th_set_user(tar, uid_t uid); //th_set_group(tar, gid_t gid); th_set_mtime(tar, time(NULL)); th_set_path(tar, (char*)"content.xml"); th_set_size(tar, len); th_finish(tar); /* caclulate and store th xsum etc */ if (th_write(tar) != 0 /* writes header block */ /* writes content.xml, padded to 512 bytes */ || full_write(tar_fd(tar), block, len512) != len512 || tar_append_eof(tar) != 0 /* writes EOF blocks */ || tar_close(tar) != 0 ) { free(block); goto ret_fail; } tar = NULL; free(block); } /* We must be sure gzip finished, and finished successfully */ int status; safe_waitpid(child, &status, 0); child = -1; if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { /* Hopefully, by this time child emitted more meaningful * error message. But just in case it didn't: */ goto ret_fail; } goto ret_clean; /* success */ ret_fail: retval = 1; /* failure */ /* We must close write fd first, or else child will wait forever */ if (tar) tar_close(tar); //close(pipe_from_parent_to_child[1]); - tar_close() does it itself /* Now wait for child to exit */ if (child > 0) { // Damn, selinux does not allow SIGKILLing our own child! wtf?? //kill(child, SIGKILL); /* just in case */ safe_waitpid(child, NULL, 0); } ret_clean: dd_close(dd); /* now it's safe to free file */ free_reportfile(file); return retval; } static bool check_for_hints(const char *url, char **login, char **password, bool ssl_verify, const char *tempfile) { bool retval = false; rhts_result_t *result = NULL; INVALID_CREDENTIALS_LOOP((*login), (*password), result, get_rhts_hints(url, *login, *password, ssl_verify, tempfile) ); #if 0 /* testing */ log_warning("ERR:%d", result->error); log_warning("MSG:'%s'", result->msg); log_warning("BODY:'%s'", result->body); result->error = 0; result->body = xstrdup( "" "" "The main Red Hat Support web site" "an ABRT report" "" "a backtrace in the ABRT report" "[RHEL 5.3] EVO autocompletion lookup hang" "" "" ); #endif if (result->error) { /* We don't use result->msg here because it looks like this: * Error in file upload at 'URL', HTTP code: 404, * server says: '404...' * TODO: make server send bare textual msgs, not XML. */ error_msg("Error in file upload at '%s', HTTP code: %d", url, result->http_resp_code); } else if (result->body) { /* The message might contain URLs to known solutions and such */ char *hint = parse_response_from_RHTS_hint_xml2txt(result->body); if (hint) { hint = append_to_malloced_string(hint, " "); hint = append_to_malloced_string(hint, _("Do you still want to create a RHTSupport ticket?") ); /* * 'Yes' to the create ticket question means no hints were found. */ retval = !ask_yes_no(hint); free(hint); } } free_rhts_result(result); return retval; } static char *ask_rh_login(const char *message) { char *login = ask(message); if (login == NULL || login[0] == '\0') { set_xfunc_error_retval(EXIT_CANCEL_BY_USER); error_msg_and_die(_("Can't continue without login")); } return login; } static char *ask_rh_password(const char *message) { char *password = ask_password(message); if (password == NULL || password[0] == '\0') { set_xfunc_error_retval(EXIT_CANCEL_BY_USER); error_msg_and_die(_("Can't continue without password")); } return password; } static void ask_rh_credentials(char **login, char **password) { free(*login); free(*password); *login = ask_rh_login(_("Invalid password or login. Please enter your Red Hat login:")); char *question = xasprintf(_("Invalid password or login. Please enter the password for '%s':"), *login); *password = ask_rh_password(question); free(question); } static char *get_param_string(const char *name, map_string_t *settings, const char *dflt) { char *envname = xasprintf("RHTSupport_%s", name); const char *envvar = getenv(envname); free(envname); return xstrdup(envvar ? envvar : (get_map_string_item_or_NULL(settings, name) ? : dflt)); } static char *create_case_url(char *url, const char *case_no) { char *url1 = concat_path_file(url, RHTSUPPORT_CASE_URL_PATH); free(url); url = concat_path_file(url1, case_no); free(url1); return url; } static char *ask_case_no_create_url(char *url) { char *msg = xasprintf(_("Please enter customer case number to which you want to attach the data:")); char *case_no = ask(msg); free(msg); if (case_no == NULL || case_no[0] == '\0') { set_xfunc_error_retval(EXIT_CANCEL_BY_USER); error_msg_and_die(_("Can't continue without Red Hat Support case number")); } char *new_url = create_case_url(url, (const char *)case_no); free(case_no); return new_url; } int main(int argc, char **argv) { abrt_init(argv); /* I18n */ setlocale(LC_ALL, ""); #if ENABLE_NLS bindtextdomain(PACKAGE, LOCALEDIR); textdomain(PACKAGE); #endif const char *dump_dir_name = "."; const char *case_no = NULL; GList *conf_file = NULL; const char *fmt_file = NULL; /* Can't keep these strings/structs static: _() doesn't support that */ const char *program_usage_string = _( "\n" "& [-v] [-c CONFFILE] [-F FMTFILE] -d DIR\n" "or:\n" "& [-v] [-c CONFFILE] [-d DIR] -t[ID] [-u -C UR_CONFFILE] FILE...\n" "\n" "Reports a problem to RHTSupport.\n" "\n" "If not specified, CONFFILE defaults to "CONF_DIR"/plugins/rhtsupport.conf\n" "and user's local ~"USER_HOME_CONFIG_PATH"/rhtsupport.conf.\n" "Its lines should have 'PARAM = VALUE' format.\n" "Recognized string parameters: URL, Login, Password, BigFileURL.\n" "Recognized numeric parameter: BigSizeMB.\n" "Recognized boolean parameter (VALUE should be 1/0, yes/no): SSLVerify.\n" "User's local configuration overrides the system wide configuration.\n" "Parameters can be overridden via $RHTSupport_PARAM environment variables.\n" "\n" "Option -t uploads FILEs to the already created case on RHTSupport site.\n" "The case ID is retrieved from directory specified by -d DIR.\n" "If problem data in DIR was never reported to Red Hat Support, you will be asked\n" "to enter case ID to which you want to upload the FILEs.\n" "\n" "Option -tCASE uploads FILEs to the case CASE on RHTSupport site.\n" "-d DIR is ignored.\n" ); enum { OPT_v = 1 << 0, OPT_d = 1 << 1, OPT_c = 1 << 2, OPT_t = 1 << 3, OPT_f = 1 << 4, OPT_F = 1 << 5, OPT_D = 1 << 6, }; /* Keep enum above and order of options below in sync! */ struct options program_options[] = { OPT__VERBOSE(&g_verbose), OPT_STRING( 'd', NULL, &dump_dir_name, "DIR" , _("Problem directory")), OPT_LIST( 'c', NULL, &conf_file , "FILE", _("Configuration file (may be given many times)")), OPT_OPTSTRING('t', NULL, &case_no , "ID" , _("Upload FILEs [to case with this ID]")), OPT_BOOL( 'f', NULL, NULL , _("Force reporting even if this problem is already reported")), OPT_STRING( 'F', NULL, &fmt_file , "FILE", _("Formatting file for a new case")), OPT_BOOL( 'D', NULL, NULL , _("Debug")), OPT_END() }; unsigned opts = parse_opts(argc, argv, program_options, program_usage_string); argv += optind; export_abrt_envvars(0); /* Parse config, extract necessary params */ map_string_t *settings = new_map_string(); char *local_conf = NULL; if (!conf_file) { conf_file = g_list_append(conf_file, (char*) CONF_DIR"/plugins/rhtsupport.conf"); local_conf = xasprintf("%s"USER_HOME_CONFIG_PATH"/rhtsupport.conf", getenv("HOME")); conf_file = g_list_append(conf_file, local_conf); } while (conf_file) { const char *fn = (char *)conf_file->data; log_notice("Loading settings from '%s'", fn); load_conf_file(fn, settings, /*skip key w/o values:*/ false); log_debug("Loaded '%s'", fn); conf_file = g_list_remove(conf_file, fn); } free(local_conf); char *url = get_param_string("URL" , settings, "https://api.access.redhat.com/rs"); char *login = get_param_string("Login" , settings, ""); char *password = get_param_string("Password" , settings, ""); char *bigurl = get_param_string("BigFileURL", settings, "ftp://dropbox.redhat.com/incoming/"); if (login[0] == '\0') { free(login); login = ask_rh_login(_("Login is not provided by configuration. Please enter your RHTS login:")); } if (password[0] == '\0') { free(password); char *question = xasprintf(_("Password is not provided by configuration. Please enter the password for '%s':"), login); password = ask_rh_password(question); free(question); } char* envvar; envvar = getenv("RHTSupport_SSLVerify"); bool ssl_verify = string_to_bool( envvar ? envvar : (get_map_string_item_or_NULL(settings, "SSLVerify") ? : "1") ); envvar = getenv("RHTSupport_BigSizeMB"); unsigned bigsize = xatoi_positive( /* RH has a 250m limit for web attachments (as of 2013) */ envvar ? envvar : (get_map_string_item_or_NULL(settings, "BigSizeMB") ? : "200") ); free_map_string(settings); char *base_api_url = xstrdup(url); if (opts & OPT_t) { if (!case_no) { /* -t: extract URL where we previously reported it */ report_result_t *reported_to = get_reported_to(dump_dir_name); if (reported_to && reported_to->url) { free(url); url = reported_to->url; reported_to->url = NULL; free_report_result(reported_to); char *msg = xasprintf( _("We found a similar Red Hat support case %s. " "Do you want to attach the data to the case? " "Otherwise, you will have to enter the existing " "Red Hat support case number."), url); int yes = ask_yes_no(msg); free(msg); if (!yes) url = ask_case_no_create_url(url); } else { log_warning("Problem was not reported to Red Hat Support."); url = ask_case_no_create_url(url); } } else { /* -tCASE */ url = create_case_url(url, case_no); } if (*argv) { /* -t[CASE] FILE: just attach files and exit */ while (*argv) { log_warning(_("Attaching '%s' to case '%s'"), *argv, url); rhts_result_t *result = attach_file_to_case(url, login, password, ssl_verify, *argv ); if (result->error) error_msg_and_die("%s", result->msg); log_warning("Attachment URL:%s", result->url); log_warning("File attached successfully"); free_rhts_result(result); argv++; } return 0; } } else /* no -t: creating a new case */ { if (*argv) show_usage_and_die(program_usage_string, program_options); report_result_t *reported_to = get_reported_to(dump_dir_name); if (reported_to && reported_to->url && !(opts & OPT_f)) { char *msg = xasprintf("This problem was already reported to RHTS (see '%s')." " Do you still want to create a RHTSupport ticket?", reported_to->url); int yes = ask_yes_no(msg); free(msg); if (!yes) return 0; } free_report_result(reported_to); } problem_data_t *problem_data = create_problem_data_for_reporting(dump_dir_name); if (!problem_data) xfunc_die(); /* create_problem_data_for_reporting already emitted error msg */ const char *errmsg = NULL; char tmpdir_name[sizeof(LARGE_DATA_TMP_DIR"/rhtsupport-"LIBREPORT_ISO_DATE_STRING_SAMPLE"-XXXXXX")]; snprintf(tmpdir_name, sizeof(tmpdir_name), LARGE_DATA_TMP_DIR"/rhtsupport-%s-XXXXXX", iso_date_string(NULL)); /* mkdtemp does mkdir(xxx, 0700), should be safe (is it?) */ if (mkdtemp(tmpdir_name) == NULL) { error_msg_and_die(_("Can't create a temporary directory in "LARGE_DATA_TMP_DIR)); } /* Starting from here, we must perform cleanup on errors * (delete temp dir) */ char *tempfile = NULL; tempfile = concat_path_basename(tmpdir_name, dump_dir_name); tempfile = append_to_malloced_string(tempfile, ".tar.gz"); rhts_result_t *result = NULL; rhts_result_t *result_atch = NULL; const char *package; package = problem_data_get_content_or_NULL(problem_data, FILENAME_PACKAGE); const char *dsc = NULL; const char *summary = NULL; const char *count = NULL; count = problem_data_get_content_or_NULL(problem_data, FILENAME_COUNT); if (count != NULL && strcmp(count, "1") == 0 /* the 'count' file can lie */ && get_problem_data_reproducible(problem_data) <= PROBLEM_REPRODUCIBLE_UNKNOWN) { int r = ask_yes_no( _("The problem has only occurred once and the ability to reproduce " "the problem is unknown. Please ensure you will be able to " "provide detailed information to our Support Team. " "Would you like to continue and open a new support case?")); if (!r) exit(EXIT_CANCEL_BY_USER); } const char *vendor = NULL; vendor = problem_data_get_content_or_NULL(problem_data, FILENAME_PKG_VENDOR); if (package && vendor && strcmp(vendor, "Red Hat, Inc.") != 0) { char *message = xasprintf( _("The crashed program was released by '%s'. " "Would you like to report the problem to Red Hat Support?"), vendor); int r = ask_yes_no(message); free(message); if (!r) exit(EXIT_CANCEL_BY_USER); } /* In the case there is no pkg_vendor file use "unknown vendor" */ if (!vendor) problem_data_add_text_noteditable(problem_data, FILENAME_PKG_VENDOR, "unknown vendor"); const char *executable = NULL; executable = problem_data_get_content_or_NULL(problem_data, FILENAME_EXECUTABLE); if (!package) { char *message = xasprintf( _("The program '%s' does not appear to be provided by Red Hat. " "Would you like to report the problem to Red Hat Support?"), executable); int r = ask_yes_no(message); free(message); if (!r) exit(EXIT_CANCEL_BY_USER); problem_data_add_text_noteditable(problem_data, FILENAME_PACKAGE, "not belong to any package"); } problem_formatter_t *pf = problem_formatter_new(); /* formatting conf file was set */ if (fmt_file) { if (problem_formatter_load_file(pf, fmt_file)) error_msg_and_die("Invalid format file: %s", fmt_file); } /* using formatting template */ else { if (problem_formatter_load_string(pf, PROBLEM_REPORT_TEMPLATE)) error_msg_and_die("Invalid problem report format string"); } problem_report_t *pr = NULL; if (problem_formatter_generate_report(pf, problem_data, &pr)) error_msg_and_die("Failed to format bug report from problem data"); /* Add information about attachments into the description */ problem_report_buffer *dsc_buffer = problem_report_get_buffer(pr, PR_SEC_DESCRIPTION); char *tarball_name = basename(tempfile); problem_report_buffer_printf(dsc_buffer, "\n" "sosreport and other files were attached as '%s' to the case.\n" "For more details about elements collected by ABRT see:\n" "%s\n" , tarball_name, ABRT_ELEMENTS_KB_ARTICLE); summary = problem_report_get_summary(pr); dsc = problem_report_get_description(pr); /* debug */ if (opts & OPT_D) { printf("summary: %s\n" "\n" "%s" "\n" , summary , dsc ); problem_report_free(pr); problem_formatter_free(pf); exit(0); } /* Gzipping e.g. 0.5gig coredump takes a while. Let user know what we are doing */ log_warning(_("Compressing data")); struct dump_dir *dd = dd_opendir(dump_dir_name, /*flags:*/ 0); if (!dd) xfunc_die(); /* error msg is already logged by dd_opendir */ if (create_tarball(tempfile, dd, problem_data) != 0) { errmsg = _("Can't create temporary file in "LARGE_DATA_TMP_DIR); goto ret; } off_t tempfile_size = stat_st_size_or_die(tempfile); if (!(opts & OPT_t)) { if (tempfile_size <= QUERY_HINTS_IF_SMALLER_THAN) { /* Check for hints and show them if we have something */ log_warning(_("Checking for hints")); if (check_for_hints(base_api_url, &login, &password, ssl_verify, tempfile)) goto ret; } log_warning(_("Creating a new case")); char *product = NULL; char *version = NULL; map_string_t *osinfo = new_map_string(); problem_data_get_osinfo(problem_data, osinfo); parse_osinfo_for_rhts(osinfo, &product, &version); free_map_string(osinfo); if (!product) { /* How can we help user sorting out this problem? */ error_msg_and_die(_("Can't determine RH Support Product from problem data.")); } INVALID_CREDENTIALS_LOOP(login, password, result, create_new_case(url, login, password, ssl_verify, product, version, summary, dsc, package) ); free(version); free(product); problem_report_free(pr); problem_formatter_free(pf); if (result->error) { /* * Message can contain "...server says: 'multi-line text'" * Replace all '\n' with spaces: * we want this message to be, logically, one log entry. * IOW: one line, not many lines. */ char *src, *dst; errmsg = dst = src = result->msg; while (1) { unsigned char c = *src++; if (c == '\n') c = ' '; *dst++ = c; if (c == '\0') break; } /* Remove trailing spaces (usually produced by trailing '\n') */ while (--dst >= errmsg && *dst == ' ') *dst = '\0'; goto ret; } /* No error in case creation */ /* Record "reported_to" element */ dd = dd_opendir(dump_dir_name, /*flags:*/ 0); if (dd) { struct report_result rr = { .label = (char *)"RHTSupport" }; rr.url = result->url; rr.msg = result->msg; time(&rr.timestamp); add_reported_to_entry(dd, &rr); dd_close(dd); if (result->msg) log_warning("%s", result->msg); log_warning("URL=%s", result->url); } /* else: error msg was already emitted by dd_opendir */ url = result->url; result->url = NULL; free_rhts_result(result); result = NULL; } char *remote_filename = NULL; if (bigsize != 0 && tempfile_size / (1024*1024) >= bigsize) { /* Upload tarball of -d DIR to "big file" FTP */ /* log_warning(_("Uploading problem data to '%s'"), bigurl); - upload_file does this */ remote_filename = upload_file(bigurl, tempfile); } if (remote_filename) { log_warning(_("Adding comment to case '%s'"), url); /* * Do not translate message below - it goes * to a server where *other people* will read it. */ char *comment_text = xasprintf( "Problem data was uploaded to %s", remote_filename ); free(remote_filename); INVALID_CREDENTIALS_LOOP(login, password, result_atch, add_comment_to_case(url, login, password, ssl_verify, comment_text) ); free(comment_text); } else { /* Attach the tarball of -d DIR */ log_warning(_("Attaching problem data to case '%s'"), url); INVALID_CREDENTIALS_LOOP(login, password, result_atch, attach_file_to_case(url, login, password, ssl_verify, tempfile) ); } if (result_atch->error) { if (!(opts & OPT_t)) { /* Prepend "Case created" text to whatever error message there is, * so that user knows that case _was_ created despite error in attaching. */ log_warning("Case created but failed to attach problem data: %s", result_atch->msg); } else { log_warning("Failed to attach problem data: %s", result_atch->msg); } } ret: unlink(tempfile); free(tempfile); rmdir(tmpdir_name); /* Note: errmsg may be = result->msg, don't move this code block * below free_rhts_result(result)! */ if (errmsg) error_msg_and_die("%s", errmsg); free_rhts_result(result_atch); free_rhts_result(result); free(base_api_url); free(url); free(login); free(password); problem_data_free(problem_data); return 0; }