// This file is part of BOINC.
// http://boinc.berkeley.edu
// Copyright (C) 2008 University of California
//
// BOINC is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License
// as published by the Free Software Foundation,
// either version 3 of the License, or (at your option) any later version.
//
// BOINC 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with BOINC.  If not, see <http://www.gnu.org/licenses/>.

#include "cpp.h"

#ifdef _WIN32
#include "boinc_win.h"
#else
#include "config.h"
#include <cstring>
#include <sstream>
#include <algorithm>
#include <sys/stat.h>
#include <cerrno>
#include <unistd.h>
#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif
#endif

#include "error_numbers.h"
#include "filesys.h"
#include "client_msgs.h"
#include "log_flags.h"
#include "str_util.h"
#include "util.h"

#include "network.h"
#include "file_names.h"
#include "client_msgs.h"
#include "base64.h"
#include "http_curl.h"
#include "client_state.h"

using std::min;
using std::vector;

static CURLM* g_curlMulti = NULL;

static char g_user_agent_string[256] = {""};
static const char g_content_type[] = {"Content-Type: application/x-www-form-urlencoded"};


// Breaks a HTTP URL down into its server, port and file components
// format of url:
// [http[s]://]host.dom.dom[:port][/dir/file]
//
void parse_url(const char* url, char* host, int &port, char* file) {
    char* p;
    char buf[256];
    bool bSSL = false;

    //const short csiLen = bSSL ? 8 : 7; // get right size of http/s string comparator

    // strip off http:// if present
    //
    //if (strncmp(url, (bSSL ? "https://" : "http://"), csiLen) == 0) {
    if (strncmp(url, "http://", 7) == 0) {
        safe_strcpy(buf, url+7);
    } else { 
        // wait, may be https://
        if (strncmp(url, "https://", 8) == 0) {
            safe_strcpy(buf, url+8);
            bSSL = true; // retain the fact that this was a secure http url
        }
        else { // no http:// or https:// prepended on url
            safe_strcpy(buf, url);
        }
    }

    // parse and strip off file part if present
    //
    p = strchr(buf, '/');
    if (p) {
        strcpy(file, p+1);
        *p = 0;
    } else {
        strcpy(file, "");
    }

    // parse and strip off port if present
    //
    p = strchr(buf,':');
    if (p) {
        port = atol(p+1);
        *p = 0;
    } else {
        // CMC note:  if they didn't pass in a port #, 
        //    but the url starts with https://, assume they
        //    want a secure port (HTTPS, port 443)
        port = (bSSL ? 443 : 80);  
    }

    // what remains is the host
    //
    strcpy(host, buf);
}

void get_user_agent_string() {
    sprintf(g_user_agent_string, "BOINC client (%s %d.%d.%d)",
        gstate.get_primary_platform(),
        BOINC_MAJOR_VERSION, BOINC_MINOR_VERSION, BOINC_RELEASE
    );
}

void HTTP_OP::init() {
    reset();
    start_time = gstate.now;
	start_bytes_xferred = 0;
}

void HTTP_OP::reset() {
    req1 = NULL;
    strcpy(infile, "");
    strcpy(outfile, "");
    strcpy(error_msg, "");
    CurlResult = CURLE_OK;
    bTempOutfile = true;
    want_download = false;
    want_upload = false;
    fileIn = NULL;
    fileOut = NULL;
    connect_error = 0;
    bytes_xferred = 0;
    bSentHeader = false;
    close_socket();
}


HTTP_OP::HTTP_OP() {
    strcpy(m_url, "");
    strcpy(m_curl_ca_bundle_location, "");
    content_length = 0;
    file_offset = 0;
    strcpy(request_header, "");
    http_op_state = HTTP_STATE_IDLE;
    http_op_type = HTTP_OP_NONE;
    http_op_retval = 0;
    trace_id = 0;
    pcurlList = NULL; // these have to be NULL, just in constructor
    curlEasy = NULL;
    pcurlFormStart = NULL;
    pcurlFormEnd = NULL;
    pByte = NULL;
    lSeek = 0;
    xfer_speed = 0;
    reset();
}

HTTP_OP::~HTTP_OP() {
    close_socket();
    close_file();
}

// Initialize HTTP GET operation;
// output goes to the given file, starting at given offset
//
int HTTP_OP::init_get(
    const char* url, const char* out, bool del_old_file, double off
) {
    if (del_old_file) {
        unlink(out);
    }
    req1 = NULL;  // not using req1, but init_post2 uses it
    file_offset = off;
    HTTP_OP::init();
    // usually have an outfile on a get
    if (off != 0) {
        bytes_xferred = off;
		start_bytes_xferred = off;
    }
    http_op_type = HTTP_OP_GET;
    http_op_state = HTTP_STATE_CONNECTING;
    if (log_flags.http_debug) {
        msg_printf(0, MSG_INFO, "[http_debug] HTTP_OP::init_get(): %s", url);
    }
    return HTTP_OP::libcurl_exec(url, NULL, out, off, false);
}

