/* fastback.cpp Copyright (C) 2009 Gavin Romig-Koch Copyright (C) 2009 Red Hat 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. */ /* Fastback uploads a file to a pre-configurable place. Fastback can be configured to send files (core files, log files, sos reports, etc) to places like your company's helpdesk, or support provider so that when something goes wrong you can concentrate on sending the file, not on remembering where or how to send the file. Fastback can also be included in automated scripts for delivering files. The file is named on the command line. An option to the command will cause the file to be encrypted before upload, and the output of the command will include the encryption key used. An option to the command will allow the user to associate a ticket name/number with the file. $ fastback FILE [ -t TICKET | -n ] [ -e ] where FILE is the file to be uploaded; where -e indicates that the file should be encrypted before it's uploaded; where -n indicates a new ticket; where TICKET is the name of the ticket If -t is not specified, -n is assumed. The name of the uploaded file will be the FILE name, prefixed with the TICKET name, and suffixed with a string of random characters to make the file unique. The file will be compressed if it is not already compressed. The configuration file is called fastback.conf. The program keeps a log of all files uploaded, where they were uploaded to, encryption key used (if any), and a log of the messages from the transfer. */ // $ fastback FILE [ -t TICKET | -n ] [ -e ] // where FILE is the file to be uploaded // where -e indicates that the file should be encrypted // where -n indicates a new ticket // where TICKET is the name of the ticket // // If -t is not specified, -n is assumed. // #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include #include #include #include #include #include #include #include #include #include #include #include typedef std::string string; static const char fastback_name[] = "fastback"; static const char fastback_config_file[] = "/etc/fastback.conf"; // These are command line options // if set they must be malloc'ed memory. static char* fastback_filename = 0; // local file to be uploaded static char* fastback_ticket = 0; // ticket to upload to static char* fastback_URLDIR = 0; static char* fastback_conf_LOGFILE = 0; static bool fastback_encrypt = false; // encrypt file before upload // '-n' option explicitly set static bool fastback_newticket = false; static bool fastback_verbose = false; static FILE* fastback_logfile = 0; // if we need to create temporary files // first create a temporary directory, stick it's name here // and delete the whole tree when we are done static string fastback_tmpdir; static size_t fastback_read(void *buffer, size_t size, size_t nmemb, void *userp) { return fread(buffer, size, nmemb, (FILE*)userp); } static void show(FILE* file, string url) { if (fastback_URLDIR) fprintf(file,"fastback URLDIR: %s\n", fastback_URLDIR); else fprintf(file,"fastback URLDIR not set\n"); if (fastback_conf_LOGFILE) fprintf(file,"fastback LOGFILE: %s\n", fastback_conf_LOGFILE); else fprintf(file,"fastback LOGFILE not set\n"); if (fastback_filename) fprintf(file,"fastback file: %s\n", fastback_filename); if (fastback_ticket) fprintf(file,"fastback ticket: %s\n", fastback_ticket); else { if (fastback_newticket) fprintf(file,"fastback new ticket explicitly set\n"); else fprintf(file,"fastback new ticket by default\n"); } if (fastback_encrypt) fprintf(file,"encrypt file\n"); else fprintf(file,"don't encrypt file\n"); fprintf(file,"fastback URL: %s\n", url.c_str()); } static void check_curl_error(CURLcode err, string url, const char* msg) { if (err) { show(stderr, url); string tmsg = fastback_name; tmsg += ": error: "; tmsg += msg; tmsg += ": "; tmsg += curl_easy_strerror(err); tmsg += "\n"; fprintf(stderr,tmsg.c_str()); exit(4); } } static error_t fastback_argp_parser (int key, char *arg, struct argp_state *state) { switch (key) { case ARGP_KEY_ARG: if (fastback_filename) argp_error(state,"multiple FILE arguments specified"); fastback_filename = strdup(arg); break; case 'e': fastback_encrypt = true; break; case 'n': if (fastback_ticket) argp_error(state, "invalid options: -n and -t conflict"); fastback_newticket = true; break; case 't': if (fastback_newticket) argp_error(state, "invalid options: -n and -t conflict"); else fastback_ticket = strdup(arg); break; case 'v': fastback_verbose = true; break; default: return ARGP_ERR_UNKNOWN; } return 0; } static struct argp_option fastback_options[] = { {"ticket",'t', "TICKET", 0, "the ticket to associate FILE with"}, {0,'n',0,0,"create a new ticket for FILE"}, {"encrypt",'e',0,0,"encrypt FILE before uploading"}, {0,'v',0,0,"be verbose"}, { 0 }}; static struct argp fastback_argp = { fastback_options, fastback_argp_parser, "FILE" }; struct config_option { const char* option; char** storage; }; config_option all_config_options[] = { { "URLDIR", &fastback_URLDIR }, { "LOGFILE", &fastback_conf_LOGFILE }, { 0, 0 } }; static bool config_line_parse(const char* filename, const char* line, size_t var_start, size_t var_end, size_t value_start, size_t value_end) { config_option* each; for (each = all_config_options; each->option; each++) { size_t option_length = strlen(each->option); if (option_length == (var_end - var_start) && memcmp(each->option,line+var_start,option_length) == 0) { int value_length = value_end - value_start; (*each->storage) = (char*)malloc(value_length + 1); memcpy((*each->storage), line+value_start, value_length); (*each->storage)[value_length] = 0; return false; } } { int var_length = var_end - var_start; char* var = (char*)malloc(var_length + 1); memcpy(var, line+var_start, var_length); var[var_length] = 0; fprintf(stderr,"%s: %s\n", filename, line); fprintf(stderr, "%s: error: unknown config option: %s\n", fastback_name, var); free(var); return true; } } static void error_regerror (int errcode, regex_t *compiled, const char* func) { size_t length = regerror (errcode, compiled, NULL, 0); char *buffer = (char*)malloc (length); (void) regerror (errcode, compiled, buffer, length); fprintf(stderr, "%s: error: %s: %s\n", fastback_name, func, buffer); free(buffer); } static bool parse_config(const char* filename) { int config_file_error = 0; int err; regex_t regexp; FILE* file; char *line; size_t linesize; ssize_t readcount; const char pattern[] = "^[[:space:]]*" "\\(\\|#.*\\|" "\\([[:alpha:]][[:alnum:]]*\\)[[:space:]]*=" "[[:space:]]*\\(\"\\([^\"]*\\)\"\\|\\([^[:space:]]*\\)\\)[[:space:]]*" "\\)$"; const bool debug_pattern = false; err = regcomp( ®exp, pattern, 0); if (err) { error_regerror(err, ®exp, "regcomp"); exit(4); } size_t matchsize = regexp.re_nsub + 1; regmatch_t* matchptr = (regmatch_t*)malloc(sizeof(regmatch_t) * matchsize); file = fopen(filename,"r"); if (!file) { string msg = fastback_name; msg += ": error: could not open: "; msg += filename; perror(msg.c_str()); exit(4); } line = 0; linesize = 0; readcount = getline(&line, &linesize, file); while (readcount != -1) { /* trim the newline */ if (readcount != 0 && line[readcount-1] == '\n') line[readcount-1] = 0; err = regexec( ®exp, line, matchsize, matchptr, 0); if (err == 0) { if (debug_pattern) { int i; fprintf(stderr,"%s: debug: line: %s: %s\n", fastback_name, filename, line); for (i=0; i < matchsize; i++) if (matchptr[i].rm_so == -1) fprintf(stderr,"%s: debug: match (%d): no match\n", fastback_name, i); else { int j; char buf[1000]; for(j=0; j < (matchptr[i].rm_eo - matchptr[i].rm_so); j++) buf[j] = line[matchptr[i].rm_so + j]; buf[j] = 0; if (j >= 1000) { fprintf(stderr,"INTERNAL ERROR: J too large\n"); exit(4); } fprintf(stderr, "%s: debug: match (%d): \"%s\"\n", fastback_name, i, buf); } } if (matchptr[2].rm_so != -1) { if (matchptr[4].rm_so != -1) config_file_error |= config_line_parse(filename, line, matchptr[2].rm_so, matchptr[2].rm_eo, matchptr[4].rm_so, matchptr[4].rm_eo); else if (matchptr[5].rm_so != -1) config_file_error |= config_line_parse(filename, line, matchptr[2].rm_so, matchptr[2].rm_eo, matchptr[5].rm_so, matchptr[5].rm_eo); } } else if (err == REG_NOMATCH) { fprintf(stderr,"%s: %s\n", filename, line); fprintf(stderr,"%s: error: invalid config line\n", fastback_name); config_file_error = true; } else { fprintf(stderr,"%s: %s\n", filename, line); error_regerror(err, ®exp, "regexe"); exit(4); } readcount = getline(&line, &linesize, file); } fclose(file); return config_file_error; } void Error(string func, string msg) { std::cerr << func << msg << std::endl; exit(4); } void RunCommand(string cmd) { int retcode = system(cmd.c_str()); if (retcode == -1) { Error("RunCommand:", "error: could not start subshell: " + cmd); } if (retcode) { std::ostringstream msg; msg << "error: subshell failed (rc=" << retcode << "):" << cmd; Error("RunCommand:", msg.str()); } } string ReadCommand(string cmd) { FILE* fp = popen(cmd.c_str(),"r"); if (!fp) { Error("ReadCommand:", "error: could not start subshell: " + cmd); } __gnu_cxx::stdio_filebuf command_output_buffer(fp, std::ios_base::in); std::ostringstream output_stream; output_stream << &command_output_buffer; int retcode = pclose(fp); if (retcode) { std::ostringstream msg; msg << "error: subshell failed (rc=" << retcode << "):" << cmd; Error("ReadCommand:", msg.str()); } return output_stream.str(); } void WriteCommand(string cmd, string input) { FILE* fp = popen(cmd.c_str(),"w"); if (!fp) { Error("WriteCommand:", "error: could not start subshell: " + cmd); } size_t input_length = input.length(); size_t check = fwrite(input.c_str(),1,input_length,fp); if (input_length != check) { Error("WriteCommand:", "error: could not send input to subshell: " + cmd); } int retcode = pclose(fp); if (retcode) { std::ostringstream msg; msg << "error: subshell failed (rc=" << retcode << "):" << cmd; Error("WriteCommand:", msg.str()); } } static string rand_base64(int i) { // return a string of random base64 characters of 'i' length // // This should probably use the openssl library (or some other // library directly, but it doesn't right now std::ostringstream cmd; cmd << "openssl rand -base64 " << i; string r = ReadCommand(cmd.str()); return r.substr(0,r.length()-1); } static string randomize_filename(string filename) { // create (max_rand_bytes * 8) random bits encoded as base64 // add those bits to the filename before the first dot if any. const int max_rand_bytes = 3; // 24 random bits string::size_type p = filename.find_first_of('.'); string r = rand_base64(max_rand_bytes); if (p == string::npos) return filename + '-' + r; else return filename.substr(0,p) + '-' + r + filename.substr(p,string::npos); } static bool is_compressed(const string& filename) { string cmd = string("file ") + filename; string output = ReadCommand(cmd); return output.find("compressed") != string::npos; } static void compress(const string& in_filename, const string& out_filename) { // compress file 'in_filename' into 'out_filename' // again this should probably use a library, but doesn't yet string cmd = string("gzip <") + in_filename + " >" + out_filename; ReadCommand(cmd); } static bool readable(string filename) { // is 'filename' readable by the running user FILE* file = fopen(filename.c_str(),"r"); if (file) { fclose(file); return true; } return false; } static string filename_basename(const string& filename) { string::size_type p = filename.find_last_of('/'); if (p == string::npos) return filename; else return filename.substr(p+1,string::npos); } static string filename_dirname(const string& filename) { string::size_type p = filename.find_last_of('/'); if (p == string::npos) return "."; else return filename.substr(0,p); } static string filename_temporary(string filename) { // ensure that a temporary directory (for this run) exists // 'filename' must be the basename of a file // 'filename' must be unique to this run if (fastback_tmpdir == "") { char TEMPLATE[] = "/tmp/fastbackXXXXXX"; fastback_tmpdir = mkdtemp(TEMPLATE); } return fastback_tmpdir + '/' + filename; } static void cleanup() { free(fastback_filename); free(fastback_ticket); free(fastback_URLDIR); free(fastback_conf_LOGFILE); if (fastback_tmpdir != "") RunCommand(string("rm -rf " + fastback_tmpdir)); if (fastback_logfile) fclose(fastback_logfile); } int main(int argc, char** argv) { error_t err; bool use_scp_command = false; err = argp_parse( &fastback_argp, argc, argv, 0, 0, 0); if (err) { if (errno == err) { string msg = fastback_name; msg += ": error: argp_parse"; perror(msg.c_str()); } else fprintf(stderr, "%s: error from argp_parse: error code %d\n", fastback_name, err); cleanup(); exit(2); } if (!fastback_filename) { fprintf(stderr, "%s: error: no FILE given on command line\n", fastback_name); fprintf(stderr, "Try `%s --help' or `fastback --usage' for more information.\n", fastback_name); exit(2); } if (parse_config(fastback_config_file)) exit(2); if (fastback_conf_LOGFILE) { fastback_logfile = fopen(fastback_conf_LOGFILE,"a"); if (!fastback_logfile) { string msg = fastback_name; msg += ": error: could not open logfile: "; msg += fastback_conf_LOGFILE; perror(msg.c_str()); exit(3); } } if (!fastback_URLDIR || !strcmp(fastback_URLDIR,"")) { fprintf(stderr, "%s: error: URLDIR not set in: %s\n", fastback_name, fastback_config_file); exit(3); } if (!readable(fastback_filename)) { string msg = fastback_name; msg += ": error: could not read: "; msg += fastback_filename; perror(msg.c_str()); exit(3); } string outfile_name = fastback_filename; // compress the file if it isn't already if (!is_compressed(outfile_name)) { string compressed_filename = filename_temporary(filename_basename(outfile_name) + ".gz"); compress(outfile_name,compressed_filename); outfile_name = compressed_filename; } // encrypt if requested string key; if (fastback_encrypt) { string cmd = string("openssl rand -base64 48"); key = ReadCommand(cmd); string infile_name = outfile_name; outfile_name = filename_temporary(filename_basename(outfile_name) + ".aes"); cmd = string("openssl aes-128-cbc -in ") + infile_name + " -out " + outfile_name + " -pass stdin"; WriteCommand(cmd,key); } // generate md5sum string cmd = string("md5sum ") + outfile_name; string md5sum = ReadCommand(cmd); // randomize the remote file name // and alter the md5sum output to contain the remote file name string remotefile_name = randomize_filename(filename_basename(outfile_name.c_str())); md5sum = md5sum.substr(0,md5sum.find_first_of(' ')) + " " + remotefile_name + '\n'; // create the full remote file name, 'url' string url = fastback_URLDIR; if (url[url.length()-1] != '/') url += '/'; url += remotefile_name; if (fastback_verbose) show(stdout,url); if (fastback_logfile) show(fastback_logfile,url); if (url.substr(0,4) == "scp:") use_scp_command = true; if (use_scp_command) { if (url.substr(0,6) != "scp://") { fprintf(stderr, "%s: error: invalid scp URL, does not start with 'scp://': %s\n", fastback_name, url.c_str()); exit(3); } size_t urlend = url.length(); size_t hostend = url.find_first_of('/',6); string host = url.substr(6,hostend-6); const char* replacement; if (hostend+1 >= urlend || url[hostend+1] != '~') replacement = ":/"; else replacement = ":"; string cmd = string("scp ") + outfile_name + ' ' + host + replacement + url.substr(hostend+1); if (fastback_logfile) fprintf(fastback_logfile,"$ %s\n", cmd.c_str()); RunCommand(cmd); } else { CURL* handle; CURLcode curl_err; FILE* file; file = fopen(outfile_name.c_str(),"r"); if (!file) { string msg = fastback_name; msg += ": error: could not open: "; msg += outfile_name; perror(msg.c_str()); exit(3); } if (curl_global_init(CURL_GLOBAL_ALL)) { fprintf(stderr,"%s: error: curl_global_init: could not initialze curl\n", fastback_name); exit(3); } handle = curl_easy_init(); if (!handle) { fprintf(stderr,"%s: error: curl_easy_init: could not initialize curl\n", fastback_name); exit(3); } if (fastback_logfile) { curl_err = curl_easy_setopt(handle, CURLOPT_VERBOSE, 1); check_curl_error(curl_err, url, "curl_easy_setopt(CURLOPT_VERBOSE)"); curl_err = curl_easy_setopt(handle, CURLOPT_STDERR, fastback_logfile); check_curl_error(curl_err, url, "curl_easy_setopt(CURLOPT_STDERR)"); } curl_err = curl_easy_setopt(handle, CURLOPT_URL, url.c_str()); check_curl_error(curl_err, url, "curl_easy_setopt(CURLOPT_URL)"); curl_err = curl_easy_setopt(handle, CURLOPT_READFUNCTION, fastback_read); check_curl_error(curl_err, url, "curl_easy_setopt(CURLOPT_READFUNCTION)"); curl_err = curl_easy_setopt(handle, CURLOPT_READDATA, (void*)file); check_curl_error(curl_err, url, "curl_easy_setopt(CURLOPT_READDATA)"); curl_err = curl_easy_setopt(handle, CURLOPT_UPLOAD, 1L); check_curl_error(curl_err, url, "curl_easy_setopt(CURLOPT_READDATA)"); curl_err = curl_easy_perform(handle); check_curl_error(curl_err, url, "curl_easy_perform"); curl_easy_cleanup(handle); fclose(file); } if (fastback_ticket) printf("Please copy this into %s:\n", fastback_ticket); else printf("Please send this to your technical support:\n"); printf("FASTBACK: This report was sent to %s\n", filename_dirname(url).c_str()); if (fastback_ticket) printf("TICKET: %s\n", fastback_ticket); printf("FILE: %s\n", filename_basename(url).c_str()); printf("MD5SUM:\n"); printf("%s", md5sum.c_str()); if (fastback_encrypt) { printf("KEY: aes-128-cbc\n"); printf("%s", key.c_str()); } printf("END:\n"); cleanup(); return 0; }