/* 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"; // 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 bool fastback_encrypt = false; // encrypt file before upload // '-n' option explicitly set static bool fastback_newticket = false; static bool fastback_verbose = false; static bool fastback_logging = false; // 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 const char* fastback_default_URLDIR = "ftp://dropbox.redhat.com/incoming/"; static const char* fastback_default_URLDIR = "ftp://indus.usersys.redhat.com/incoming"; 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_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; case 'l': fastback_logging = 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"}, {"logging",'l',0,0,"print logfile to stderr"}, { 0 }}; static struct argp fastback_argp = { fastback_options, fastback_argp_parser, "FILE" }; static bool option_parse(const char* filename, const char* line, size_t var_start, size_t var_end, size_t value_start, size_t value_end) { bool config_file_error = false; const char option1[] = "URLDIR"; size_t option_length = strlen(option1); if (option_length == (var_end - var_start) && memcmp(option1,line+var_start,option_length) == 0) { int value_length = value_end - value_start; fastback_URLDIR = (char*)malloc(value_length + 1); memcpy(fastback_URLDIR, line+value_start, value_length); fastback_URLDIR[value_length] = 0; } else { 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); config_file_error = true; free(var); } return config_file_error; } 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 |= option_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 |= option_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(); } 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) { return basename(filename.c_str()); } static string create_temporary_file(string filename) { // create a filename in a temporary directory // for now you must make sure that the name is 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); if (fastback_tmpdir != "") RunCommand(string("rm -rf " + fastback_tmpdir)); } int main(int argc, char** argv) { error_t err; 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.conf")) exit(2); if (!fastback_URLDIR) fastback_URLDIR = strdup(fastback_default_URLDIR); if (!readable(fastback_filename)) { string msg = fastback_name; msg += ": error: could not open: "; msg += fastback_filename; perror(msg.c_str()); exit(3); } string final_filename = fastback_filename; if (!is_compressed(final_filename)) { string compressed_filename = create_temporary_file(filename_basename(final_filename) + ".gz"); compress(final_filename,compressed_filename); final_filename = compressed_filename; } CURL* handle; CURLcode curl_err; FILE* file; string url; file = fopen(final_filename.c_str(),"r"); if (!file) { string msg = fastback_name; msg += ": error: could not open: "; msg += final_filename; perror(msg.c_str()); exit(3); } url = fastback_URLDIR; if (url[url.length()-1] != '/') url += '/'; url += randomize_filename(filename_basename(final_filename.c_str())); if (fastback_verbose) show(stdout,url); if (fastback_logging) show(stderr,url); 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); } // note that it is the fastback logging option which invokes curl verbose curl_err = curl_easy_setopt(handle, CURLOPT_VERBOSE, fastback_logging); check_curl_error(curl_err, url, "curl_easy_setopt(CURLOPT_VERBOSE)"); 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 paste this into %s:\n", fastback_ticket); else printf("Please create a new ticket, and paste this into it:\n"); printf("FASTBACK:\n"); if (fastback_ticket) printf("TICKET: %s\n", fastback_ticket); printf("UPLOAD: %s\n", url.c_str()); printf("END:\n"); cleanup(); return 0; }