Skip to content

File HTTPClient.cpp

File List > arduino > libraries > ext > HTTPClient > HTTPClient.cpp

Go to the documentation of this file.

#if LT_ARD_HAS_WIFI

#include <Arduino.h>

#ifdef HTTPCLIENT_1_1_COMPATIBLE
#include <WiFi.h>
#include <WiFiClientSecure.h>
#endif

// #include <StreamString.h>
#include <base64.h>

#include "HTTPClient.h"

#include <time.h>

#ifdef HTTPCLIENT_1_1_COMPATIBLE
class TransportTraits {
  public:
    virtual ~TransportTraits() {}

    virtual std::unique_ptr<WiFiClient> create() {
        return std::unique_ptr<WiFiClient>(new WiFiClient());
    }

    virtual bool verify(WiFiClient &client, const char *host) {
        return true;
    }
};

class TLSTraits : public TransportTraits {
  public:
    TLSTraits(const char *CAcert, const char *clicert = nullptr, const char *clikey = nullptr)
        : _cacert(CAcert), _clicert(clicert), _clikey(clikey) {}

    std::unique_ptr<WiFiClient> create() override {
        return std::unique_ptr<WiFiClient>(new WiFiClientSecure());
    }

    bool verify(WiFiClient &client, const char *host) override {
        WiFiClientSecure &wcs = static_cast<WiFiClientSecure &>(client);
        if (_cacert == nullptr) {
            wcs.setInsecure();
        } else {
            wcs.setCACert(_cacert);
            wcs.setCertificate(_clicert);
            wcs.setPrivateKey(_clikey);
        }
        return true;
    }

  protected:
    const char *_cacert;
    const char *_clicert;
    const char *_clikey;
};
#endif // HTTPCLIENT_1_1_COMPATIBLE

HTTPClient::HTTPClient() {}

HTTPClient::~HTTPClient() {
    if (_client) {
        _client->stop();
    }
    if (_currentHeaders) {
        delete[] _currentHeaders;
    }
    if (_tcpDeprecated) {
        _tcpDeprecated.reset(nullptr);
    }
    if (_transportTraits) {
        _transportTraits.reset(nullptr);
    }
}

void HTTPClient::clear() {
    _returnCode = 0;
    _size       = -1;
    _headers    = "";
}

bool HTTPClient::begin(WiFiClient &client, String url) {
#ifdef HTTPCLIENT_1_1_COMPATIBLE
    if (_tcpDeprecated) {
        log_d("mix up of new and deprecated api");
        _canReuse = false;
        end();
    }
#endif

    _client = &client;

    // check for : (http: or https:)
    int index = url.indexOf(':');
    if (index < 0) {
        log_d("failed to parse protocol");
        return false;
    }

    String protocol = url.substring(0, index);
    if (protocol != "http" && protocol != "https") {
        log_d("unknown protocol '%s'", protocol.c_str());
        return false;
    }

    _port   = (protocol == "https" ? 443 : 80);
    _secure = (protocol == "https");
    return beginInternal(url, protocol.c_str());
}

bool HTTPClient::begin(WiFiClient &client, String host, uint16_t port, String uri, bool https) {
#ifdef HTTPCLIENT_1_1_COMPATIBLE
    if (_tcpDeprecated) {
        log_d("mix up of new and deprecated api");
        _canReuse = false;
        end();
    }
#endif

    _client = &client;

    clear();
    _host     = host;
    _port     = port;
    _uri      = uri;
    _protocol = (https ? "https" : "http");
    _secure   = https;
    return true;
}

#ifdef HTTPCLIENT_1_1_COMPATIBLE
bool HTTPClient::begin(String url, const char *CAcert) {
    if (_client && !_tcpDeprecated) {
        log_d("mix up of new and deprecated api");
        _canReuse = false;
        end();
    }

    clear();
    _port = 443;
    if (!beginInternal(url, "https")) {
        return false;
    }
    _secure          = true;
    _transportTraits = TransportTraitsPtr(new TLSTraits(CAcert));
    if (!_transportTraits) {
        log_e("could not create transport traits");
        return false;
    }

    return true;
}

bool HTTPClient::begin(String url) {
    if (_client && !_tcpDeprecated) {
        log_d("mix up of new and deprecated api");
        _canReuse = false;
        end();
    }

    clear();
    _port = 80;
    if (!beginInternal(url, "http")) {
        return begin(url, (const char *)NULL);
    }
    _transportTraits = TransportTraitsPtr(new TransportTraits());
    if (!_transportTraits) {
        log_e("could not create transport traits");
        return false;
    }

    return true;
}
#endif // HTTPCLIENT_1_1_COMPATIBLE

