// // aegis - project change supervisor // Copyright (C) 2003-2008 Peter Miller // // 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 3 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, see // . // #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_LIBCURL #define FATAL(function, reason) \ fatal_raw("%s: %d: " function ": %s", __FILE__, __LINE__, reason); // // If there is more than one URL open at a time, all are processed // in parallel. The multi-handle aggregates them all. // static CURLM *multi_handle; static bool call_multi_immediate; static itab_ty *stp; input_curl::~input_curl() { // // Release libcurl resources. // curl_multi_remove_handle(multi_handle, handle); curl_easy_cleanup(handle); handle = 0; eof = true; if (progress_cleanup) { write(2, "\n", 1); progress_cleanup = 0; } // // Release dynamic memory resources. // delete [] curl_buffer; curl_buffer = 0; curl_buffer_position = 0; curl_buffer_length = 0; curl_buffer_maximum = 0; } static int progress_callback(void *p, double dt, double dc, double, double) { input_curl *icp = (input_curl *)p; icp->progress_callback(dt, dc); return 0; } // // Libcurl calls this function when it receives more data. // static size_t write_callback(char *data, size_t size, size_t nitems, void *p) { input_curl *icp = (input_curl *)p; size_t nbytes = size * nitems; return icp->write_callback(data, nbytes); } input_curl::input_curl(const nstring &arg) : fn(arg), pos(0), curl_buffer(0), curl_buffer_maximum(0), curl_buffer_position(0), curl_buffer_length(0), eof(false) { handle = curl_easy_init(); if (!handle) nfatal("curl_easy_init"); CURLcode err = curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, errbuf); if (err) FATAL("curl_easy_setopt", curl_easy_strerror(err)); #if (LIBCURL_VERSION_NUM < 0x070b01) // // libcurl prior to 7.11.1 has problems handling autenticated // proxy specified by http_proxy or HTTP_PROXY, so we set them // manually. // int uid; int gid; int umask; // // We need to save the user identity because the url::split method // call os_become_ itself and we must issue os_become_undo and // os_become to not raise a multiple permission error. // os_become_query(&uid, &gid, &umask); os_become_undo(); url target_url(fn); os_become(uid, gid, umask); if (target_url.get_protocol() == "http") { char *http_proxy = getenv("http_proxy"); if (!http_proxy || http_proxy[0] == '\0') http_proxy = getenv("HTTP_PROXY"); if (http_proxy && http_proxy[0] != '\0') { // // We use the user's identity previously saved to // undo/restore the process identity in order to prevent a // multiple permission error from url::split. // os_become_undo(); url proxy_url(http_proxy); os_become(uid, gid, umask); userpass = proxy_url.get_userpass(); proxy = proxy_url.reassemble(true); if (!userpass.empty()) { curl_easy_setopt ( handle, CURLOPT_PROXYUSERPWD, userpass.c_str() ); } curl_easy_setopt(handle, CURLOPT_PROXY, proxy.c_str()); } } #endif err = curl_easy_setopt(handle, CURLOPT_URL, fn.c_str()); if (err) FATAL("curl_easy_setopt", curl_easy_strerror(err)); err = curl_easy_setopt(handle, CURLOPT_FILE, this); if (err) FATAL("curl_easy_setopt", curl_easy_strerror(err)); err = curl_easy_setopt(handle, CURLOPT_VERBOSE, 0); if (err) FATAL("curl_easy_setopt", curl_easy_strerror(err)); err = curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, ::write_callback); if (err) FATAL("curl_easy_setopt", curl_easy_strerror(err)); err = curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1); if (err) FATAL("curl_easy_setopt", curl_easy_strerror(err)); progress_start = 0; progress_buflen = 0; progress_buffer = 0; progress_cleanup = 0; if (option_verbose_get()) { err = curl_easy_setopt(handle, CURLOPT_NOPROGRESS, 0); if (err) FATAL("curl_easy_setopt", curl_easy_strerror(err)); err = curl_easy_setopt ( handle, CURLOPT_PROGRESSFUNCTION, ::progress_callback ); if (err) FATAL("curl_easy_setopt", curl_easy_strerror(err)); err = curl_easy_setopt(handle, CURLOPT_PROGRESSDATA, this); if (err) FATAL("curl_easy_setopt", curl_easy_strerror(err)); time(&progress_start); progress_buflen = page_width_get(80); if (progress_buflen < 40) progress_buflen = 40; progress_buffer = new char [progress_buflen]; } if (!multi_handle) { multi_handle = curl_multi_init(); if (!multi_handle) nfatal("curl_multi_init"); } CURLMcode merr = curl_multi_add_handle(multi_handle, handle); switch (merr) { case CURLM_CALL_MULTI_PERFORM: call_multi_immediate = true; break; case CURLM_OK: break; default: FATAL("curl_multi_add_handle", curl_multi_strerror(merr)); } // // Start the fetch as soon as possible. // call_multi_immediate = true; // // Build an associate table from libcurl handles to our file pointers. // if (!stp) stp = itab_alloc(); itab_assign(stp, (itab_key_ty)handle, (void *)this); } static void print_byte_count(char *buf, size_t len, double number) { if (number < 0) { snprintf(buf, len, "-----"); return; } // K is Kelvin, k is kilo const char *units = " kMGTPEZY"; for (;;) { if (*units != ' ') { if (number < 10) { snprintf(buf, len, "%4.2f%cB", number, *units); return; } if (number < 100) { snprintf(buf, len, "%4.1f%cB", number, *units); return; } } if (number < (1<<10)) { snprintf(buf, len, "%4d%cB", (int)number, *units); return; } number /= 1024.; ++units; } } void input_curl::progress_callback(double down_total, double down_current) { if (down_current <= 0 || down_total <= 0) return; if (down_current >= down_total && !progress_cleanup) return; time_t curtim; time(&curtim); curtim -= progress_start; char buf1[7]; print_byte_count(buf1, sizeof(buf1), (long)down_current); char buf2[7]; print_byte_count(buf2, sizeof(buf2), (long)down_total); double frac = (down_total <= 0) ? 0 : (down_current / down_total); time_t predict = (time_t)(frac ? (0.5 + curtim / frac) : 0); time_t remaining = predict - curtim; char buf3[7]; format_elapsed(buf3, sizeof(buf3), remaining); memset(progress_buffer, ' ', progress_buflen); memcpy(progress_buffer + 0, buf1, 6); memcpy(progress_buffer + 6, " of ", 4); memcpy(progress_buffer + 10, buf2, 6); snprintf(progress_buffer + 17, 5, "%3d%%", (int)(100 * frac + 0.5)); int lhs = 23; int rhs = (int)(lhs + (progress_buflen - 37) * frac); while (lhs < rhs) progress_buffer[lhs++] = '='; progress_buffer[lhs] = '>'; memcpy(progress_buffer + progress_buflen - 11, "ETA", 3); memcpy(progress_buffer + progress_buflen - 7, buf3, 6); progress_buffer[progress_buflen - 1] = '\r'; write(2, progress_buffer, progress_buflen); progress_cleanup = 1; if (down_current >= down_total) { write(2, "\n", 1); progress_cleanup = 0; } } size_t input_curl::write_callback(char *data, size_t nbytes) { // // Grow the buffer if necessary. // // Always keep it a power of 2, because sigma(2**-n)==1, so we get // O(1) behaviour. (That +32 means we are always just 32 bytes // short of a power of 2, leaving room for the malloc header, which // results in a nicer malloc fit on many systems. // if (curl_buffer_length + nbytes > curl_buffer_maximum) { for (;;) { curl_buffer_maximum = curl_buffer_maximum * 2 + 32; if (curl_buffer_length + nbytes <= curl_buffer_maximum) break; } char *new_curl_buffer = new char [curl_buffer_maximum]; memcpy(new_curl_buffer, curl_buffer, curl_buffer_length); delete [] curl_buffer; curl_buffer = new_curl_buffer; } // // Copy the data into the buffer. // memcpy(curl_buffer + curl_buffer_length, data, nbytes); curl_buffer_length += nbytes; // // A negative return will stop the transfer for this stream. // return nbytes; } static input_curl * handle_to_fp(CURL *handle) { assert(stp); input_curl *result = (input_curl *)itab_query(stp, (itab_key_ty)handle); if (!result || !result->verify_handle(handle)) { fatal_raw ( "%s: %d: handle %p gave file %p", __FILE__, __LINE__, (void *)handle, (void *)result ); } return result; } /** * The perform function is a wrapper around the curl_multi_perform * function. It checks for messages that may be waiting, waits in * select if necessary, and calls curl_multi_perform eventually. * * It is expected that this function will be repeatedly called from a * tight loop, so it doesn't loop itself. */ static void perform(void) { // // See if there are any messages waiting. // These tell us about errors, and completed transfers. // for (;;) { int msgs = 0; CURLMsg *msg = curl_multi_info_read(multi_handle, &msgs); if (!msg) break; if (msg->msg == CURLMSG_NONE) break; if (msg->msg != CURLMSG_DONE) fatal_raw("curl_multi_info_read -> %d (bug)", msg->msg); input_curl *fp = handle_to_fp(msg->easy_handle); if (msg->data.result == 0) { // transfer over, no error fp->eof_notify(); } else { fp->read_error(); } } // // Look for more to happen. // if (call_multi_immediate) { call_multi_immediate = false; for (;;) { int num_xfer = 0; CURLMcode ret = curl_multi_perform(multi_handle, &num_xfer); switch (ret) { case CURLM_CALL_MULTI_PERFORM: call_multi_immediate = true; return; case CURLM_OK: return; default: error_raw ( "%s: %d: curl_multi_perform: %s", __FILE__, __LINE__, curl_multi_strerror(ret) ); } } } else { fd_set fdread; FD_ZERO(&fdread); fd_set fdwrite; FD_ZERO(&fdwrite); fd_set fdexcep; FD_ZERO(&fdexcep); // get file descriptors from the transfers int maxfd = 0; CURLcode err = (CURLcode) curl_multi_fdset(multi_handle, &fdread, &fdwrite, &fdexcep, &maxfd); if (err) FATAL("curl_multi_fdset", curl_easy_strerror(err)); if (maxfd >= 0) { // set a suitable timeout to fail on struct timeval timeout; timeout.tv_sec = 60; // 1 minute timeout.tv_usec = 0; int rc = select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout); if (rc < 0) { if (errno != EINTR) { nfatal ( "%s: %d: select: %s", __FILE__, __LINE__, strerror(errno) ); // NOTREACHED } } if (rc > 0) { // // Some sockets are ready. // call_multi_immediate = true; } } } } void input_curl::read_error() { sub_context_ty sc; sc.var_set_string("File_Name", fn); sc.var_set_charstar("ERRNO", errbuf); sc.var_override("ERRNO"); sc.fatal_intl(i18n("read $filename: $errno")); // NOTREACHED } /** * The read_data function is used to read data into the data buffer provided. * Returns the number of bytes read. */ long input_curl::read_data(void *data, size_t nbytes) { // // attempt to fill buffer // while (!eof && curl_buffer_position + nbytes > curl_buffer_length) perform(); // // Extract as much data as possible from the buffer. // size_t size_of_buffer = curl_buffer_length - curl_buffer_position; if (nbytes > size_of_buffer) nbytes = size_of_buffer; memcpy(data, curl_buffer + curl_buffer_position, nbytes); curl_buffer_position += nbytes; // // Rearrange the buffer so that it does not grow forever. // size_of_buffer = curl_buffer_length - curl_buffer_position; if (size_of_buffer == 0) { curl_buffer_position = 0; curl_buffer_length = 0; } else if (size_of_buffer <= curl_buffer_position) { // can shuffle the data down easily memcpy(curl_buffer, curl_buffer + curl_buffer_position, size_of_buffer); curl_buffer_position = 0; curl_buffer_length = size_of_buffer; } if (nbytes == 0 && progress_cleanup) { write(2, "\n", 1); progress_cleanup = 0; } // // Return the number of bytes read. // return nbytes; } long input_curl::read_inner(void *data, size_t len) { os_become_must_be_active(); long result = read_data(data, len); assert(result >= 0); pos += result; return result; } long input_curl::ftell_inner() { return pos; } nstring input_curl::name() { return fn; } long input_curl::length() { // Maybe there was a Content-Length header? return -1; } #else input_curl::~input_curl() { } input_curl::input_curl(const nstring &arg) : fn(arg) { sub_context_ty sc; sc.var_set_string("FileLine", fn); sc.fatal_intl(i18n("open $filename: no curl library")); } long input_curl::read_inner(void *, size_t) { return 0; } long input_curl::ftell_inner() { return 0; } nstring input_curl::name() { return fn; } long input_curl::length() { return -1; } #endif // HAVE_LIBCURL bool input_curl::looks_likely(const nstring &file_name) { const char *cp = file_name.c_str(); if (!isalpha((unsigned char)*cp)) return 0; for (;;) { ++cp; if (!isalpha((unsigned char)*cp)) break; } return (cp[0] == ':' && cp[1] != '\0'); } bool input_curl::is_remote() const { return true; }