Subversion Repositories tpanel

Rev

Rev 446 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2021, 2022 by Andreas Theofilu <andreas@theosys.at>
 *
 * 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, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */

#include <vector>
#include <thread>

#include <string.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netdb.h>
#include <netinet/tcp.h>
#include <openssl/err.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <signal.h>
#include <poll.h>

#include "thttpclient.h"
#include "base64.h"
#include "terror.h"
#include "tresources.h"
#include "tconfig.h"
#include "texcept.h"

#define HTTP_DIRECTION_RECEIVE  1
#define HTTP_DIRECTION_SEND     2

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

THTTPClient::THTTPClient()
{
    DECL_TRACER("THTTPClient::THTTPClient()");
    mBody.body = nullptr;
    mBody.len = 0;
    mRequest.code = 0;
    mRequest.direction = 0;
    mRequest.method = UNSUPPORTED;
    mRequest.path = nullptr;
    mRequest.status = nullptr;
    mRequest.version = nullptr;
}

THTTPClient::~THTTPClient()
{
    DECL_TRACER("THTTPClient::~THTTPClient()");
    mBody.clear();
    mRequest.clear();
}

#define MAX_BUFFER  65535
#define MAX_BLOCK   32767

char *THTTPClient::tcall(size_t *size, const string& URL, const string& user, const string& pw)
{
    DECL_TRACER("THTTPClient::tcall(size_t size, const string& URL, const string& user, const string& pw)");

    char *buffer = nullptr;
    size_t bufsize = 0;
    mUser = user;
    mPassword = pw;
    bool encrypt = false;

    try
    {
        buffer = new char[MAX_BUFFER];
        bufsize = MAX_BUFFER;
    }
    catch (std::exception& e)
    {
        MSG_ERROR(e.what());
        return nullptr;
    }

    string request = makeRequest(URL);

    if (TError::isError() || request.empty())
    {
        delete[] buffer;
        return nullptr;
    }

    if (mURL.scheme == "https")
        encrypt = true;

    if (!connect(mURL.host, mURL.port, encrypt))
    {
        delete[] buffer;
        return nullptr;
    }

    size_t ret = 0;
    bool repeat = false;

    try
    {
        do
        {
            if ((ret = send((char *)request.c_str(), request.length())) < 0)
            {
                if (errno)
                {
                    MSG_ERROR("[" << mURL.host << "] Write error: " << strerror(errno));
                }
                else if (encrypt)
                {
                    int err = retrieveSSLerror(static_cast<int>(ret));
                    repeat = false;

                    switch (err)
                    {
                        case SSL_ERROR_ZERO_RETURN:     MSG_ERROR("The TLS/SSL peer has closed the connection for writing by sending the close_notify alert."); break;

                        case SSL_ERROR_WANT_READ:
                        case SSL_ERROR_WANT_WRITE:      MSG_TRACE("The operation did not complete and can be retried later."); repeat = true; break;
                        case SSL_ERROR_WANT_CONNECT:
                        case SSL_ERROR_WANT_ACCEPT:     MSG_TRACE("The operation did not complete; the same TLS/SSL I/O function should be called again later."); repeat = true; break;
                        case SSL_ERROR_WANT_X509_LOOKUP:MSG_TRACE("The operation did not complete because an application callback set by SSL_CTX_set_client_cert_cb() has asked to be called again."); repeat = true; break;
                        case SSL_ERROR_WANT_ASYNC:      MSG_TRACE("The operation did not complete because an asynchronous engine is still processing data."); repeat = true; break;
                        case SSL_ERROR_WANT_ASYNC_JOB:  MSG_TRACE("The asynchronous job could not be started because there were no async jobs available in the pool."); repeat = true; break;
                        case SSL_ERROR_WANT_CLIENT_HELLO_CB: MSG_TRACE("The operation did not complete because an application callback set by SSL_CTX_set_client_hello_cb() has asked to be called again."); repeat = true; break;

                        case SSL_ERROR_SYSCALL:         MSG_ERROR("Some non-recoverable, fatal I/O error occurred."); break;
                        case SSL_ERROR_SSL:             MSG_ERROR("A non-recoverable, fatal error in the SSL library occurred, usually a protocol error."); break;

                        default:
                            MSG_ERROR("Unknown error " << err << " occured!");
                    }
                }
                else
                {
                    MSG_ERROR("[" << mURL.host << "] Write error!");
                }

                if (!repeat)
                {
                    close();

                    if (buffer)
                        delete[] buffer;

                    return nullptr;
                }
            }

            if (repeat)
                std::this_thread::sleep_for(std::chrono::microseconds(1000));
        }
        while (repeat);
    }
    catch (TXceptNetwork& e)
    {
        MSG_ERROR("Error writing to " << mURL.host << ":" << mURL.port);

        if (buffer)
            delete[] buffer;

        return nullptr;
    }

    char buf[8194];
    memset(buf, 0, sizeof(buf));
    size_t pos = 0, length = 0;
    size_t rlen, totalLen = 0;
    int loop = 0;

    try
    {
        std::chrono::steady_clock::time_point timePoint = std::chrono::steady_clock::now();
        totalLen = 0;

        while ((rlen = receive(buf, sizeof(buf))) > 0 && rlen != TSocket::npos)
        {
            size_t len = rlen;

            if (totalLen == 0 && loop < 2)
            {
                // Let's see if we have already the content length
                char *cLenPos = nullptr;

                if ((cLenPos = strnstr(buf, "Content-Length:", rlen)) != nullptr)
                {
                    cLenPos += 16;  // Start of content length information
                    size_t cLen = atol(cLenPos);

                    char *cStart = strstr(buf, "\r\n\r\n"); // Find start of content

                    if (cStart)
                        totalLen = cLen + ((cStart + 4) - buf);

                    MSG_DEBUG("Total length: " << totalLen << ", content length: " << cLen);
                }
            }

            if ((pos + len) >= bufsize)
            {
                renew(&buffer, bufsize, bufsize + MAX_BLOCK);

                if (!buffer)
                {
                    MSG_ERROR("[" << mURL.host << "] Memory error: Allocating of " << (bufsize + MAX_BLOCK) << " failed!");
                    return nullptr;
                }

                bufsize += MAX_BLOCK;
            }

            if (len > 0)
            {
                memcpy(buffer+pos, buf, len);
                pos += len;
                length += len;
            }

            if (length && totalLen && length >= totalLen)
                break;

            memset(buf, 0, sizeof(buf));
            loop++;
        }

        if (TStreamError::checkFilter(HLOG_DEBUG))
        {
            std::chrono::steady_clock::time_point endPoint = std::chrono::steady_clock::now();
            std::chrono::nanoseconds difftime = endPoint - timePoint;
            std::chrono::seconds secs = std::chrono::duration_cast<std::chrono::seconds>(difftime);
            std::chrono::milliseconds msecs = std::chrono::duration_cast<std::chrono::milliseconds>(difftime) - std::chrono::duration_cast<std::chrono::seconds>(secs);
            std::stringstream s;
            s << std::chrono::duration_cast<std::chrono::nanoseconds> (difftime).count() << "[ns]" << " --> " << std::chrono::duration_cast<std::chrono::seconds>(secs).count() << "s " << std::chrono::duration_cast<std::chrono::milliseconds>(msecs).count() << "ms";
            MSG_DEBUG("[" << mURL.host << "] Elapsed time for receive: " << s.str());
        }
    }
    catch(TXceptNetwork& e)
    {
        MSG_ERROR("Error reading from " << mURL.host << ": " << e.what());

        if (buffer)
            delete[] buffer;

        return nullptr;
    }
    catch (std::exception& e)
    {
        MSG_ERROR("Error reading from " << mURL.host << ": " << e.what());
        close();

        if (buffer)
            delete[] buffer;

        return nullptr;
    }

    int myerrno = errno;
    close();

    if (pos == 0)
    {
        if (myerrno)
        {
            MSG_ERROR("Internal read error [" << mURL.host << "]: " << strerror(myerrno));
        }
        else
        {
            MSG_ERROR("Internal read error: Received no data from " << mURL.host);
        }

        if (buffer)
            delete[] buffer;

        return nullptr;
    }

    MSG_DEBUG("[" << mURL.host << "] Read " << length << " bytes.");

    if (parseHeader(buffer, length) != 0)
    {
        if (buffer)
            delete[] buffer;

        return nullptr;
    }

    if (mRequest.code >= 300)
    {
        if (mRequest.status && *mRequest.status)
        {
            MSG_ERROR("[" << mURL.host << "] " << mRequest.code << ": " << mRequest.status);
        }
        else
        {
            MSG_ERROR("[" << mURL.host << "] " << mRequest.code << ": UNKNOWN");
        }

        if (buffer)
            delete[] buffer;

        return nullptr;
    }

    if (buffer)
        delete[] buffer;

    *size = mBody.len;
    return mBody.body;
}