bool HTTPClient::beginInternal(String url, const char *expectedProtocol) {
    log_v("url: %s", url.c_str());

    // check for : (http: or https:
    int index = url.indexOf(':');
    if (index < 0) {
        log_e("failed to parse protocol");
        return false;
    }

    _protocol = url.substring(0, index);
    if (_protocol != expectedProtocol) {
        log_d("unexpected protocol: %s, expected %s", _protocol.c_str(), expectedProtocol);
        return false;
    }

    url.remove(0, (index + 3)); // remove http:// or https://

    index = url.indexOf('/');
    if (index == -1) {
        index = url.length();
        url += '/';
    }
    String host = url.substring(0, index);
    url.remove(0, index); // remove host part

    // get Authorization
    index = host.indexOf('@');
    if (index >= 0) {
        // auth info
        String auth = host.substring(0, index);
        host.remove(0, index + 1); // remove auth part including @
        _base64Authorization = base64::encode(auth);
    }

    // get port
    index = host.indexOf(':');
    String the_host;
    if (index >= 0) {
        the_host = host.substring(0, index); // hostname
        host.remove(0, (index + 1));         // remove hostname + :
        _port = host.toInt();                // get port
    } else {
        the_host = host;
    }
    if (_host != the_host && connected()) {
        log_d("switching host from '%s' to '%s'. disconnecting first", _host.c_str(), the_host.c_str());
        _canReuse = false;
        disconnect(true);
    }
    _host = the_host;
    _uri  = url;
    log_d("protocol: %s, host: %s port: %d url: %s", _protocol.c_str(), _host.c_str(), _port, _uri.c_str());
    return true;
}

#ifdef HTTPCLIENT_1_1_COMPATIBLE
bool HTTPClient::begin(String host, uint16_t port, String uri) {
    if (_client && !_tcpDeprecated) {
        log_d("mix up of new and deprecated api");
        _canReuse = false;
        end();
    }

    clear();
    _host            = host;
    _port            = port;
    _uri             = uri;
    _transportTraits = TransportTraitsPtr(new TransportTraits());
    log_d("host: %s port: %d uri: %s", host.c_str(), port, uri.c_str());
    return true;
}

bool HTTPClient::begin(String host, uint16_t port, String uri, const char *CAcert) {
    if (_client && !_tcpDeprecated) {
        log_d("mix up of new and deprecated api");
        _canReuse = false;
        end();
    }

    clear();
    _host = host;
    _port = port;
    _uri  = uri;

    if (strlen(CAcert) == 0) {
        return false;
    }
    _secure          = true;
    _transportTraits = TransportTraitsPtr(new TLSTraits(CAcert));
    return true;
}

bool HTTPClient::begin(
    String host, uint16_t port, String uri, const char *CAcert, const char *cli_cert, const char *cli_key
) {
    if (_client && !_tcpDeprecated) {
        log_d("mix up of new and deprecated api");
        _canReuse = false;
        end();
    }

    clear();
    _host = host;
    _port = port;
    _uri  = uri;

    if (strlen(CAcert) == 0) {
        return false;
    }
    _secure          = true;
    _transportTraits = TransportTraitsPtr(new TLSTraits(CAcert, cli_cert, cli_key));
    return true;
}
#endif // HTTPCLIENT_1_1_COMPATIBLE

void HTTPClient::end(void) {
    disconnect(false);
    clear();
}

void HTTPClient::disconnect(bool preserveClient) {
    if (connected()) {
        if (_client->available() > 0) {
            log_d("still data in buffer (%d), clean up.\n", _client->available());
            _client->flush();
        }

        if (_reuse && _canReuse) {
            log_d("tcp keep open for reuse");
        } else {
            log_d("tcp stop");
            _client->stop();
            if (!preserveClient) {
                _client = nullptr;
#ifdef HTTPCLIENT_1_1_COMPATIBLE
                if (_tcpDeprecated) {
                    _transportTraits.reset(nullptr);
                    _tcpDeprecated.reset(nullptr);
                }
#endif
            }
        }
    } else {
        log_d("tcp is closed\n");
    }
}

bool HTTPClient::connected() {
    if (_client) {
        return ((_client->available() > 0) || _client->connected());
    }
    return false;
}

void HTTPClient::setReuse(bool reuse) {
    _reuse = reuse;
}

void HTTPClient::setUserAgent(const String &userAgent) {
    _userAgent = userAgent;
}

void HTTPClient::setAuthorization(const char *user, const char *password) {
    if (user && password) {
        String auth = user;
        auth += ":";
        auth += password;
        _base64Authorization = base64::encode(auth);
    }
}

void HTTPClient::setAuthorization(const char *auth) {
    if (auth) {
        _base64Authorization = auth;
    }
}

void HTTPClient::setAuthorizationType(const char *authType) {
    if (authType) {
        _authorizationType = authType;
    }
}

void HTTPClient::setConnectTimeout(int32_t connectTimeout) {
    _connectTimeout = connectTimeout;
}

void HTTPClient::setTimeout(uint16_t timeout) {
    _tcpTimeout = timeout;
    if (connected()) {
        _client->setTimeout((timeout + 500) / 1000);
    }
}

void HTTPClient::useHTTP10(bool useHTTP10) {
    _useHTTP10 = useHTTP10;
    _reuse     = !useHTTP10;
}

int HTTPClient::GET() {
    return sendRequest("GET");
}

int HTTPClient::POST(uint8_t *payload, size_t size) {
    return sendRequest("POST", payload, size);
}