// Initialize HTTP POST operation where
// the input is a file, and the output is a file,
// and both are read/written from the beginning (no resumption of partial ops)
// This is used for scheduler requests and account mgr RPCs.
//
int HTTP_OP::init_post(
    const char* url, const char* in, const char* out
) {
    int retval;
    double size;

    req1 = NULL;  // not using req1, but init_post2 uses it

    if (in) {
        strcpy(infile, in);
        retval = file_size(infile, size);
        if (retval) return retval;  // this will return 0 or ERR_NOT_FOUND
        content_length = (int)size;
    }
    HTTP_OP::init();
    http_op_type = HTTP_OP_POST;
    http_op_state = HTTP_STATE_CONNECTING;
    if (log_flags.http_debug) {
        msg_printf(0, MSG_INFO, "[http_debug] HTTP_OP::init_post(): %s", url);
    }
    return HTTP_OP::libcurl_exec(url, in, out, 0.0, true);
}

// Initialize an HTTP POST operation,
// where the input is a memory string (r1) followed by an optional file (in)
// with optional offset,
// and the output goes to memory (also r1)
// This is used for file upload (both get_file_size and file_upload)
//
int HTTP_OP::init_post2(
    const char* url, char* r1, int r1_len, const char* in, double offset
) {
    int retval;
    double size;

    init();
    req1 = r1;
    req1_len = r1_len;
    if (in) {
        safe_strcpy(infile, in);
        file_offset = offset;
        retval = file_size(infile, size);
        if (retval) {
            printf("HTTP::init_post2: couldn't get file size\n");
            return retval; // this will be 0 or ERR_NOT_FOUND
        }
        content_length = (int)size - (int)offset;
    }
    content_length += (int)strlen(req1);
    http_op_type = HTTP_OP_POST2;
    http_op_state = HTTP_STATE_CONNECTING;
    return HTTP_OP::libcurl_exec(url, in, NULL, offset, true);
}

// is URL in proxy exception list?
//
bool HTTP_OP::no_proxy_for_url(const char* url) {
	if (log_flags.proxy_debug) {
        msg_printf(0, MSG_INFO, "[proxy_debug] HTTP_OP::no_proxy_for_url(): %s", url);
    }
	char hosturl[256];
	char hostnoproxy[256];	
	char file[256];
	char noproxy[256];
	int port;

	// extract the host from the url
	parse_url(url,hosturl,port,file);

	// tokenize the noproxy-entry and check for identical hosts
    //
	strcpy(noproxy,pi.noproxy_hosts);
	char* token = strtok(noproxy,",");
	while(token!= NULL) {
		// extract the host from the no_proxy url
		parse_url(token,hostnoproxy,port,file);
		if (!strcmp(hostnoproxy,hosturl)) {
			if (log_flags.proxy_debug) {
				msg_printf(0, MSG_INFO, "[proxy_debug] disabling proxy for %s",url);
			}
			return true;
		}
		token = strtok(NULL,",");
	}
	if (log_flags.proxy_debug) {
		msg_printf(0, MSG_INFO, "[proxy_debug] returning false");
	}
	return false;
}

