/* Copyright 2019 OSU OPEnS Lab
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this
 * software and associated documentation files (the "Software"), to deal in the Software
 * without restriction, including without limitation the rights to use, copy, modify,
 * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include "SSLClient.h"

#if defined(ARDUINO_ARCH_SAMD)
// system reset definitions
static constexpr auto SYSRESETREQ = (1<<2);
static constexpr auto VECTKEY = (0x05fa0000UL);
static constexpr auto VECTKEY_MASK = (0x0000ffffUL);
/** Trigger a software reset. Only use if in unrecoverable state */
[[ noreturn ]] static void RESET() {
    (*(uint32_t*)0xe000ed0cUL)=((*(uint32_t*)0xe000ed0cUL)&VECTKEY_MASK)|VECTKEY|SYSRESETREQ;
    while(1) { }
}
#endif

#ifdef __arm__
// should use uinstd.h to define sbrk but Due causes a conflict
extern "C" char* sbrk(int incr);
#else  // __ARM__
extern char *__brkval;
#endif  // __arm__
 
/** Get the free memory availible in bytes (stack - heap) */
static int freeMemory() {
  char top;
#ifdef __arm__
  return &top - reinterpret_cast<char*>(sbrk(0));
#elif defined(CORE_TEENSY) || (ARDUINO > 103 && ARDUINO != 151)
  return &top - __brkval;
#else  // __arm__
  return __brkval ? &top - __brkval : &top - __malloc_heap_start;
#endif  // __arm__
}

/* see SSLClient.h */
SSLClient::SSLClient(   Client& client, 
                        const br_x509_trust_anchor *trust_anchors, 
                        const size_t trust_anchors_num, 
                        const int analog_pin, 
                        const size_t max_sessions,
                        const DebugLevel debug)
    : m_client(client) 
    , m_sessions()
    , m_max_sessions(max_sessions)
    , m_analog_pin(analog_pin)
    , m_debug(debug)
    , m_is_connected(false)
    , m_write_idx(0) {

    setTimeout(30*1000);
    // zero the iobuf just in case it's still garbage
    memset(m_iobuf, 0, sizeof m_iobuf);
    // initlalize the various bearssl libraries so they're ready to go when we connect
    br_client_init_TLS12_only(&m_sslctx, &m_x509ctx, trust_anchors, trust_anchors_num);
    // comment the above line and uncomment the line below if you're having trouble connecting over SSL
    // br_ssl_client_init_full(&m_sslctx, &m_x509ctx, m_trust_anchors, m_trust_anchors_num);
    // check if the buffer size is half or full duplex
    constexpr auto duplex = sizeof m_iobuf <= BR_SSL_BUFSIZE_MONO ? 0 : 1;
    br_ssl_engine_set_buffer(&m_sslctx.eng, m_iobuf, sizeof m_iobuf, duplex);
}

/* see SSLClient.h*/
int SSLClient::connect(IPAddress ip, uint16_t port) {
    const char* func_name = __func__;
    // connection check
    if (get_arduino_client().connected()) {
        m_error("Cannot have two connections at the same time! Please create another SSLClient instance.", func_name);
        return -1;
    }
    // reset indexs for saftey
    m_write_idx = 0;
    // Warning for security
    m_warn("Using a raw IP Address for an SSL connection bypasses some important verification steps. You should use a domain name (www.google.com) whenever possible.", func_name);
    // first we need our hidden client member to negotiate the socket for us,
    // since most times socket functionality is implemented in hardeware.
    if (!get_arduino_client().connect(ip, port)) {
        m_error("Failed to connect using m_client. Are you connected to the internet?", func_name);
        setWriteError(SSL_CLIENT_CONNECT_FAIL);
        return 0;
    }
    m_info("Base client connected!", func_name);
    return m_start_ssl(nullptr);
}