int HTTPClient::POST(String payload) {
    return POST((uint8_t *)payload.c_str(), payload.length());
}

int HTTPClient::PATCH(uint8_t *payload, size_t size) {
    return sendRequest("PATCH", payload, size);
}

int HTTPClient::PATCH(String payload) {
    return PATCH((uint8_t *)payload.c_str(), payload.length());
}

int HTTPClient::PUT(uint8_t *payload, size_t size) {
    return sendRequest("PUT", payload, size);
}

int HTTPClient::PUT(String payload) {
    return PUT((uint8_t *)payload.c_str(), payload.length());
}

int HTTPClient::sendRequest(const char *type, String payload) {
    return sendRequest(type, (uint8_t *)payload.c_str(), payload.length());
}

int HTTPClient::sendRequest(const char *type, uint8_t *payload, size_t size) {
    int code;
    bool redirect          = false;
    uint16_t redirectCount = 0;
    do {
        // wipe out any existing headers from previous request
        for (size_t i = 0; i < _headerKeysCount; i++) {
            if (_currentHeaders[i].value.length() > 0) {
                _currentHeaders[i].value = ""; // LT: changed from clear()
            }
        }

        log_d("request type: '%s' redirCount: %d\n", type, redirectCount);

        // connect to server
        if (!connect()) {
            return returnError(HTTPC_ERROR_CONNECTION_REFUSED);
        }

        if (payload && size > 0) {
            addHeader(F("Content-Length"), String(size));
        }

        // add cookies to header, if present
        String cookie_string;
        if (generateCookieString(&cookie_string)) {
            addHeader("Cookie", cookie_string);
        }

        // send Header
        if (!sendHeader(type)) {
            return returnError(HTTPC_ERROR_SEND_HEADER_FAILED);
        }

        // send Payload if needed
        if (payload && size > 0) {
            if (_client->write(&payload[0], size) != size) {
                return returnError(HTTPC_ERROR_SEND_PAYLOAD_FAILED);
            }
        }

        code = handleHeaderResponse();
        log_d("sendRequest code=%d\n", code);

        // Handle redirections as stated in RFC document:
        // https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
        //
        // Implementing HTTP_CODE_FOUND as redirection with GET method,
        // to follow most of existing user agent implementations.
        //
        redirect = false;
        if (_followRedirects != HTTPC_DISABLE_FOLLOW_REDIRECTS && redirectCount < _redirectLimit &&
            _location.length() > 0) {
            switch (code) {
                // redirecting using the same method
                case HTTP_CODE_MOVED_PERMANENTLY:
                case HTTP_CODE_TEMPORARY_REDIRECT: {
                    if (
                        // allow to force redirections on other methods
                        // (the RFC require user to accept the redirection)
                        _followRedirects == HTTPC_FORCE_FOLLOW_REDIRECTS ||
                        // allow GET and HEAD methods without force
                        !strcmp(type, "GET") ||
                        !strcmp(type, "HEAD")
                    ) {
                        redirectCount += 1;
                        log_d(
                            "following redirect (the same method): '%s' redirCount: %d\n",
                            _location.c_str(),
                            redirectCount
                        );
                        if (!setURL(_location)) {
                            log_d("failed setting URL for redirection\n");
                            // no redirection
                            break;
                        }
                        // redirect using the same request method and payload, diffrent URL
                        redirect = true;
                    }
                    break;
                }
                // redirecting with method dropped to GET or HEAD
                // note: it does not need `HTTPC_FORCE_FOLLOW_REDIRECTS` for any method
                case HTTP_CODE_FOUND:
                case HTTP_CODE_SEE_OTHER: {
                    redirectCount += 1;
                    log_d(
                        "following redirect (dropped to GET/HEAD): '%s' redirCount: %d\n",
                        _location.c_str(),
                        redirectCount
                    );
                    if (!setURL(_location)) {
                        log_d("failed setting URL for redirection\n");
                        // no redirection
                        break;
                    }
                    // redirect after changing method to GET/HEAD and dropping payload
                    type     = "GET";
                    payload  = nullptr;
                    size     = 0;
                    redirect = true;
                    break;
                }

                default:
                    break;
            }
        }

    } while (redirect);
    // handle Server Response (Header)
    return returnError(code);
}