// the following will do an HTTP GET or POST using libcurl
//
int HTTP_OP::libcurl_exec(
    const char* url, const char* in, const char* out, double offset, bool bPost
) {
    CURLMcode curlMErr;
    CURLcode curlErr;
    char strTmp[128];
    static int outfile_seqno=0;

    safe_strcpy(m_url, url);

    if (g_user_agent_string[0] == 0x00) {
        get_user_agent_string();
    }

    if (in) {
        strcpy(infile, in);
    }
    if (out) {
        bTempOutfile = false;
        strcpy(outfile, out);
    } else {
        // always want an outfile for the server response, delete when op done
        bTempOutfile = true;
        sprintf(outfile, "http_temp_%d", outfile_seqno++);
    }

    curlEasy = curl_easy_init(); // get a curl_easy handle to use
    if (!curlEasy) {
        msg_printf(0, MSG_INTERNAL_ERROR, "Couldn't create curlEasy handle");
        return ERR_HTTP_ERROR; // returns 0 (CURLM_OK) on successful handle creation
    }

	// the following seems to be a no-op
	//curlErr = curl_easy_setopt(curlEasy, CURLOPT_ERRORBUFFER, error_msg);

    curlErr = curl_easy_setopt(curlEasy, CURLOPT_URL, m_url);


    // This option determines whether curl verifies that the server
    // claims to be who you want it to be.
    // When negotiating an SSL connection,
    // the server sends a certificate indicating its identity.
    // When CURLOPT_SSL_VERIFYHOST is 2,
    // that certificate must indicate that the server is the server
    // to which you meant to connect, or the connection fails.
    // Curl considers the server the intended one when the
    // Common Name field or a Subject Alternate Name field in the certificate
    // matches the host name in the URL to which you told Curl to connect.
    // When the value is 1, the certificate must contain a Common Name field,
    // but it doesn't matter what name it says.
    // (This is not ordinarily a useful setting).
    // When the value is 0, the connection succeeds
    // regardless of the names in the certificate.
    // The default, since 7.10, is 2.
    // The checking this option controls is of the identity that
    // the server claims. The server could be lying.
    // To control lying, see CURLOPT_SSL_VERIFYPEER. 
    //
    curlErr = curl_easy_setopt(curlEasy, CURLOPT_SSL_VERIFYHOST, 2L);

    // the following sets "tough" certificate checking
    // (i.e. whether self-signed is OK)
    // if zero below, will accept self-signed certificates
    // (cert not 3rd party trusted)
    // if non-zero below, you need a valid 3rd party CA (i.e. Verisign, Thawte)
    //
    curlErr = curl_easy_setopt(curlEasy, CURLOPT_SSL_VERIFYPEER, 1L);

    // if the above is nonzero, you need the following:
    //
#ifdef _WIN32
    if (strlen(m_curl_ca_bundle_location) == 0) {
        TCHAR szPath[MAX_PATH-1];
        GetModuleFileName(NULL, szPath, (sizeof(szPath)/sizeof(TCHAR)));

        TCHAR *pszProg = strrchr(szPath, '\\');
        if (pszProg) {
            szPath[pszProg - szPath + 1] = 0;

            strncat(
                m_curl_ca_bundle_location,
                szPath, 
                sizeof(m_curl_ca_bundle_location)-strlen(m_curl_ca_bundle_location)
            );
            strncat(
                m_curl_ca_bundle_location,
                CA_BUNDLE_FILENAME, 
                sizeof(m_curl_ca_bundle_location)-strlen(m_curl_ca_bundle_location)
            );

            if (log_flags.http_debug) {
                msg_printf(
                    0,
                    MSG_INFO,
                    "[http_debug] HTTP_OP::libcurl_exec(): ca-bundle '%s'",
                    m_curl_ca_bundle_location
                );
            }
        }
    }
    if (boinc_file_exists(m_curl_ca_bundle_location)) {
        // call this only if a local copy of ca-bundle.crt exists;
        // otherwise, let's hope that it exists in the default place
        //
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_CAINFO, m_curl_ca_bundle_location);
        if (log_flags.http_debug) {
            msg_printf(
                0,
                MSG_INFO,
                "[http_debug] HTTP_OP::libcurl_exec(): ca-bundle set"
            );
        }
    }
#else
    if (boinc_file_exists(CA_BUNDLE_FILENAME)) {
        // call this only if a local copy of ca-bundle.crt exists;
        // otherwise, let's hope that it exists in the default place
        //
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_CAINFO, CA_BUNDLE_FILENAME);
    }
#endif

    // set the user agent as this boinc client & version
    //
    curlErr = curl_easy_setopt(curlEasy, CURLOPT_USERAGENT, g_user_agent_string);

    // bypass any signal handlers that curl may want to install
    //
    curlErr = curl_easy_setopt(curlEasy, CURLOPT_NOSIGNAL, 1L);
    // bypass progress meter
    //
    curlErr = curl_easy_setopt(curlEasy, CURLOPT_NOPROGRESS, 1L);

    // setup timeouts
    //
    curlErr = curl_easy_setopt(curlEasy, CURLOPT_TIMEOUT, 0L);
    curlErr = curl_easy_setopt(curlEasy, CURLOPT_LOW_SPEED_LIMIT, 10L);
    curlErr = curl_easy_setopt(curlEasy, CURLOPT_LOW_SPEED_TIME, 300L);
    curlErr = curl_easy_setopt(curlEasy, CURLOPT_CONNECTTIMEOUT, 120L);
    
    // force curl to use HTTP/1.0 if config specifies it
	// (curl uses 1.1 by default)
	//
	if (config.http_1_0 || (config.force_auth == "ntlm")) {
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
	}
    curlErr = curl_easy_setopt(curlEasy, CURLOPT_MAXREDIRS, 50L);
    curlErr = curl_easy_setopt(curlEasy, CURLOPT_AUTOREFERER, 1L);
    curlErr = curl_easy_setopt(curlEasy, CURLOPT_FOLLOWLOCATION, 1L);
    curlErr = curl_easy_setopt(curlEasy, CURLOPT_POST301, 1L);

    // if we tell Curl to accept any encoding (e.g. deflate)
    // it seems to accept them all, which screws up projects that
    // use gzip at the application level.
    // So, detect this and don't accept any encoding in that case
    //
    if (!out || !ends_with(std::string(out), std::string(".gz"))) {
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_ENCODING, "");
    }
	
	// setup any proxy they may need
    //
    setupProxyCurl(no_proxy_for_url(url));


    // set the content type in the header
    //
    pcurlList = curl_slist_append(pcurlList, g_content_type);

	// set the file offset for resumable downloads
    //
	if (!bPost && offset>0.0f) {
        file_offset = offset;
        sprintf(strTmp, "Range: bytes=%.0f-", offset);
        pcurlList = curl_slist_append(pcurlList, strTmp);
    }

    // set up an output file for the reply
    //
    if (strlen(outfile)) {  
		if (file_offset>0.0) {
            fileOut = boinc_fopen(outfile, "ab+");
        } else {
            fileOut = boinc_fopen(outfile, "wb+");
        }
        if (!fileOut) {
            msg_printf(NULL, MSG_INTERNAL_ERROR, 
                "Can't create HTTP response output file %s", outfile
            );
            http_op_retval = ERR_FOPEN;
            http_op_state = HTTP_STATE_DONE;
            return ERR_FOPEN;
        }
        // we can make the libcurl_write "fancier" in the future,
        // for now it just fwrite's to the file request, which is sufficient
        //
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_WRITEFUNCTION, libcurl_write);
        // note that in my lib_write I'm sending in a pointer
        // to this instance of HTTP_OP
        //
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_WRITEDATA, this);
    }

    if (bPost) {
        want_upload = true;
        want_download = false;
        if (infile && strlen(infile)>0) {
            fileIn = boinc_fopen(infile, "rb");
            if (!fileIn) {
                msg_printf(NULL, MSG_INTERNAL_ERROR, "No HTTP input file %s", infile);
                http_op_retval = ERR_FOPEN;
                http_op_state = HTTP_STATE_DONE;
                return ERR_FOPEN;
            }
        }        

        if (pcurlList) { // send custom headers if required
            curlErr = curl_easy_setopt(curlEasy, CURLOPT_HTTPHEADER, pcurlList);
        }

        // set the data file info to read for the PUT/POST
        // note the use of this curl typedef for large filesizes

#if 0
        // HTTP PUT method
        curl_off_t fs = (curl_off_t) content_length; 
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_POSTFIELDS, NULL);
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_INFILESIZE, content_length);
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_READDATA, fileIn);
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_INFILESIZE_LARGE, fs);
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_PUT, 1L);
#endif

        // HTTP POST method
        // set the multipart form for the file --
        // boinc just has the one section (file)
        