/* see SSLClient.h*/
int SSLClient::connect(const char *host, uint16_t port) {
    const char* func_name = __func__;
    // connection check
    if (get_arduino_client().connected()) {
        m_error("Cannot have two connections at the same time! Please create another SSLClient instance.", func_name);
        return -1;
    }
    m_info("Client not connected, continuing...", func_name);
    // reset indexs for saftey
    m_write_idx = 0;
    // first, if we have a session, check if we're trying to resolve the same host
    // as before
    const bool connect_ok = get_arduino_client().connect(host, port);
    // first we need our hidden client member to negotiate the socket for us,
    // since most times socket functionality is implemented in hardeware.
    if (!connect_ok) {
        m_error("Failed to connect using m_client. Are you connected to the internet?", func_name);
        setWriteError(SSL_CLIENT_CONNECT_FAIL);
        return 0;
    }
    m_info("Base client connected!", func_name);
    // start ssl!
    return m_start_ssl(host, getSession(host));
}

/* see SSLClient.h*/
size_t SSLClient::write(const uint8_t *buf, size_t size) {
    const char* func_name = __func__;
    // check if the socket is still open and such
    if (!m_soft_connected(func_name) || !buf || !size) return 0;
    // add to the bearssl io buffer, simply appending whatever we want to write
    size_t alen;
    unsigned char *br_buf = br_ssl_engine_sendapp_buf(&m_sslctx.eng, &alen);
    size_t cur_idx = 0;
    bool did_overflow = false;
    // while there are still elements to write
    while (cur_idx < size) {
        // run until the ssl socket is ready to write, unless we've already written
        // to the buffer in which we conclude it's already safe to write
        if(m_write_idx == 0) {
            if (m_run_until(BR_SSL_SENDAPP) < 0) {
                m_error("Failed while waiting for the engine to enter BR_SSL_SENDAPP", func_name);
                return 0;
            }
            // reset the buffer pointer
            br_ssl_engine_sendapp_buf(&m_sslctx.eng, &alen);
        }
        // if we're about to fill the buffer, we need to send the data and then wait
        // for another oppurtinity to send
        // so we only send the smallest of the buffer size or our data size - how much we've already sent
        const size_t cpamount = m_write_idx + (size - cur_idx) > alen ? alen : size - cur_idx;
        memcpy(br_buf + m_write_idx, buf + cur_idx, cpamount);
        // if we filled the buffer, reset m_write_idx, and mark the data for sending
        // or if we've overflowed since we're writing to the network already we may as well finish
        if (cpamount == alen || did_overflow) {
            m_write_idx = 0;
            br_ssl_engine_sendapp_ack(&m_sslctx.eng, alen);
            did_overflow = true;
        }
        // else increment
        else m_write_idx += cpamount;
        // increment the buffer pointer
        cur_idx += cpamount;
    } 
    // works oky
    return size;
}

/* see SSLClient.h*/
int SSLClient::available() {
    const char* func_name = __func__;
    // connection check
    if (!m_soft_connected(func_name)) return 0;
    // run the SSL engine until we are waiting for either user input or a server response
    unsigned state = m_update_engine();
    if (state == 0) m_error("SSL engine failed to update.", func_name);
    else if(state & BR_SSL_RECVAPP) {
        // return how many received bytes we have
        size_t alen;
        br_ssl_engine_recvapp_buf(&m_sslctx.eng, &alen);
        return (int)(alen);
    }
    else if (state == BR_SSL_CLOSED) m_info("Engine closed after update", func_name);
    // flush the buffer if it's stuck in the SENDAPP state
    else if (state & BR_SSL_SENDAPP) br_ssl_engine_flush(&m_sslctx.eng, 0);
    // other state, or client is closed
    return 0;
}

/* see SSLClient.h */
int SSLClient::read(uint8_t *buf, size_t size) {
    // check that the engine is ready to read
    if (available() <= 0 || !size) return -1;
    // read the buffer, send the ack, and return the bytes read
    size_t alen;
    unsigned char* br_buf = br_ssl_engine_recvapp_buf(&m_sslctx.eng, &alen);
    const size_t read_amount = size > alen ? alen : size;
    if(buf) memcpy(buf, br_buf, read_amount);
    // tell engine we read that many bytes
    br_ssl_engine_recvapp_ack(&m_sslctx.eng, read_amount);
    // tell the user we read that many bytes
    return read_amount;
}

/* see SSLClient.h */
int SSLClient::peek() {
    // check that the engine is ready to read
    if (available() <= 0) return -1; 
    // read the buffer, send the ack, and return the bytes read
    size_t alen;
    uint8_t read_num;
    read_num = br_ssl_engine_recvapp_buf(&m_sslctx.eng, &alen)[0];
    // tell the user we read that many bytes
    return (int)read_num;
}