string THTTPClient::makeURL(const string& scheme, const string& host, int port, const string& path)
{
    DECL_TRACER("THTTPClient::makeURL(const string& scheme, const string& host, int port, const string& path)");

    string url = scheme + "://" + host;

    if (port > 0)
        url += std::to_string(port);

    url += "/" + path;
    MSG_DEBUG("URL: " << url);
    return url;
}

string THTTPClient::makeURLs(const string& scheme, const string& host, int port, const string& path)
{
    DECL_TRACER("THTTPClient::makeURLs(const string& scheme, const string& host, int port, const string& path)");

    THTTPClient c;
    return c.makeURL(scheme, host, port, path);
}

URL_t& THTTPClient::parseURL(const string& URL)
{
    DECL_TRACER("THTTPClient::parseURL(const string& URL, const string& user, const string& pw)");

    if (URL.empty())
    {
        MSG_ERROR("Invalid empty URL!");
        TError::setError();
        mURL.clear();
        return mURL;
    }

    size_t pos = URL.find("://");

    if (pos == string::npos)
    {
        MSG_ERROR("Invalid URL: " << URL);
        TError::setError();
        mURL.clear();
        return mURL;
    }

    mURL.scheme = URL.substr(0, pos);
    pos += 3;
    string part = URL.substr(pos);
    pos = part.find("/");
    string host;

    if (pos == string::npos)
    {
        host = part;
        part.clear();
    }
    else
    {
        host = part.substr(0, pos);
        part = part.substr(pos + 1);
    }

    pos = host.find(":");

    if (pos != string::npos)
    {
        string sport = host.substr(pos + 1);
        mURL.host = host.substr(0, pos);
        mURL.port = atoi(sport.c_str());
    }
    else
        mURL.host = host;

    if (!part.empty())
        mURL.path = part;

    pos = host.find("@");

    if (pos != string::npos)
    {
        mURL.user = host.substr(0, pos);
        mURL.host = host.substr(pos + 1);
    }

    if (mURL.port == 0)
    {
        if (mURL.scheme.compare("https") == 0)
            mURL.port = 443;
        else
            mURL.port = 80;
    }

    MSG_DEBUG("URL components: Scheme: " << mURL.scheme << ", Host: " << mURL.host << ", Port: " << mURL.port << ", Path: " << mURL.path << ", User: " << mURL.user << ", Password: " << ((mURL.pw.empty()) ? "" : "****"));
    return mURL;
}