#if 0
        //  if we ever want to do POST as multipart forms someday 
        // (many seem to prefer it that way, i.e. libcurl)
        //
        pcurlFormStart = pcurlFormEnd = NULL;
        curl_formadd(&pcurlFormStart, &pcurlFormEnd, 
               CURLFORM_FILECONTENT, infile,
               CURLFORM_CONTENTSLENGTH, content_length,
               CURLFORM_CONTENTTYPE, g_content_type, 
               CURLFORM_END);
        curl_formadd(&post, &last,
               CURLFORM_COPYNAME, "logotype-image",
               CURLFORM_FILECONTENT, "curl.png", CURLFORM_END);
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_HTTPPOST, pcurlFormStart);
#endif

        curl_off_t fs = (curl_off_t) content_length; 

        pByte = NULL;
        lSeek = 0;    // initialize the vars we're going to use for byte transfers

        // we can make the libcurl_read "fancier" in the future,
        // for now it just fwrite's to the file request, which is sufficient
        //
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_POSTFIELDS, NULL);
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_POSTFIELDSIZE_LARGE, fs);
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_READFUNCTION, libcurl_read);
        // in my lib_write I'm sending in a pointer to this instance of HTTP_OP
        //
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_READDATA, this);

        // callback function to rewind input file 
        //
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_IOCTLFUNCTION, libcurl_ioctl);
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_IOCTLDATA, this);

        curlErr = curl_easy_setopt(curlEasy, CURLOPT_POST, 1L);
    } else {  // GET
        want_upload = false;
        want_download = true;

        // now write the header, pcurlList gets freed in net_xfer_curl
        //
        if (pcurlList) { // send custom headers if required
            curlErr = curl_easy_setopt(curlEasy, CURLOPT_HTTPHEADER, pcurlList);
        }

        // setup the GET!
        //
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_HTTPGET, 1L);
    }

    // turn on debug info if tracing enabled
    //
    if (log_flags.http_debug) {
        static int trace_count = 0;
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_DEBUGFUNCTION, libcurl_debugfunction);
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_DEBUGDATA, this );
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_VERBOSE, 1L); 
        trace_id = trace_count++;
    }

    // last but not least, add this to the curl_multi

    curlMErr = curl_multi_add_handle(g_curlMulti, curlEasy);
    if (curlMErr != CURLM_OK && curlMErr != CURLM_CALL_MULTI_PERFORM) {
        // bad error, couldn't attach easy curl handle
        msg_printf(0, MSG_INTERNAL_ERROR,
            "Couldn't add curlEasy handle to curlMulti"
        );
        return ERR_HTTP_ERROR;
        // returns 0 (CURLM_OK) on successful handle creation
    }

    return 0;
}

// Returns true if the HTTP operation is complete
//
bool HTTP_OP::http_op_done() {
    return (http_op_state == HTTP_STATE_DONE);
}

HTTP_OP_SET::HTTP_OP_SET() {
    bytes_up = 0;
    bytes_down = 0;
}

// Adds an HTTP_OP to the set
//
int HTTP_OP_SET::insert(HTTP_OP* ho) {
    http_ops.push_back(ho);
    return 0;
}