/* see SSLClient.h */
void SSLClient::flush() {
    if (m_write_idx > 0)
        if(m_run_until(BR_SSL_RECVAPP) < 0) m_error("Could not flush write buffer!", __func__);
}

/* see SSLClient.h */
void SSLClient::stop() {
    // tell the SSL connection to gracefully close
    br_ssl_engine_close(&m_sslctx.eng);
    // if the engine isn't closed, and the socket is still open
    const auto state = br_ssl_engine_current_state(&m_sslctx.eng);
    while (getWriteError() == SSL_OK
        && m_is_connected
        && state != BR_SSL_CLOSED
        && state != 0
        && m_run_until(BR_SSL_SENDAPP | BR_SSL_RECVAPP) == 0) {
        /*
		 * Discard any incoming application data.
		 */
		size_t len;

		if (br_ssl_engine_recvapp_buf(&m_sslctx.eng, &len) != nullptr) {
			br_ssl_engine_recvapp_ack(&m_sslctx.eng, len);
		}
	}
    // close the ethernet socket
    get_arduino_client().flush();
    get_arduino_client().stop();
    // we are no longer connected 
    m_is_connected = false;
}

/* see SSLClient.h */
uint8_t SSLClient::connected() {
    const char* func_name = __func__;
    // check all of the error cases 
    const auto c_con = get_arduino_client().connected();
    const auto br_con = br_ssl_engine_current_state(&m_sslctx.eng) != BR_SSL_CLOSED && m_is_connected;
    const auto wr_ok = getWriteError() == 0;
    // if we're in an error state, close the connection and set a write error
    if (br_con && !c_con) {
        // If we've got a write error, the client probably failed for some reason
        if (get_arduino_client().getWriteError()) {
            m_error("Socket was unexpectedly interrupted. m_client error: ", func_name);
            m_error(get_arduino_client().getWriteError(), func_name);
            setWriteError(SSL_CLIENT_WRTIE_ERROR);
        }
        // Else tell the user the endpoint closed the socket on us (ouch)
        else {
            m_warn("Socket was dropped unexpectedly (this can be an alternative to closing the connection)", func_name);
        }
        // we are not connected
        m_is_connected = false;
        // set the write error so the engine doesn't try to close the connection
        stop();
    }
    else if (!wr_ok) {
        m_error("Not connected because write error is set", func_name);
        m_print_ssl_error(getWriteError(), SSL_ERROR);
    }
    return c_con && br_con;
}

/* see SSLClient.h */
SSLSession* SSLClient::getSession(const char* host) {
    const char* func_name = __func__;
    // search for a matching session with the IP
    int temp_index = m_get_session_index(host);
    // if none are availible, use m_session_index
    if (temp_index < 0) return nullptr;
    // return the pointed to value
    m_info("Using session index: ", func_name);
    m_info(temp_index, func_name);
    return &(m_sessions[temp_index]);
}

/* see SSLClient.h */
void SSLClient::removeSession(const char* host) {
    const char* func_name = __func__;
    int temp_index = m_get_session_index(host);
    if (temp_index >= 0) {
        m_info(" Deleted session ", func_name);
        m_info(temp_index, func_name);
        m_sessions.erase(m_sessions.begin() + static_cast<size_t>(temp_index));
    }
}

/* see SSLClient.h */
void SSLClient::setMutualAuthParams(const SSLClientParameters* params) {
    // if mutual authentication if needed, configure bearssl to support it.
    if (params != nullptr)
        br_ssl_client_set_single_ec(    &m_sslctx, 
                                        params->client_cert_chain, 
                                        params->chain_len,
                                        &params->ec_key,
                                        BR_KEYTYPE_KEYX | BR_KEYTYPE_SIGN,
                                        BR_KEYTYPE_EC,
                                        br_ssl_engine_get_ec(&m_sslctx.eng),
                                        &br_ecdsa_i15_sign_asn1);
}