int THTTPClient::parseHeader(const char *buffer, size_t len)
{
    DECL_TRACER("THTTPClient::parseHeader(const char *buffer, size_t len)");

    if (buffer == nullptr || len == 0)
    {
        MSG_ERROR("[" << mURL.host << "] Empty receive buffer!");
        return -1;
    }

    int blen = 0, receive = 0, code = 0;
    char *raw = (char *)buffer;
    char *path, *version, *status;
    int direction = HTTP_DIRECTION_SEND;
    METHOD_t method;
    mHeader.clear();
    mBody.clear();
    mRequest.clear();

    // Method
    size_t meth_len = strcspn(raw, " ");

    if (meth_len >= len)
    {
        MSG_ERROR("[" << mURL.host << "] Buffer contains no valid HTTP response!");
        return -1;
    }

    if (memcmp(raw, "GET", 3) == 0)
        method = GET;
    else if (memcmp(raw, "PUT", 3) == 0)
        method = PUT;
    else if (memcmp(raw, "POST", 4) == 0)
        method = POST;
    else if (memcmp(raw, "HEAD", 4) == 0)
        method = HEAD;
    else
    {
        method = UNSUPPORTED;
        direction = HTTP_DIRECTION_RECEIVE;
        receive = 1;
        MSG_DEBUG("[" << mURL.host << "] Detected a receive buffer");
    }

    mRequest.method = method;
    mRequest.direction = direction;
    raw += meth_len + 1; // move past <SP>
    status = path = version = nullptr;

    if (!receive)
    {
        // Request-URI
        size_t path_len = strcspn(raw, " ");

        try
        {
            path = new char[path_len+1];
            memcpy(path, raw, path_len);
            path[path_len] = '\0';
            raw += path_len + 1; // move past <SP>

            // HTTP-Version
            size_t ver_len = strcspn(raw, "\r\n");
            version = new char[ver_len + 1];
            memcpy(version, raw, ver_len);
            version[ver_len] = '\0';
            raw += ver_len + 2; // move past <CR><LF>
            mRequest.path = path;
            mRequest.version = version;
        }
        catch (std::exception& e)
        {
            MSG_ERROR("[" << mURL.host << "] Error allocating memory: " << e.what());

            if (path)
                delete[] path;

            if (version)
                delete[] version;

            mRequest.path = nullptr;
            mRequest.version = nullptr;
            return -1;
        }
    }
    else
    {
        char scode[16];
        memset(scode, 0, sizeof(scode));
        size_t code_len = strcspn(raw, " ");
        strncpy(scode, raw, min(code_len, sizeof(scode)));
        scode[sizeof(scode)-1] = 0;
        code = atoi(scode);

        MSG_DEBUG("[" << mURL.host << "] Received code " << code);

        if (strstr(buffer, "\r\n\r\n") == NULL)
        {
            MSG_ERROR("[" << mURL.host << "] Received no content!");
            return -1;
        }

        if (code_len >= len)
        {
            MSG_ERROR("[" << mURL.host << "] Buffer contains no valid HTTP response!");
            return -1;
        }

        raw += code_len + 1;
        size_t stat_len = strcspn(raw, "\r\n");

        try
        {
            status = new char[stat_len + 1];
            memcpy(status, raw, stat_len);
            status[stat_len] = '\0';
            raw += stat_len + 2;
            mRequest.status = status;
        }
        catch (std::exception& e)
        {
            MSG_ERROR("[" << mURL.host << "] Error allocating memory: " << e.what());
            return -1;
        }
    }

    char *end = strstr(raw, "\r\n\r\n");

    while (raw < end)
    {
        HTTPHEAD_t head;
        char hv0[1024];
        // name
        size_t name_len = strcspn(raw, ":");

        size_t mylen = min(name_len, sizeof(hv0));
        memcpy(hv0, raw, mylen);
        hv0[mylen] = '\0';
        head.name = hv0;
        raw += name_len + 1; // move past :

        while (*raw == ' ')
            raw++;

        // value
        size_t value_len = strcspn(raw, "\r\n");
        mylen = min(value_len, sizeof(hv0));
        memcpy(hv0, raw, mylen);
        hv0[mylen] = '\0';
        head.content = hv0;
        MSG_DEBUG("[" << mURL.host << "] Found header: " << head.name << ": " << head.content);
        raw += value_len + 2; // move past <CR><LF>

        if (head.name.compare("Content-Length") == 0)
            blen = atoi(head.content.c_str());

        // next
        mHeader.push_back(head);
    }

    if (blen == 0)
    {
        size_t head_len = strcspn(buffer, "\r\n\r\n");

        if (head_len < len)
            blen = static_cast<int>(len - head_len + 4);
    }

    MSG_DEBUG("[" << mURL.host << "] Content length: " << blen);

    if (blen > 0)
    {
        raw = end + 4;

        try
        {
            mBody.body = new char[blen +1];
            memcpy(mBody.body, raw, blen);
            mBody.body[blen] = '\0';
            mBody.len = blen;
        }
        catch(std::exception& e)
        {
            MSG_ERROR(e.what());

            if (status)
                delete[] status;

            if (path)
                delete[] path;

            if (version)
                delete[] version;

            mRequest.status = nullptr;
            mRequest.path = nullptr;
            mRequest.version = nullptr;
            return -1;
        }
    }

    return 0;
}