// Remove an HTTP_OP from the set
//
int HTTP_OP_SET::remove(HTTP_OP* p) {
    vector<HTTP_OP*>::iterator iter;

    iter = http_ops.begin();
    while (iter != http_ops.end()) {
        if (*iter == p) {
            iter = http_ops.erase(iter);
            return 0;
        }
        iter++;
    }
    msg_printf(NULL, MSG_INTERNAL_ERROR, "HTTP operation not found");
    return ERR_NOT_FOUND;
}

int HTTP_OP_SET::nops() {
    return (int)http_ops.size();  
}


int HTTP_OP::set_proxy(PROXY_INFO *new_pi) {
    pi.use_http_proxy = new_pi->use_http_proxy;
    strcpy(pi.http_user_name, new_pi->http_user_name);
    strcpy(pi.http_user_passwd, new_pi->http_user_passwd);
    strcpy(pi.http_server_name, new_pi->http_server_name);
    pi.http_server_port = new_pi->http_server_port;
    pi.use_http_auth = new_pi->use_http_auth;
	strcpy(pi.noproxy_hosts, new_pi->noproxy_hosts);

    pi.use_socks_proxy = new_pi->use_socks_proxy;
    strcpy(pi.socks5_user_name, new_pi->socks5_user_name);
    strcpy(pi.socks5_user_passwd, new_pi->socks5_user_passwd);
    strcpy(pi.socks_server_name, new_pi->socks_server_name);
    pi.socks_server_port = new_pi->socks_server_port;
    pi.socks_version = new_pi->socks_version;

    return 0;
}

size_t libcurl_write(void *ptr, size_t size, size_t nmemb, HTTP_OP* phop) {
    // take the stream param as a FILE* and write to disk
    // TODO: maybe assert stRead == size*nmemb,
    // add exception handling on phop members
    //
    size_t stWrite = fwrite(ptr, size, nmemb, phop->fileOut);
    if (log_flags.http_xfer_debug) {
        msg_printf(NULL, MSG_INFO,
            "[http_xfer_debug] HTTP: wrote %d bytes", (int)stWrite
        );
    }
    phop->bytes_xferred += (double)(stWrite);
    phop->update_speed();  // this should update the transfer speed
    return stWrite;
}

size_t libcurl_read( void *ptr, size_t size, size_t nmemb, HTTP_OP* phop) {
    // OK here's the deal -- phop points to the calling object,
    // which has already pre-opened the file.  we'll want to
    // use pByte as a pointer for fseek calls into the file, and
    // write out size*nmemb # of bytes to ptr

    // take the stream param as a FILE* and write to disk
    // if (pByte) delete [] pByte;
    // pByte = new unsigned char[content_length];
    // memset(pByte, 0x00, content_length); // may as will initialize it!

    // note that fileIn was opened earlier,
    // go to lSeek from the top and read from there
    //
    size_t stSend = size * nmemb;
    int stRead = 0;

    if (phop->req1 && !phop->bSentHeader) {
        // need to send headers first, then data file
        // so requests from 0 to strlen(req1)-1 are from memory,
        // and from strlen(req1) to content_length are from the file
        if (phop->lSeek < (long) strlen(phop->req1)) {  
            // need to read header, either just starting to read
            // (i.e. this is the first time in this function for this phop)
            // or the last read didn't ask for the entire header

            stRead = (int)strlen(phop->req1) - phop->lSeek;
                // how much of header left to read

            // only memcpy if request isn't out of bounds
            if (stRead < 0) {
                stRead = 0;
            } else {
                memcpy(ptr, (void*)(phop->req1 + phop->lSeek), stRead);
            }
            phop->lSeek += (long) stRead;  // increment lSeek to new position

            // Don't count header in bytes transferred.
            // Otherwise the GUI will show e.g. "400 out of 300 bytes xferred"
            //phop->bytes_xferred += (double)(stRead);

            // see if we're done with headers
            if (phop->lSeek >= (long) strlen(phop->req1)) {
                phop->bSentHeader = true;
                phop->lSeek = 0; 
            }
            return stRead;
        } else {
            // shouldn't happen
            phop->bSentHeader = true;
            phop->lSeek = 0; 
        }
    }
    if (phop->fileIn) { 
        long lFileSeek = phop->lSeek + (long) phop->file_offset;
        fseek(phop->fileIn, lFileSeek, SEEK_SET);
        if (!feof(phop->fileIn)) {
            stRead = (int)fread(ptr, 1, stSend, phop->fileIn); 
        }    
        phop->lSeek += (long) stRead;
        phop->bytes_xferred += (double)(stRead);
    }
    phop->update_speed();
    return stRead;
}

curlioerr libcurl_ioctl(CURL*, curliocmd cmd, HTTP_OP* phop) {
    // reset input stream to beginning - resends header
    // and restarts data back to starting point

    switch(cmd) {
    case CURLIOCMD_RESTARTREAD:
        phop->lSeek = 0;
        phop->bytes_xferred = phop->file_offset;
        phop->bSentHeader = false;
        break;
    default: // should never get here
        return CURLIOE_UNKNOWNCMD;
    }
    return CURLIOE_OK;
}