int HTTPClient::sendRequest(const char *type, Stream *stream, size_t size) {

    if (!stream) {
        return returnError(HTTPC_ERROR_NO_STREAM);
    }

    // connect to server
    if (!connect()) {
        return returnError(HTTPC_ERROR_CONNECTION_REFUSED);
    }

    if (size > 0) {
        addHeader("Content-Length", String(size));
    }

    // add cookies to header, if present
    String cookie_string;
    if (generateCookieString(&cookie_string)) {
        addHeader("Cookie", cookie_string);
    }

    // send Header
    if (!sendHeader(type)) {
        return returnError(HTTPC_ERROR_SEND_HEADER_FAILED);
    }

    int buff_size = HTTP_TCP_BUFFER_SIZE;

    int len          = size;
    int bytesWritten = 0;

    if (len == 0) {
        len = -1;
    }

    // if possible create smaller buffer then HTTP_TCP_BUFFER_SIZE
    if ((len > 0) && (len < HTTP_TCP_BUFFER_SIZE)) {
        buff_size = len;
    }

    // create buffer for read
    uint8_t *buff = (uint8_t *)malloc(buff_size);

    if (buff) {
        // read all data from stream and send it to server
        while (connected() && (stream->available() > -1) && (len > 0 || len == -1)) {

            // get available data size
            int sizeAvailable = stream->available();

            if (sizeAvailable) {

                int readBytes = sizeAvailable;

                // read only the asked bytes
                if (len > 0 && readBytes > len) {
                    readBytes = len;
                }

                // not read more the buffer can handle
                if (readBytes > buff_size) {
                    readBytes = buff_size;
                }

                // read data
                int bytesRead = stream->readBytes(buff, readBytes);

                // write it to Stream
                int bytesWrite = _client->write((const uint8_t *)buff, bytesRead);
                bytesWritten += bytesWrite;

                // are all Bytes a writen to stream ?
                if (bytesWrite != bytesRead) {
                    log_d("short write, asked for %d but got %d retry...", bytesRead, bytesWrite);

                    // check for write error
                    if (_client->getWriteError()) {
                        log_d("stream write error %d", _client->getWriteError());

                        // reset write error for retry
                        _client->clearWriteError();
                    }

                    // some time for the stream
                    delay(1);

                    int leftBytes = (readBytes - bytesWrite);

                    // retry to send the missed bytes
                    bytesWrite = _client->write((const uint8_t *)(buff + bytesWrite), leftBytes);
                    bytesWritten += bytesWrite;

                    if (bytesWrite != leftBytes) {
                        // failed again
                        log_d("short write, asked for %d but got %d failed.", leftBytes, bytesWrite);
                        free(buff);
                        return returnError(HTTPC_ERROR_SEND_PAYLOAD_FAILED);
                    }
                }

                // check for write error
                if (_client->getWriteError()) {
                    log_d("stream write error %d", _client->getWriteError());
                    free(buff);
                    return returnError(HTTPC_ERROR_SEND_PAYLOAD_FAILED);
                }

                // count bytes to read left
                if (len > 0) {
                    len -= readBytes;
                }

                delay(0);
            } else {
                delay(1);
            }
        }

        free(buff);

        if (size && (int)size != bytesWritten) {
            log_d("Stream payload bytesWritten %d and size %d mismatch!.", bytesWritten, size);
            log_d("ERROR SEND PAYLOAD FAILED!");
            return returnError(HTTPC_ERROR_SEND_PAYLOAD_FAILED);
        } else {
            log_d("Stream payload written: %d", bytesWritten);
        }

    } else {
        log_d("too less ram! need %d", HTTP_TCP_BUFFER_SIZE);
        return returnError(HTTPC_ERROR_TOO_LESS_RAM);
    }

    // handle Server Response (Header)
    return returnError(handleHeaderResponse());
}

int HTTPClient::getSize(void) {
    return _size;
}

WiFiClient &HTTPClient::getStream(void) {
    if (connected()) {
        return *_client;
    }

    log_w("getStream: not connected");
    static WiFiClient empty;
    return empty;
}

WiFiClient *HTTPClient::getStreamPtr(void) {
    if (connected()) {
        return _client;
    }

    log_w("getStreamPtr: not connected");
    return nullptr;
}

int HTTPClient::writeToStream(Stream *stream) {

    if (!stream) {
        return returnError(HTTPC_ERROR_NO_STREAM);
    }

    if (!connected()) {
        return returnError(HTTPC_ERROR_NOT_CONNECTED);
    }

    // get length of document (is -1 when Server sends no Content-Length header)
    int len = _size;
    int ret = 0;

    if (_transferEncoding == HTTPC_TE_IDENTITY) {
        ret = writeToStreamDataBlock(stream, len);

        // have we an error?
        if (ret < 0) {
            return returnError(ret);
        }
    } else if (_transferEncoding == HTTPC_TE_CHUNKED) {
        int size = 0;
        while (1) {
            if (!connected()) {
                return returnError(HTTPC_ERROR_CONNECTION_LOST);
            }
            String chunkHeader = _client->readStringUntil('\n');

            if (chunkHeader.length() <= 0) {
                return returnError(HTTPC_ERROR_READ_TIMEOUT);
            }

            chunkHeader.trim(); // remove \r

            // read size of chunk
            len = (uint32_t)strtol((const char *)chunkHeader.c_str(), NULL, 16);
            size += len;
            log_d(" read chunk len: %d", len);

            // data left?
            if (len > 0) {
                int r = writeToStreamDataBlock(stream, len);
                if (r < 0) {
                    // error in writeToStreamDataBlock
                    return returnError(r);
                }
                ret += r;
            } else {

                // if no length Header use global chunk size
                if (_size <= 0) {
                    _size = size;
                }

                // check if we have write all data out
                if (ret != _size) {
                    return returnError(HTTPC_ERROR_STREAM_WRITE);
                }
                break;
            }

            // read trailing \r\n at the end of the chunk
            char buf[2];
            auto trailing_seq_len = _client->readBytes((uint8_t *)buf, 2);
            if (trailing_seq_len != 2 || buf[0] != '\r' || buf[1] != '\n') {
                return returnError(HTTPC_ERROR_READ_TIMEOUT);
            }

            delay(0);
        }
    } else {
        return returnError(HTTPC_ERROR_ENCODING);
    }

    //    end();
    disconnect(true);
    return ret;
}