bool SSLClient::m_soft_connected(const char* func_name) {
    // check if the socket is still open and such
    if (getWriteError()) {
        m_error("Cannot operate if the write error is not reset: ", func_name); 
        m_print_ssl_error(getWriteError(), SSL_ERROR);      
        return false;
    }
    // check if the ssl engine is still open
    if(!m_is_connected || br_ssl_engine_current_state(&m_sslctx.eng) == BR_SSL_CLOSED) {
        m_error("Cannot operate on a closed SSL connection.", func_name);
        int error = br_ssl_engine_last_error(&m_sslctx.eng);
        if(error != BR_ERR_OK) m_print_br_error(error, SSL_ERROR);   
        return false;
    }
    return true;
}

/* see SSLClient.h */
int SSLClient::m_start_ssl(const char* host, SSLSession* ssl_ses) {
    const char* func_name = __func__;
    // clear the write error
    setWriteError(SSL_OK);
    // get some random data by reading the analog pin we've been handed
    // we want 128 bits to be safe, as recommended by the bearssl docs
    uint8_t rng_seeds[16];
    // take the bottom 8 bits of the analog read
    for (uint8_t i = 0; i < sizeof rng_seeds; i++) 
        rng_seeds[i] = static_cast<uint8_t>(analogRead(m_analog_pin));
    br_ssl_engine_inject_entropy(&m_sslctx.eng, rng_seeds, sizeof rng_seeds);
    // inject session parameters for faster reconnection, if we have any
    if(ssl_ses != nullptr) {
        br_ssl_engine_set_session_parameters(&m_sslctx.eng, ssl_ses->to_br_session());
        m_info("Set SSL session!", func_name);
    }
    // reset the engine, but make sure that it reset successfully
    int ret = br_ssl_client_reset(&m_sslctx, host, 1);
    if (!ret) {
        m_error("Reset of bearSSL failed (is bearssl setup properly?)", func_name);
        m_print_br_error(br_ssl_engine_last_error(&m_sslctx.eng), SSL_ERROR);
        setWriteError(SSL_BR_CONNECT_FAIL);
        return 0;
    }
    // initialize the SSL socket over the network
    // normally this would happen in write, but I think it makes
    // a little more structural sense to put it here
    if (m_run_until(BR_SSL_SENDAPP) < 0) {
		m_error("Failed to initlalize the SSL layer", func_name);
        m_print_br_error(br_ssl_engine_last_error(&m_sslctx.eng), SSL_ERROR);
        return 0;
	}
    m_info("Connection successful!", func_name);
    m_is_connected = true;
    // all good to go! the SSL socket should be up and running
    // overwrite the session we got with new parameters
    if (ssl_ses != nullptr)
        br_ssl_engine_get_session_parameters(&m_sslctx.eng, ssl_ses->to_br_session());
    else if (host != nullptr) {
        if (m_sessions.size() >= m_max_sessions)
            m_sessions.erase(m_sessions.begin());
        SSLSession session(host);
        br_ssl_engine_get_session_parameters(&m_sslctx.eng, session.to_br_session());
        m_sessions.push_back(session);
    }
    return 1;
}