int libcurl_debugfunction(
    CURL*, curl_infotype type,
    unsigned char *data, size_t size, HTTP_OP* phop
) {
    const char *text;
    char hdr[100];
    char buf[1024];
    size_t mysize;
    
    switch (type) {
    case CURLINFO_TEXT:
        if (log_flags.http_debug) {
            msg_printf(0, MSG_INFO,
                "[http_debug] [ID#%i] info: %s\n", phop->trace_id, data
            );
        }
        return 0;
    case CURLINFO_HEADER_OUT:
        text = "Sent header to server:";
        break;
    case CURLINFO_HEADER_IN:
        text = "Received header from server:";
        break;
    default: /* in case a new one is introduced to shock us */
       return 0;
    }

    sprintf( hdr,"[ID#%i] %s", phop->trace_id, text);
    mysize = min(size, sizeof(buf)-1);
    strncpy(buf, (char *)data, mysize);
    buf[mysize]='\0';
    if (log_flags.http_debug) {
        msg_printf(0, MSG_INFO,
            "[http_debug] %s %s\n", hdr, buf
        );
    }
    return 0;
}


void HTTP_OP::setupProxyCurl(bool no_proxy) {
// PROXY_INFO pi useful members:
//  pi.http_server_name
//  pi.http_server_port
//  pi.http_user_name
//  pi.http_user_passwd
//  pi.socks5_user_name
//  pi.socks5_user_passwd
//  pi.socks_server_name
//  pi.socks_server_port
//  pi.socks_version
//  pi.use_http_auth
//  pi.use_http_proxy
//  pi.use_socks_proxy
//
// Curl self-explanatory setopt params for proxies:
//    CURLOPT_HTTPPROXYTUNNEL
//    CURLOPT_PROXYTYPE  (pass in CURLPROXY_HTTP or CURLPROXY_SOCKS5)
//    CURLOPT_PROXYPORT  -- a long port #
//    CURLOPT_PROXY - pass in char* of the proxy url
//    CURLOPT_PROXYUSERPWD -- a char* in the format username:password
//    CURLOPT_HTTPAUTH -- pass in one of CURLAUTH_BASIC, CURLAUTH_DIGEST, 
//        CURLAUTH_GSSNEGOTIATE, CURLAUTH_NTLM, CURLAUTH_ANY, CURLAUTH_ANYSAFE
//    CURLOPT_PROXYAUTH -- "or" | the above bitmasks -- only basic, digest, ntlm work

    CURLcode curlErr;

    // CMC Note: the string szCurlProxyUserPwd must remain in memory
    // outside of this method (libcurl relies on it later when it makes
    // the proxy connection), so it has been placed as a member data for HTTP_OP
    memset(szCurlProxyUserPwd,0x00,128);

	if (no_proxy) {
		curlErr = curl_easy_setopt(curlEasy,CURLOPT_PROXY,"");
		return;
	}

	if (pi.use_http_proxy) {
		if (log_flags.proxy_debug) {
			msg_printf(0, MSG_INFO,"[proxy_debug]: setting up proxy %s:%d",pi.http_server_name,pi.http_server_port);
	    }
        // setup a basic http proxy
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_PROXYPORT, (long) pi.http_server_port);
        curlErr = curl_easy_setopt(curlEasy, CURLOPT_PROXY, (char*) pi.http_server_name);

        if (pi.use_http_auth) {
            if        (config.force_auth == "basic") {
                curlErr = curl_easy_setopt(curlEasy, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
            } else if (config.force_auth == "digest") {
                curlErr = curl_easy_setopt(curlEasy, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST);
            } else if (config.force_auth == "gss-negotiate") {
                curlErr = curl_easy_setopt(curlEasy, CURLOPT_PROXYAUTH, CURLAUTH_GSSNEGOTIATE);
            } else if (config.force_auth == "ntlm") {
                curlErr = curl_easy_setopt(curlEasy, CURLOPT_PROXYAUTH, CURLAUTH_NTLM);
            } else {
                curlErr = curl_easy_setopt(curlEasy, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
            }
            sprintf(szCurlProxyUserPwd, "%s:%s", pi.http_user_name, pi.http_user_passwd);
            curlErr = curl_easy_setopt(curlEasy, CURLOPT_PROXYUSERPWD, szCurlProxyUserPwd);
        }
    } else {    
        if (pi.use_socks_proxy) {
            //pi.socks_version -- picks between socks5 & socks4 -- but libcurl only socks5!
            curlErr = curl_easy_setopt(curlEasy, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
            curlErr = curl_easy_setopt(curlEasy, CURLOPT_PROXYPORT, (long) pi.socks_server_port);
            curlErr = curl_easy_setopt(curlEasy, CURLOPT_PROXY, (char*) pi.socks_server_name);
            // libcurl uses blocking sockets with socks proxy, so limit timeout.
            // - imlemented with local patch to libcurl
            curlErr = curl_easy_setopt(curlEasy, CURLOPT_CONNECTTIMEOUT, 20L);

            if (
                strlen(pi.socks5_user_passwd)>0 || strlen(pi.socks5_user_name)>0
            ) {
                sprintf(szCurlProxyUserPwd, "%s:%s", pi.socks5_user_name, pi.socks5_user_passwd);
                curlErr = curl_easy_setopt(curlEasy, CURLOPT_PROXYUSERPWD, szCurlProxyUserPwd);
                curlErr = curl_easy_setopt(curlEasy, CURLOPT_PROXYAUTH, CURLAUTH_ANY & ~CURLAUTH_NTLM);
            }
        }
    }
}

// the file descriptor sets need to be global so libcurl has access always
//
fd_set read_fds, write_fds, error_fds;

// call these once at the start of the program and once at the end
//
int curl_init() {
    curl_global_init(CURL_GLOBAL_ALL);
    g_curlMulti = curl_multi_init();
    return (int)(g_curlMulti == NULL);
}

int curl_cleanup() {
    if (g_curlMulti) {
        curl_multi_cleanup(g_curlMulti);
    }
    return 0;
}

void HTTP_OP::close_socket() {
    // this cleans up the curlEasy, and "spoofs" the old close_socket
    //
    if (pcurlList) {
        curl_slist_free_all(pcurlList);
        pcurlList = NULL;
    }
    if (curlEasy && pcurlFormStart) {
        curl_formfree(pcurlFormStart);
        curl_formfree(pcurlFormEnd);
        pcurlFormStart = pcurlFormEnd = NULL;
    }
    if (curlEasy && g_curlMulti) {  // release this handle
        curl_multi_remove_handle(g_curlMulti, curlEasy);
        curl_easy_cleanup(curlEasy);
        curlEasy = NULL;
    }
}

void HTTP_OP::close_file() {
    if (fileIn) {
        fclose(fileIn);
        fileIn = NULL;
    }
    if (fileOut) {
        fclose(fileOut);
        fileOut = NULL;
    }
    if (pByte) { //free any read memory used
        delete [] pByte;
        pByte = NULL;
    }
}

void HTTP_OP_SET::get_fdset(FDSET_GROUP& fg) {
    CURLMcode curlMErr;
    curlMErr = curl_multi_fdset(
        g_curlMulti, &fg.read_fds, &fg.write_fds, &fg.exc_fds, &fg.max_fd
    );
}

// we have a message for this HTTP_OP.
// get the response code for this request
//
void HTTP_OP::handle_messages(CURLMsg *pcurlMsg) {
    CURLcode curlErr;
    int retval;

    curlErr = curl_easy_getinfo(curlEasy, 
        CURLINFO_RESPONSE_CODE, &response
    );

    // CURLINFO_LONG+25 is a workaround for a bug in the gcc version
    // included with Mac OS X 10.3.9
    //
    curlErr = curl_easy_getinfo(curlEasy, 
        (CURLINFO)(CURLINFO_LONG+25) /*CURLINFO_OS_ERRNO*/, &connect_error
    );

    // update byte counts and transfer speed
    //
    if (want_download) {
        // SIZE_DOWNLOAD is the byte count "on the wire"
        // (possible with compression)
        // TOTAL_TIME is the elapsed time of the download
        // STARTTRANSFER_TIME is portion of elapsed time involved
        // with setup (connection establishment etc.)
        // SPEED_DOWNLOAD is bytes/sec based on uncompressed size
        // (we don't use it)
        //
        double size_download, total_time, starttransfer_time;
        curlErr = curl_easy_getinfo(curlEasy, 
            CURLINFO_SIZE_DOWNLOAD, &size_download
        );
        curlErr = curl_easy_getinfo(curlEasy, 
            CURLINFO_TOTAL_TIME, &total_time
        );
        curlErr = curl_easy_getinfo(curlEasy, 
            CURLINFO_STARTTRANSFER_TIME, &starttransfer_time
        );
        double dt = total_time - starttransfer_time;
        if (dt > 0) {
            gstate.net_stats.down.update(size_download, dt);
        }
    }
    if (want_upload) {
        double size_upload, total_time, starttransfer_time;
        curlErr = curl_easy_getinfo(curlEasy, 
            CURLINFO_SIZE_UPLOAD, &size_upload
        );
        curlErr = curl_easy_getinfo(curlEasy, 
            CURLINFO_TOTAL_TIME, &total_time
        );
        curlErr = curl_easy_getinfo(curlEasy, 
            CURLINFO_STARTTRANSFER_TIME, &starttransfer_time
        );
        double dt = total_time - starttransfer_time;
        if (dt > 0) {
            gstate.net_stats.up.update(size_upload, dt);
        }
    }

    // the op is done if curl_multi_msg_read gave us a msg for this http_op
    //
    http_op_state = HTTP_STATE_DONE;        
    CurlResult = pcurlMsg->data.result;

    if (CurlResult == CURLE_OK) {
        if ((response/100)*100 == HTTP_STATUS_OK) {
            http_op_retval = 0;  
        } else if ((response/100)*100 == HTTP_STATUS_CONTINUE) {
            return;
        } else {
            // Got a response from server but its not OK or CONTINUE,
            // so save response with error message to display later.
            //
            if (response >= 400) {
                strcpy(error_msg, boincerror(response));
            } else {
                sprintf(error_msg, "HTTP error %ld", response);
            }
            switch (response) {
            case HTTP_STATUS_NOT_FOUND:
                http_op_retval = ERR_FILE_NOT_FOUND;
                break;
            default:
                http_op_retval = ERR_HTTP_ERROR;
            }
        }
        net_status.need_physical_connection = false;
    } else {
        strcpy(error_msg, curl_easy_strerror(CurlResult));
        switch(CurlResult) {
        case CURLE_COULDNT_RESOLVE_HOST:
            http_op_retval = ERR_GETHOSTBYNAME;
            break;
        case CURLE_COULDNT_CONNECT:
            http_op_retval = ERR_CONNECT;
            break;
        default:
            http_op_retval = ERR_HTTP_ERROR;
        }
        net_status.got_http_error();
        if (log_flags.http_debug) {
            msg_printf(NULL, MSG_INFO,
                "[http_debug] HTTP error: %s", error_msg
            );
        }
    }

    if (!http_op_retval && http_op_type == HTTP_OP_POST2) {
        // for a successfully completed request on a "post2" --
        // read in the temp file into req1 memory
        //
        size_t dSize = ftell(fileOut);
        retval = fseek(fileOut, 0, SEEK_SET);
        if (retval) {
            // flag as a bad response for a possible retry later
            response = 1;
            msg_printf(NULL, MSG_INTERNAL_ERROR,
                "[http_debug] can't rewind post output file %s",
                outfile
            );
        } else {
            strcpy(req1, "");
            if (dSize >= (size_t)req1_len) {
                dSize = req1_len-1;
            }
            size_t nread = fread(req1, 1, dSize, fileOut); 
            if (nread != dSize) {
                if (log_flags.http_debug) {
                    msg_printf(NULL, MSG_INFO,
                        "[http_debug] post output file read failed %ld",
                        nread
                    );
                }
            }
            req1[nread] = 0;
        }
    }

    // close files and "sockets" (i.e. libcurl handles)
    //
    close_file();
    close_socket();

    // finally remove the tmpfile if not explicitly set
    //
    if (bTempOutfile) {
        boinc_delete_file(outfile);
    }
}

void HTTP_OP_SET::got_select(FDSET_GROUP&, double timeout) {
    int iNumMsg;
    HTTP_OP* hop = NULL;
    CURLMsg *pcurlMsg = NULL;

    int iRunning = 0;  // curl flags for max # of fds & # running queries
    CURLMcode curlMErr;

    // get the data waiting for transfer in or out
    // use timeout value so that we don't hog CPU in this loop
    //
    while (1) {
        curlMErr = curl_multi_perform(g_curlMulti, &iRunning);
        if (curlMErr != CURLM_CALL_MULTI_PERFORM) break;
        if (dtime() - gstate.now > timeout) break;
    }

    // read messages from curl that may have come in from the above loop
    //
	while (1) {
		pcurlMsg = curl_multi_info_read(g_curlMulti, &iNumMsg);
		if (!pcurlMsg) break;

        // if we have a msg, then somebody finished
        // can check also with pcurlMsg->msg == CURLMSG_DONE
        //
        hop = lookup_curl(pcurlMsg->easy_handle);
        if (!hop) continue;
        hop->handle_messages(pcurlMsg);
    }
}

// Return the HTTP_OP object with given Curl object
//
HTTP_OP* HTTP_OP_SET::lookup_curl(CURL* pcurl)  {
	for (unsigned int i=0; i<http_ops.size(); i++) {
		if (http_ops[i]->curlEasy == pcurl) {
            return http_ops[i];
        }
    }
    return 0;
}

// Update the transfer speed for this HTTP_OP
// called on every I/O
//
void HTTP_OP::update_speed() {
    double delta_t = dtime() - start_time;
    if (delta_t > 0) {
        xfer_speed = (bytes_xferred-start_bytes_xferred) / delta_t;
    }
}

void HTTP_OP::set_speed_limit(bool is_upload, double bytes_sec) {
#if LIBCURL_VERSION_NUM >= 0x070f05
	CURLcode cc = CURLE_OK;
    curl_off_t bs = (curl_off_t)bytes_sec;

    if (is_upload) {
        cc = curl_easy_setopt(curlEasy, CURLOPT_MAX_SEND_SPEED_LARGE, bs);
    } else {
        cc = curl_easy_setopt(curlEasy, CURLOPT_MAX_RECV_SPEED_LARGE, bs);
    }
	if (cc && log_flags.http_debug) {
		msg_printf(NULL, MSG_INFO, "[http_debug] Curl error in set_speed_limit(): %s", curl_easy_strerror(cc));
	}
#endif
}

void HTTP_OP_SET::cleanup_temp_files() {
    char filename[256];

    DIRREF d = dir_open(".");
    while (1) {
        int retval = dir_scan(filename, d, sizeof(filename));
        if (retval) break;
        if (strstr(filename, "blc") != filename) continue;
        if (!is_file(filename)) continue;
        boinc_delete_file(filename);
    }
    dir_close(d);
}

const char *BOINC_RCSID_57f273bb60 = "$Id: http_curl.cpp 16189 2008-10-11 18:13:33Z davea $";