/* String HTTPClient::getString(void) {
    // _size can be -1 when Server sends no Content-Length header
    if (_size > 0 || _size == -1) {
        StreamString sstring;
        // try to reserve needed memory (noop if _size == -1)
        if (sstring.reserve((_size + 1))) {
            writeToStream(&sstring);
            return sstring;
        } else {
            log_d("not enough memory to reserve a string! need: %d", (_size + 1));
        }
    }

    return "";
} */

String HTTPClient::errorToString(int error) {
    switch (error) {
        case HTTPC_ERROR_CONNECTION_REFUSED:
            return F("connection refused");
        case HTTPC_ERROR_SEND_HEADER_FAILED:
            return F("send header failed");
        case HTTPC_ERROR_SEND_PAYLOAD_FAILED:
            return F("send payload failed");
        case HTTPC_ERROR_NOT_CONNECTED:
            return F("not connected");
        case HTTPC_ERROR_CONNECTION_LOST:
            return F("connection lost");
        case HTTPC_ERROR_NO_STREAM:
            return F("no stream");
        case HTTPC_ERROR_NO_HTTP_SERVER:
            return F("no HTTP server");
        case HTTPC_ERROR_TOO_LESS_RAM:
            return F("too less ram");
        case HTTPC_ERROR_ENCODING:
            return F("Transfer-Encoding not supported");
        case HTTPC_ERROR_STREAM_WRITE:
            return F("Stream write error");
        case HTTPC_ERROR_READ_TIMEOUT:
            return F("read Timeout");
        default:
            return String();
    }
}

void HTTPClient::addHeader(const String &name, const String &value, bool first, bool replace) {
    // not allow set of Header handled by code
    if (!name.equalsIgnoreCase(F("Connection")) && !name.equalsIgnoreCase(F("User-Agent")) &&
        !name.equalsIgnoreCase(F("Host")) &&
        !(name.equalsIgnoreCase(F("Authorization")) && _base64Authorization.length())) {

        String headerLine = name;
        headerLine += ": ";

        if (replace) {
            int headerStart = _headers.indexOf(headerLine);
            if (headerStart != -1 && (headerStart == 0 || _headers[headerStart - 1] == '\n')) {
                int headerEnd = _headers.indexOf('\n', headerStart);
                _headers      = _headers.substring(0, headerStart) + _headers.substring(headerEnd + 1);
            }
        }

        headerLine += value;
        headerLine += "\r\n";
        if (first) {
            _headers = headerLine + _headers;
        } else {
            _headers += headerLine;
        }
    }
}

void HTTPClient::collectHeaders(const char *headerKeys[], const size_t headerKeysCount) {
    _headerKeysCount = headerKeysCount;
    if (_currentHeaders) {
        delete[] _currentHeaders;
    }
    _currentHeaders = new RequestArgument[_headerKeysCount];
    for (size_t i = 0; i < _headerKeysCount; i++) {
        _currentHeaders[i].key = headerKeys[i];
    }
}

String HTTPClient::header(const char *name) {
    for (size_t i = 0; i < _headerKeysCount; ++i) {
        if (_currentHeaders[i].key == name) {
            return _currentHeaders[i].value;
        }
    }
    return String();
}

String HTTPClient::header(size_t i) {
    if (i < _headerKeysCount) {
        return _currentHeaders[i].value;
    }
    return String();
}

String HTTPClient::headerName(size_t i) {
    if (i < _headerKeysCount) {
        return _currentHeaders[i].key;
    }
    return String();
}

int HTTPClient::headers() {
    return _headerKeysCount;
}

bool HTTPClient::hasHeader(const char *name) {
    for (size_t i = 0; i < _headerKeysCount; ++i) {
        if ((_currentHeaders[i].key == name) && (_currentHeaders[i].value.length() > 0)) {
            return true;
        }
    }
    return false;
}

bool HTTPClient::connect(void) {
    if (connected()) {
        if (_reuse) {
            log_d("already connected, reusing connection");
        } else {
            log_d("already connected, try reuse!");
        }
        while (_client->available() > 0) {
            _client->read();
        }
        return true;
    }

#ifdef HTTPCLIENT_1_1_COMPATIBLE
    if (_transportTraits && !_client) {
        _tcpDeprecated = _transportTraits->create();
        if (!_tcpDeprecated) {
            log_e("failed to create client");
            return false;
        }
        _client = _tcpDeprecated.get();
    }
#endif

    if (!_client) {
        log_d("HTTPClient::begin was not called or returned error");
        return false;
    }
#ifdef HTTPCLIENT_1_1_COMPATIBLE
    if (_tcpDeprecated && !_transportTraits->verify(*_client, _host.c_str())) {
        log_d("transport level verify failed");
        _client->stop();
        return false;
    }
#endif
    if (!_client->connect(_host.c_str(), _port, _connectTimeout)) {
        log_d("failed connect to %s:%u", _host.c_str(), _port);
        return false;
    }

    // set Timeout for WiFiClient and for Stream::readBytesUntil() and Stream::readStringUntil()
    _client->setTimeout((_tcpTimeout + 500) / 1000);

    log_d(" connected to %s:%u", _host.c_str(), _port);

    /*
    #ifdef ESP8266
        _client->setNoDelay(true);
    #endif
     */
    return connected();
}