/* see SSLClient.h */
int SSLClient::m_run_until(const unsigned target) {
    const char* func_name = __func__;
    unsigned lastState = 0;
    size_t lastLen = 0;
    const unsigned long start = millis();
    for (;;) {
        unsigned state = m_update_engine();
	// error check
        if (state == BR_SSL_CLOSED || getWriteError() != SSL_OK) {
            return -1;
        }
        // timeout check
        if (millis() - start > getTimeout()) {
            m_error("SSL internals timed out! This could be an internal error, bad data sent from the server, or data being discarded due to a buffer overflow. If you are using Ethernet, did you modify the library properly (see README)?", func_name);
            setWriteError(SSL_BR_WRITE_ERROR);
            stop();
            return -1;
        }
        // debug
        if (state != lastState || lastState == 0) {
            lastState = state;
            m_info("m_run changed state:", func_name);
            if(m_debug == DebugLevel::SSL_INFO) {
                m_info("State: ", __func__);
                if(state == 0) Serial.println("    Invalid");
                else if (state & BR_SSL_CLOSED) Serial.println("   Connection closed");
                else {
                    if (state & BR_SSL_SENDREC) Serial.println("   SENDREC");
                    if (state & BR_SSL_RECVREC) Serial.println("   RECVREC");
                    if (state & BR_SSL_SENDAPP) Serial.println("   SENDAPP");
                    if (state & BR_SSL_RECVAPP) Serial.println("   RECVAPP");
                }
            }
        }
        if (state & BR_SSL_RECVREC) {
            size_t len;
            br_ssl_engine_recvrec_buf(&m_sslctx.eng, &len);
            if (lastLen != len) {
                m_info("Expected bytes count: ", func_name);
                m_info(lastLen = len, func_name);
            }
        }
        /*
		 * If we reached our target, then we are finished.
		 */
		if (state & target || (target == 0 && state == 0)) return 0;

		/*
		 * If some application data must be read, and we did not
		 * exit, then this means that we are trying to write data,
		 * and that's not possible until the application data is
		 * read. This may happen if using a shared in/out buffer,
		 * and the underlying protocol is not strictly half-duplex.
		 * Normally this would be unrecoverable, however we can attempt
         * to remedy the problem by telling the engine to discard 
         * the data.
		 */
		if (state & BR_SSL_RECVAPP && target & BR_SSL_SENDAPP) {
            size_t len;
            if (br_ssl_engine_recvapp_buf(&m_sslctx.eng, &len) != nullptr) {
                m_write_idx = 0;
                m_warn("Discarded unread data to favor a write operation", func_name);
                br_ssl_engine_recvapp_ack(&m_sslctx.eng, len);
                continue;
            }
            else {
                m_error("SSL engine state is RECVAPP, however the buffer was null! (This is a problem with BearSSL internals)", func_name);
                setWriteError(SSL_BR_WRITE_ERROR);
                stop();
                return -1;
            }
        }

		/*
		 * We can reach that point if the target RECVAPP, and
		 * the state contains SENDAPP only. This may happen with
		 * a shared in/out buffer. In that case, we must flush
		 * the buffered data to "make room" for a new incoming
		 * record.
		 */
		if (state & BR_SSL_SENDAPP && target & BR_SSL_RECVAPP) br_ssl_engine_flush(&m_sslctx.eng, 0);
    }
}