string THTTPClient::getHeadParameter(const string& name)
{
    DECL_TRACER("THTTPClient::getHeadParameter(const string& name)");

    if (mHeader.empty())
        return string();

    vector<HTTPHEAD_t>::iterator iter;

    for (iter = mHeader.begin(); iter != mHeader.end(); ++iter)
    {
        if (iter->name.compare(name) == 0)
            return iter->content;
    }

    return string();
}
/*
char *THTTPClient::getContent(const char* buffer)
{
    DECL_TRACER("THTTPClient::getContent(const char* buffer)");

    if (!buffer)
        return nullptr;

    char *ctnt = strstr((char *)buffer, "\r\n\r\n");
    return ctnt;
}
*/
string THTTPClient::makeRequest(const string& url)
{
    DECL_TRACER("THTTPClient::makeRequest(const string& url)");

    URL_t uparts = parseURL(url);

    if (uparts.host == "0.0.0.0" || uparts.host == "8.8.8.8")
    {
        MSG_WARNING("Refusing to connect to host " << uparts.host << "!");
        return string();
    }

    string request = "GET " + uparts.path + " HTTP/1.1\r\n";
    request += "Host: " + uparts.host;

    if (uparts.port > 0 && uparts.port != 80 && uparts.port != 443)
        request += ":" + std::to_string(uparts.port);

    request += "\r\n";

    if (!mUser.empty())
    {
        string clearname = mUser + ":" + mPassword;
        string enc = Base64::encode((BYTE *)clearname.c_str(), static_cast<uint>(clearname.size()));
        request += "Authorization: Basic " + enc + "\r\n";
    }

    request += "User-Agent: tpanel/" + std::to_string(V_MAJOR) + "." + std::to_string(V_MINOR) + "." + std::to_string(V_PATCH) + "\r\n";
    request += "Accept: image/*\r\n";
    request += "\r\n";
    MSG_DEBUG("Requesting: " << std::endl << request << "------------------------------------------");
    return request;
}