bool HTTPClient::sendHeader(const char *type) {
    if (!connected()) {
        return false;
    }

    String header = String(type) + " " + _uri + F(" HTTP/1.");

    if (_useHTTP10) {
        header += "0";
    } else {
        header += "1";
    }

    header += String(F("\r\nHost: ")) + _host;
    if (_port != 80 && _port != 443) {
        header += ':';
        header += String(_port);
    }
    header += String(F("\r\nUser-Agent: ")) + _userAgent + F("\r\nConnection: ");

    if (_reuse) {
        header += F("keep-alive");
    } else {
        header += F("close");
    }
    header += "\r\n";

    if (!_useHTTP10) {
        header += F("Accept-Encoding: identity;q=1,chunked;q=0.1,*;q=0\r\n");
    }

    if (_base64Authorization.length()) {
        _base64Authorization.replace("\n", "");
        header += F("Authorization: ");
        header += _authorizationType;
        header += " ";
        header += _base64Authorization;
        header += "\r\n";
    }

    header += _headers + "\r\n";

    return (_client->write((const uint8_t *)header.c_str(), header.length()) == header.length());
}

int HTTPClient::handleHeaderResponse() {

    if (!connected()) {
        return HTTPC_ERROR_NOT_CONNECTED;
    }

    _returnCode = 0;
    _size       = -1;
    _canReuse   = _reuse;

    String transferEncoding;

    _transferEncoding          = HTTPC_TE_IDENTITY;
    unsigned long lastDataTime = millis();
    bool firstLine             = true;
    String date;

    while (connected()) {
        size_t len = _client->available();
        if (len > 0) {
            String headerLine = _client->readStringUntil('\n');
            headerLine.trim(); // remove \r

            lastDataTime = millis();

            log_v("RX: '%s'", headerLine.c_str());

            if (firstLine) {
                firstLine = false;
                if (_canReuse && headerLine.startsWith("HTTP/1.")) {
                    _canReuse = (headerLine[sizeof "HTTP/1." - 1] != '0');
                }
                int codePos = headerLine.indexOf(' ') + 1;
                _returnCode = headerLine.substring(codePos, headerLine.indexOf(' ', codePos)).toInt();
            } else if (headerLine.indexOf(':')) {
                String headerName  = headerLine.substring(0, headerLine.indexOf(':'));
                String headerValue = headerLine.substring(headerLine.indexOf(':') + 1);
                headerValue.trim();

                if (headerName.equalsIgnoreCase("Date")) {
                    date = headerValue;
                }

                if (headerName.equalsIgnoreCase("Content-Length")) {
                    _size = headerValue.toInt();
                }

                if (_canReuse && headerName.equalsIgnoreCase("Connection")) {
                    if (headerValue.indexOf("close") >= 0 && headerValue.indexOf("keep-alive") < 0) {
                        _canReuse = false;
                    }
                }

                if (headerName.equalsIgnoreCase("Transfer-Encoding")) {
                    transferEncoding = headerValue;
                }

                if (headerName.equalsIgnoreCase("Location")) {
                    _location = headerValue;
                }

                if (headerName.equalsIgnoreCase("Set-Cookie")) {
                    setCookie(date, headerValue);
                }

                for (size_t i = 0; i < _headerKeysCount; i++) {
                    if (_currentHeaders[i].key.equalsIgnoreCase(headerName)) {
                        // Uncomment the following lines if you need to add support for multiple headers with the same
                        // key: if (!_currentHeaders[i].value.isEmpty()) {
                        //     // Existing value, append this one with a comma
                        //     _currentHeaders[i].value += ',';
                        //     _currentHeaders[i].value += headerValue;
                        // } else {
                        _currentHeaders[i].value = headerValue;
                        // }
                        break; // We found a match, stop looking
                    }
                }
            }

            if (headerLine == "") {
                log_d("code: %d", _returnCode);

                if (_size > 0) {
                    log_d("size: %d", _size);
                }

                if (transferEncoding.length() > 0) {
                    log_d("Transfer-Encoding: %s", transferEncoding.c_str());
                    if (transferEncoding.equalsIgnoreCase("chunked")) {
                        _transferEncoding = HTTPC_TE_CHUNKED;
                    } else if (transferEncoding.equalsIgnoreCase("identity")) {
                        _transferEncoding = HTTPC_TE_IDENTITY;
                    } else {
                        return HTTPC_ERROR_ENCODING;
                    }
                } else {
                    _transferEncoding = HTTPC_TE_IDENTITY;
                }

                if (_returnCode) {
                    return _returnCode;
                } else {
                    log_d("Remote host is not an HTTP Server!");
                    return HTTPC_ERROR_NO_HTTP_SERVER;
                }
            }

        } else {
            if ((millis() - lastDataTime) > _tcpTimeout) {
                return HTTPC_ERROR_READ_TIMEOUT;
            }
            delay(10);
        }
    }

    return HTTPC_ERROR_CONNECTION_LOST;
}