/* see SSLClient.h*/
unsigned SSLClient::m_update_engine() {
    const char* func_name = __func__;
    for(;;) {
        // get the state
        unsigned state = br_ssl_engine_current_state(&m_sslctx.eng);
        if (state & BR_SSL_CLOSED) return state;
        /*
        * If there is some record data to send, do it. This takes
        * precedence over everything else.
        */
        if (state & BR_SSL_SENDREC) {
            unsigned char *buf;
            size_t len;
            int wlen;

            buf = br_ssl_engine_sendrec_buf(&m_sslctx.eng, &len);
            wlen = get_arduino_client().write(buf, len);
            if (wlen <= 0) {
                // if the arduino client encountered an error
                if (get_arduino_client().getWriteError() || !get_arduino_client().connected()) {
                    m_error("Error writing to m_client", func_name);
                    m_error(get_arduino_client().getWriteError(), func_name);
                    setWriteError(SSL_CLIENT_WRTIE_ERROR);
                }
                // else presumably the socket just closed itself, so just stop the engine
                stop();
                return 0;
            }
            if (wlen > 0) {
                br_ssl_engine_sendrec_ack(&m_sslctx.eng, wlen);
            }
	    continue;
        }
        
        /*
         * If the client has specified there is client data to send, and 
         * the engine is ready to handle it, send it along.
         */
        if (m_write_idx > 0) {
            // if we've reached the point where BR_SSL_SENDAPP is off but
            // data has been written to the io buffer, something is wrong
            if (!(state & BR_SSL_SENDAPP)) {
                m_error("Error m_write_idx > 0 but the ssl engine is not ready for data", func_name);
                m_error(br_ssl_engine_current_state(&m_sslctx.eng), func_name);
                m_error(br_ssl_engine_last_error(&m_sslctx.eng), func_name);
                setWriteError(SSL_BR_WRITE_ERROR);
                stop();
                return 0;
            }
            // else time to send the application data
            else if (state & BR_SSL_SENDAPP) {
	            size_t alen;
                unsigned char *buf = br_ssl_engine_sendapp_buf(&m_sslctx.eng, &alen);
                // engine check
                if (alen == 0 || buf == nullptr) {
                    m_error("Engine set write flag but returned null buffer", func_name);
                    setWriteError(SSL_BR_WRITE_ERROR);
                    stop();
                    return 0;
                }
                // sanity check
                if (alen < m_write_idx) {
                    m_error("Alen is less than m_write_idx", func_name);
                    setWriteError(SSL_INTERNAL_ERROR);
                    stop();
                    return 0;
                }
                // all good? lets send the data
                // presumably the SSLClient::write function has already added
                // data to *buf, so now we tell bearssl it's time for the
                // encryption step.
                // this will encrypt the data and presumably spit it out
                // for BR_SSL_SENDREC to send over ethernet.
                br_ssl_engine_sendapp_ack(&m_sslctx.eng, m_write_idx);
                // reset the iobuffer index
                m_write_idx = 0;
                // loop again!
                continue;
            }
        }
        
        /*
         * If there is some record data to recieve, check if we've
         * recieved it so far. If we have, then we can update the state.
         * else we can return that we're still waiting for the server.
         */
        if (state & BR_SSL_RECVREC) {
			size_t len;
			unsigned char * buf = br_ssl_engine_recvrec_buf(&m_sslctx.eng, &len);
            // do we have the record you're looking for?
            const auto avail = get_arduino_client().available();
            if (avail > 0 && static_cast<size_t>(avail) >= len) {
                int mem = freeMemory();
#if defined(ARDUINO_ARCH_SAMD)
                // check for a stack overflow
                // if the stack overflows we basically have to crash, and
                // hope the user is ok with that
                // since all memory is garbage we can't trust the cpu to
                // execute anything properly
                if (mem > 32 * 1024) {
                    // software reset
                    RESET();
                }
#endif
                // debug info 
                m_info("Memory: ", func_name);
                m_info(mem, func_name);
                // free memory check
                // BearSSL takes up so much memory on the stack it tends
                // to overflow if there isn't at least 7000 bytes available
                // when it starts
                if(mem < 7000) {
                    m_error("Out of memory! Decrease the number of sessions or the size of m_iobuf", func_name);
                    setWriteError(SSL_OUT_OF_MEMORY);
                    stop();
                    return 0;
                }
                // I suppose so!
                int rlen = get_arduino_client().read(buf, len);
                if (rlen <= 0) {
                    m_error("Error reading bytes from m_client. Write Error: ", func_name);
                    m_error(get_arduino_client().getWriteError(), func_name);
                    setWriteError(SSL_CLIENT_WRTIE_ERROR);
                    stop();
                    return 0;
                }
                if (rlen > 0) {
                    br_ssl_engine_recvrec_ack(&m_sslctx.eng, rlen);
                }
                continue;
            }
            // guess not, tell the state we're waiting still
			else {
                // m_print("Bytes avail: ");
                // m_print(avail);
                // m_print("Bytes needed: ");
                // m_print(len);
                // add a delay since spamming get_arduino_client().availible breaks the poor wiz chip
                delay(10);
                return state;
            }
        }
        // if it's not any of the above states, then it must be waiting to send or recieve app data
        // in which case we return 
        return state;
    }
}

/* see SSLClientImpl.h */
int SSLClient::m_get_session_index(const char* host) const {
    const char* func_name = __func__;
    if(host == nullptr) return -1;
    // search for a matching session with the IP
    for (uint8_t i = 0; i < getSessionCount(); i++) {
        // if we're looking at a real session
        if (m_sessions[i].get_hostname().equals(host)) {
            m_info(m_sessions[i].get_hostname(), func_name);
            return i;
        }
    }
    // none found
    return -1;
}

/* See SSLClient.h */
void SSLClient::m_print_prefix(const char* func_name, const DebugLevel level) const
{
    // print the sslclient prefix
    Serial.print("(SSLClient)");
    // print the debug level
    switch (level) {
        case SSL_INFO: Serial.print("(SSL_INFO)"); break;
        case SSL_WARN: Serial.print("(SSL_WARN)"); break;
        case SSL_ERROR: Serial.print("(SSL_ERROR)"); break;
        default: Serial.print("(Unknown level)");
    }
    // print the function name
    Serial.print("(");
    Serial.print(func_name);
    Serial.print("): ");
}