int HTTPClient::writeToStreamDataBlock(Stream *stream, int size) {
    int buff_size    = HTTP_TCP_BUFFER_SIZE;
    int len          = size;
    int bytesWritten = 0;

    // if possible create smaller buffer then HTTP_TCP_BUFFER_SIZE
    if ((len > 0) && (len < HTTP_TCP_BUFFER_SIZE)) {
        buff_size = len;
    }

    // create buffer for read
    uint8_t *buff = (uint8_t *)malloc(buff_size);

    if (buff) {
        // read all data from server
        while (connected() && (len > 0 || len == -1)) {

            // get available data size
            size_t sizeAvailable = _client->available();

            if (sizeAvailable) {

                int readBytes = sizeAvailable;

                // read only the asked bytes
                if (len > 0 && readBytes > len) {
                    readBytes = len;
                }

                // not read more the buffer can handle
                if (readBytes > buff_size) {
                    readBytes = buff_size;
                }

                // stop if no more reading
                if (readBytes == 0)
                    break;

                // read data
                int bytesRead = _client->readBytes(buff, readBytes);

                // write it to Stream
                int bytesWrite = stream->write(buff, bytesRead);
                bytesWritten += bytesWrite;

                // are all Bytes a writen to stream ?
                if (bytesWrite != bytesRead) {
                    log_d("short write asked for %d but got %d retry...", bytesRead, bytesWrite);

                    // check for write error
                    if (stream->getWriteError()) {
                        log_d("stream write error %d", stream->getWriteError());

                        // reset write error for retry
                        stream->clearWriteError();
                    }

                    // some time for the stream
                    delay(1);

                    int leftBytes = (readBytes - bytesWrite);

                    // retry to send the missed bytes
                    bytesWrite = stream->write((buff + bytesWrite), leftBytes);
                    bytesWritten += bytesWrite;

                    if (bytesWrite != leftBytes) {
                        // failed again
                        log_w("short write asked for %d but got %d failed.", leftBytes, bytesWrite);
                        free(buff);
                        return HTTPC_ERROR_STREAM_WRITE;
                    }
                }

                // check for write error
                if (stream->getWriteError()) {
                    log_w("stream write error %d", stream->getWriteError());
                    free(buff);
                    return HTTPC_ERROR_STREAM_WRITE;
                }

                // count bytes to read left
                if (len > 0) {
                    len -= readBytes;
                }

                delay(0);
            } else {
                delay(1);
            }
        }

        free(buff);

        log_d("connection closed or file end (written: %d).", bytesWritten);

        if ((size > 0) && (size != bytesWritten)) {
            log_d("bytesWritten %d and size %d mismatch!.", bytesWritten, size);
            return HTTPC_ERROR_STREAM_WRITE;
        }

    } else {
        log_w("too less ram! need %d", HTTP_TCP_BUFFER_SIZE);
        return HTTPC_ERROR_TOO_LESS_RAM;
    }

    return bytesWritten;
}

int HTTPClient::returnError(int error) {
    if (error < 0) {
        log_w("error(%d): %s", error, errorToString(error).c_str());
        if (connected()) {
            log_d("tcp stop");
            _client->stop();
        }
    }
    return error;
}

void HTTPClient::setFollowRedirects(followRedirects_t follow) {
    _followRedirects = follow;
}

void HTTPClient::setRedirectLimit(uint16_t limit) {
    _redirectLimit = limit;
}

bool HTTPClient::setURL(const String &url) {
    // if the new location is only a path then only update the URI
    if (url && url[0] == '/') {
        _uri = url;
        clear();
        return true;
    }

    if (!url.startsWith(_protocol + ':')) {
        log_d("new URL not the same protocol, expected '%s', URL: '%s'\n", _protocol.c_str(), url.c_str());
        return false;
    }

    // check if the port is specified
    int indexPort = url.indexOf(':', 6); // find the first ':' excluding the one from the protocol
    int indexURI  = url.indexOf('/', 7); // find where the URI starts to make sure the ':' is not part of it
    if (indexPort == -1 || indexPort > indexURI) {
        // the port is not specified
        _port = (_protocol == "https" ? 443 : 80);
    }

    // disconnect but preserve _client.
    // Also have to keep the connection otherwise it will free some of the memory used by _client
    // and will blow up later when trying to do _client->available() or similar
    _canReuse = true;
    disconnect(true);
    return beginInternal(url, _protocol.c_str());
}

const String &HTTPClient::getLocation(void) {
    return _location;
}

void HTTPClient::setCookieJar(CookieJar *cookieJar) {
    _cookieJar = cookieJar;
}

void HTTPClient::resetCookieJar() {
    _cookieJar = nullptr;
}

void HTTPClient::clearAllCookies() {
    if (_cookieJar)
        _cookieJar->clear();
}

void HTTPClient::setCookie(String date, String headerValue) {
    if (!_cookieJar) {
        return;
    }
#define HTTP_TIME_PATTERN "%a, %d %b %Y %H:%M:%S"

    Cookie cookie;
    String value;
    int pos1, pos2;

    headerValue.toLowerCase();

    struct tm tm;
    strptime(date.c_str(), HTTP_TIME_PATTERN, &tm);
    cookie.date = mktime(&tm);

    pos1 = headerValue.indexOf('=');
    pos2 = headerValue.indexOf(';');

    if (pos1 >= 0 && pos2 > pos1) {
        cookie.name  = headerValue.substring(0, pos1);
        cookie.value = headerValue.substring(pos1 + 1, pos2);
    } else {
        return; // invalid cookie header
    }

    // expires
    if (headerValue.indexOf("expires=") >= 0) {
        pos1 = headerValue.indexOf("expires=") + strlen("expires=");
        pos2 = headerValue.indexOf(';', pos1);

        if (pos2 > pos1)
            value = headerValue.substring(pos1, pos2);
        else
            value = headerValue.substring(pos1);

        strptime(value.c_str(), HTTP_TIME_PATTERN, &tm);
        cookie.expires.date  = mktime(&tm);
        cookie.expires.valid = true;
    }

    // max-age
    if (headerValue.indexOf("max-age=") >= 0) {
        pos1 = headerValue.indexOf("max-age=") + strlen("max-age=");
        pos2 = headerValue.indexOf(';', pos1);

        if (pos2 > pos1)
            value = headerValue.substring(pos1, pos2);
        else
            value = headerValue.substring(pos1);

        cookie.max_age.duration = value.toInt();
        cookie.max_age.valid    = true;
    }

    // domain
    if (headerValue.indexOf("domain=") >= 0) {
        pos1 = headerValue.indexOf("domain=") + strlen("domain=");
        pos2 = headerValue.indexOf(';', pos1);

        if (pos2 > pos1)
            value = headerValue.substring(pos1, pos2);
        else
            value = headerValue.substring(pos1);

        if (value.startsWith("."))
            value.remove(0, 1);

        if (_host.indexOf(value) >= 0) {
            cookie.domain = value;
        } else {
            return; // server tries to set a cookie on a different domain; ignore it
        }
    } else {
        pos1 = _host.lastIndexOf('.', _host.lastIndexOf('.') - 1);
        if (pos1 >= 0)
            cookie.domain = _host.substring(pos1 + 1);
        else
            cookie.domain = _host;
    }

    // path
    if (headerValue.indexOf("path=") >= 0) {
        pos1 = headerValue.indexOf("path=") + strlen("path=");
        pos2 = headerValue.indexOf(';', pos1);

        if (pos2 > pos1)
            cookie.path = headerValue.substring(pos1, pos2);
        else
            cookie.path = headerValue.substring(pos1);
    }

    // HttpOnly
    cookie.http_only = (headerValue.indexOf("httponly") >= 0);

    // secure
    cookie.secure = (headerValue.indexOf("secure") >= 0);

    // overwrite or delete cookie in/from cookie jar
    time_t now_local = time(NULL);
    time_t now_gmt   = mktime(gmtime(&now_local));

    bool found = false;

    for (auto c = _cookieJar->begin(); c != _cookieJar->end(); ++c) {
        if (c->domain == cookie.domain && c->name == cookie.name) {
            // when evaluating, max-age takes precedence over expires if both are defined
            if ((cookie.max_age.valid && ((cookie.date + cookie.max_age.duration) < now_gmt)) ||
                cookie.max_age.duration <= 0 ||
                (!cookie.max_age.valid && cookie.expires.valid && cookie.expires.date < now_gmt)) {
                _cookieJar->erase(c);
                c--;
            } else {
                *c = cookie;
            }
            found = true;
        }
    }

    // add cookie to jar
    if (!found && !(cookie.max_age.valid && cookie.max_age.duration <= 0))
        _cookieJar->push_back(cookie);
}

bool HTTPClient::generateCookieString(String *cookieString) {
    time_t now_local = time(NULL);
    time_t now_gmt   = mktime(gmtime(&now_local));

    *cookieString = "";
    bool found    = false;

    if (!_cookieJar) {
        return false;
    }
    for (auto c = _cookieJar->begin(); c != _cookieJar->end(); ++c) {
        if ((c->max_age.valid && ((c->date + c->max_age.duration) < now_gmt)) ||
            (!c->max_age.valid && c->expires.valid && c->expires.date < now_gmt)) {
            _cookieJar->erase(c);
            c--;
        } else if (_host.indexOf(c->domain) >= 0 && (!c->secure || _secure)) {
            if (*cookieString == "")
                *cookieString = c->name + "=" + c->value;
            else
                *cookieString += " ;" + c->name + "=" + c->value;
            found = true;
        }
    }

    return found;
}

#endif // LT_ARD_HAS_WIFI