/* See SSLClient.h */
void SSLClient::m_print_ssl_error(const int ssl_error, const DebugLevel level) const {
    if (level > m_debug) return;
    m_print_prefix(__func__, level);
    switch(ssl_error) {
        case SSL_OK: Serial.println("SSL_OK"); break;
        case SSL_CLIENT_CONNECT_FAIL: Serial.println("SSL_CLIENT_CONNECT_FAIL"); break;
        case SSL_BR_CONNECT_FAIL: Serial.println("SSL_BR_CONNECT_FAIL"); break;
        case SSL_CLIENT_WRTIE_ERROR: Serial.println("SSL_CLIENT_WRITE_FAIL"); break;
        case SSL_BR_WRITE_ERROR: Serial.println("SSL_BR_WRITE_ERROR"); break;
        case SSL_INTERNAL_ERROR: Serial.println("SSL_INTERNAL_ERROR"); break;
        case SSL_OUT_OF_MEMORY: Serial.println("SSL_OUT_OF_MEMORY"); break;
    }
}

/* See SSLClient.h */
void SSLClient::m_print_br_error(const unsigned br_error_code, const DebugLevel level) const {
  if (level > m_debug) return;
  m_print_prefix(__func__, level);
  switch (br_error_code) {
    case BR_ERR_BAD_PARAM: Serial.println("Caller-provided parameter is incorrect."); break;
    case BR_ERR_BAD_STATE: Serial.println("Operation requested by the caller cannot be applied with the current context state (e.g. reading data while outgoing data is waiting to be sent)."); break;
    case BR_ERR_UNSUPPORTED_VERSION: Serial.println("Incoming protocol or record version is unsupported."); break;
    case BR_ERR_BAD_VERSION: Serial.println("Incoming record version does not match the expected version."); break;
    case BR_ERR_BAD_LENGTH: Serial.println("Incoming record length is invalid."); break;
    case BR_ERR_TOO_LARGE: Serial.println("Incoming record is too large to be processed, or buffer is too small for the handshake message to send."); break;
    case BR_ERR_BAD_MAC: Serial.println("Decryption found an invalid padding, or the record MAC is not correct."); break;
    case BR_ERR_NO_RANDOM: Serial.println("No initial entropy was provided, and none can be obtained from the OS."); break;
    case BR_ERR_UNKNOWN_TYPE: Serial.println("Incoming record type is unknown."); break;
    case BR_ERR_UNEXPECTED: Serial.println("Incoming record or message has wrong type with regards to the current engine state."); break;
    case BR_ERR_BAD_CCS: Serial.println("ChangeCipherSpec message from the peer has invalid contents."); break;
    case BR_ERR_BAD_ALERT: Serial.println("Alert message from the peer has invalid contents (odd length)."); break;
    case BR_ERR_BAD_HANDSHAKE: Serial.println("Incoming handshake message decoding failed."); break;
    case BR_ERR_OVERSIZED_ID: Serial.println("ServerHello contains a session ID which is larger than 32 bytes."); break;
    case BR_ERR_BAD_CIPHER_SUITE: Serial.println("Server wants to use a cipher suite that we did not claim to support. This is also reported if we tried to advertise a cipher suite that we do not support."); break;
    case BR_ERR_BAD_COMPRESSION: Serial.println("Server wants to use a compression that we did not claim to support."); break;
    case BR_ERR_BAD_FRAGLEN: Serial.println("Server's max fragment length does not match client's."); break;
    case BR_ERR_BAD_SECRENEG: Serial.println("Secure renegotiation failed."); break;
    case BR_ERR_EXTRA_EXTENSION: Serial.println("Server sent an extension type that we did not announce, or used the same extension type several times in a single ServerHello."); break;
    case BR_ERR_BAD_SNI: Serial.println("Invalid Server Name Indication contents (when used by the server, this extension shall be empty)."); break;
    case BR_ERR_BAD_HELLO_DONE: Serial.println("Invalid ServerHelloDone from the server (length is not 0)."); break;
    case BR_ERR_LIMIT_EXCEEDED: Serial.println("Internal limit exceeded (e.g. server's public key is too large)."); break;
    case BR_ERR_BAD_FINISHED: Serial.println("Finished message from peer does not match the expected value."); break;
    case BR_ERR_RESUME_MISMATCH: Serial.println("Session resumption attempt with distinct version or cipher suite."); break;
    case BR_ERR_INVALID_ALGORITHM: Serial.println("Unsupported or invalid algorithm (ECDHE curve, signature algorithm, hash function)."); break;
    case BR_ERR_BAD_SIGNATURE: Serial.println("Invalid signature in ServerKeyExchange or CertificateVerify message."); break;
    case BR_ERR_WRONG_KEY_USAGE: Serial.println("Peer's public key does not have the proper type or is not allowed for the requested operation."); break;
    case BR_ERR_NO_CLIENT_AUTH: Serial.println("Client did not send a certificate upon request, or the client certificate could not be validated."); break;
    case BR_ERR_IO: Serial.println("I/O error or premature close on transport stream."); break;
    case BR_ERR_X509_INVALID_VALUE: Serial.println("Invalid value in an ASN.1 structure."); break;
    case BR_ERR_X509_TRUNCATED: Serial.println("Truncated certificate or other ASN.1 object."); break;
    case BR_ERR_X509_EMPTY_CHAIN: Serial.println("Empty certificate chain (no certificate at all)."); break;
    case BR_ERR_X509_INNER_TRUNC: Serial.println("Decoding error: inner element extends beyond outer element size."); break;
    case BR_ERR_X509_BAD_TAG_CLASS: Serial.println("Decoding error: unsupported tag class (application or private)."); break;
    case BR_ERR_X509_BAD_TAG_VALUE: Serial.println("Decoding error: unsupported tag value."); break;
    case BR_ERR_X509_INDEFINITE_LENGTH: Serial.println("Decoding error: indefinite length."); break;
    case BR_ERR_X509_EXTRA_ELEMENT: Serial.println("Decoding error: extraneous element."); break;
    case BR_ERR_X509_UNEXPECTED: Serial.println("Decoding error: unexpected element."); break;
    case BR_ERR_X509_NOT_CONSTRUCTED: Serial.println("Decoding error: expected constructed element, but is primitive."); break;
    case BR_ERR_X509_NOT_PRIMITIVE: Serial.println("Decoding error: expected primitive element, but is constructed."); break;
    case BR_ERR_X509_PARTIAL_BYTE: Serial.println("Decoding error: BIT STRING length is not multiple of 8."); break;
    case BR_ERR_X509_BAD_BOOLEAN: Serial.println("Decoding error: BOOLEAN value has invalid length."); break;
    case BR_ERR_X509_OVERFLOW: Serial.println("Decoding error: value is off-limits."); break;
    case BR_ERR_X509_BAD_DN: Serial.println("Invalid distinguished name."); break;
    case BR_ERR_X509_BAD_TIME: Serial.println("Invalid date/time representation."); break;
    case BR_ERR_X509_UNSUPPORTED: Serial.println("Certificate contains unsupported features that cannot be ignored."); break;
    case BR_ERR_X509_LIMIT_EXCEEDED: Serial.println("Key or signature size exceeds internal limits."); break;
    case BR_ERR_X509_WRONG_KEY_TYPE: Serial.println("Key type does not match that which was expected."); break;
    case BR_ERR_X509_BAD_SIGNATURE: Serial.println("Signature is invalid."); break;
    case BR_ERR_X509_TIME_UNKNOWN: Serial.println("Validation time is unknown."); break;
    case BR_ERR_X509_EXPIRED: Serial.println("Certificate is expired or not yet valid."); break;
    case BR_ERR_X509_DN_MISMATCH: Serial.println("Issuer/Subject DN mismatch in the chain."); break;
    case BR_ERR_X509_BAD_SERVER_NAME: Serial.println("Expected server name was not found in the chain."); break;
    case BR_ERR_X509_CRITICAL_EXTENSION: Serial.println("Unknown critical extension in certificate."); break;
    case BR_ERR_X509_NOT_CA: Serial.println("Not a CA, or path length constraint violation."); break;
    case BR_ERR_X509_FORBIDDEN_KEY_USAGE: Serial.println("Key Usage extension prohibits intended usage."); break;
    case BR_ERR_X509_WEAK_PUBLIC_KEY: Serial.println("Public key found in certificate is too small."); break;
    case BR_ERR_X509_NOT_TRUSTED: Serial.println("Chain could not be linked to a trust anchor."); break;
    default: Serial.println("Unknown error code."); break;
  }
}
