From e3dfe3235091d5c8e23e22418d66ada60600f232 Mon Sep 17 00:00:00 2001 From: John Date: Sun, 6 Feb 2022 19:32:21 +0100 Subject: [PATCH] start SSL --- CMakeLists.txt | 21 +++- config.h.cmake | 3 + src/ebusd/CMakeLists.txt | 4 + src/ebusd/main.cpp | 10 +- src/ebusd/main.h | 2 +- src/ebusd/mainloop.cpp | 8 +- src/lib/utils/httpclient.cpp | 207 ++++++++++++++++++++++++++++++++++- src/lib/utils/httpclient.h | 90 +++++++++++++-- src/lib/utils/tcpsocket.cpp | 2 +- src/lib/utils/tcpsocket.h | 25 ++--- src/tools/ebusctl.cpp | 3 +- 11 files changed, 339 insertions(+), 36 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cc79c6f3..691cdd12 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,6 +62,7 @@ check_function_exists(pselect HAVE_PSELECT) check_function_exists(ppoll HAVE_PPOLL) check_include_file(linux/serial.h HAVE_LINUX_SERIAL -DHAVE_LINUX_SERIAL=1) check_include_file(dev/usb/uftdiio.h HAVE_FREEBSD_UFTDI -DHAVE_FREEBSD_UFTDI=1) + check_function_exists(argp_parse HAVE_ARGP) if(NOT HAVE_ARGP) find_library(LIB_ARGP argp) @@ -77,11 +78,13 @@ if(NOT coverage STREQUAL OFF) link_libraries(gcov) message(STATUS "coverage enabled") endif(NOT coverage STREQUAL OFF) + option(contrib "disable inclusion of contributed sources." ON) if(contrib STREQUAL ON) set(HAVE_CONTRIB ON) message(STATUS "contrib enabled") endif(contrib STREQUAL ON) + find_library(HAVE_MQTT mosquitto) if(HAVE_MQTT) option(mqtt "disable support for MQTT handling." ON) @@ -90,7 +93,23 @@ if(HAVE_MQTT) else(mqtt STREQUAL ON) unset(HAVE_MQTT) endif(mqtt STREQUAL ON) -endif(HAVE_MQTT) +endif(HAVE_MQTT) + +find_library(HAVE_SSL ssl) +find_library(LIB_CRYPTO crypto) +if(HAVE_SSL) + if(LIB_CRYPTO) + option(ssl "disable support for SSL." ON) + if(ssl STREQUAL ON) + message(STATUS "SSL enabled") + set(CMAKE_REQUIRED_LIBRARIES "${CMAKE_REQUIRED_LIBRARIES} ${HAVE_SSL} ${LIB_CRYPTO}") + else(ssl STREQUAL ON) + unset(HAVE_SSL) + endif(ssl STREQUAL ON) + else(LIB_CRYPTO) + message(FATAL_ERROR "crypto library not available") + endif(LIB_CRYPTO) +endif(HAVE_SSL) check_cxx_source_runs(" #include diff --git a/config.h.cmake b/config.h.cmake index b194d390..8758cb1d 100755 --- a/config.h.cmake +++ b/config.h.cmake @@ -7,6 +7,9 @@ /* Defined if MQTT handling is enabled. */ #cmakedefine HAVE_MQTT +/* Defined if SSL is enabled. */ +#cmakedefine HAVE_SSL + /* Defined if ppoll() is available. */ #cmakedefine HAVE_PPOLL diff --git a/src/ebusd/CMakeLists.txt b/src/ebusd/CMakeLists.txt index 96c3f0e4..9195e7d6 100644 --- a/src/ebusd/CMakeLists.txt +++ b/src/ebusd/CMakeLists.txt @@ -18,6 +18,10 @@ if(HAVE_MQTT) set(ebusd_LIBS ${ebusd_LIBS} mosquitto) endif(HAVE_MQTT) +if(HAVE_SSL) + set(ebusd_LIBS ${ebusd_LIBS} ssl crypto) +endif(HAVE_SSL) + if(HAVE_CONTRIB) set(ebusd_LIBS ${ebusd_LIBS} ebuscontrib) endif(HAVE_CONTRIB) diff --git a/src/ebusd/main.cpp b/src/ebusd/main.cpp index 8ce37b78..6b6c9229 100644 --- a/src/ebusd/main.cpp +++ b/src/ebusd/main.cpp @@ -65,7 +65,11 @@ using std::cout; #endif /** the default path of the configuration files. */ +#ifdef HAVE_SSL +#define CONFIG_PATH "https://cfg.ebusd.eu/" +#else #define CONFIG_PATH "http://cfg.ebusd.eu/" +#endif /** the opened PID file, or nullptr. */ static FILE* pidFile = nullptr; @@ -189,7 +193,7 @@ static const struct argp_option argpoptions[] = { {"latency", O_DEVLAT, "MSEC", 0, "Extra transfer latency in ms [0]", 0 }, {nullptr, 0, nullptr, 0, "Message configuration options:", 2 }, - {"configpath", 'c', "PATH", 0, "Read CSV config files from PATH (local folder or HTTP URL) [" CONFIG_PATH + {"configpath", 'c', "PATH", 0, "Read CSV config files from PATH (local folder or HTTPS URL) [" CONFIG_PATH "]", 0 }, {"scanconfig", 's', "ADDR", OPTION_ARG_OPTIONAL, "Pick CSV config files matching initial scan (ADDR=" "\"none\" or empty for no initial scan message, \"full\" for full scan, or a single hex address to scan, " @@ -308,7 +312,7 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) { break; // Message configuration options: - case 'c': // --configpath=http://cfg.ebusd.eu/ + case 'c': // --configpath=https://cfg.ebusd.eu/ if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) { argp_error(state, "invalid configpath"); return EINVAL; @@ -1307,7 +1311,7 @@ int main(int argc, char* argv[]) { logError(lf_main, "invalid configPath URL"); return EINVAL; } - if (!s_configHttpClient.connect(configHost, configPort, PACKAGE_NAME "/" PACKAGE_VERSION)) { + if (!s_configHttpClient.connect(configHost, configPort, proto=="https", PACKAGE_NAME "/" PACKAGE_VERSION)) { logError(lf_main, "invalid configPath URL"); return EINVAL; } diff --git a/src/ebusd/main.h b/src/ebusd/main.h index ca99bf29..66945d39 100644 --- a/src/ebusd/main.h +++ b/src/ebusd/main.h @@ -41,7 +41,7 @@ struct options { bool initialSend; //!< send an initial escape symbol after connecting device unsigned int extraLatency; //!< extra transfer latency in ms [0 for USB, 10 for IP] - const char* configPath; //!< path to CSV configuration files [http://cfg.ebusd.eu/] + const char* configPath; //!< path to CSV configuration files [https://cfg.ebusd.eu/] bool scanConfig; //!< pick configuration files matching initial scan /** the initial address to scan for scanconfig * (@a ESC=none, 0xfe=broadcast ident, @a SYN=full scan, else: single slave address). */ diff --git a/src/ebusd/mainloop.cpp b/src/ebusd/mainloop.cpp index 16ea4fd2..ac4eebfd 100644 --- a/src/ebusd/mainloop.cpp +++ b/src/ebusd/mainloop.cpp @@ -332,7 +332,13 @@ void MainLoop::run() { } if (m_runUpdateCheck && !m_shutdown && now > nextCheckRun) { HttpClient client; - if (!client.connect("upd.ebusd.eu", 80, PACKAGE_NAME "/" PACKAGE_VERSION)) { + if (!client.connect("upd.ebusd.eu", +#ifdef HAVE_SSL + 443, true, +#else + 80, false, +#endif + PACKAGE_NAME "/" PACKAGE_VERSION)) { logError(lf_main, "update check connect error"); } else { ostringstream ostr; diff --git a/src/lib/utils/httpclient.cpp b/src/lib/utils/httpclient.cpp index a108ccf4..afd65102 100755 --- a/src/lib/utils/httpclient.cpp +++ b/src/lib/utils/httpclient.cpp @@ -17,9 +17,11 @@ */ #include "lib/utils/httpclient.h" +#include "lib/utils/log.h" #include #include #include +#include namespace ebusd { @@ -28,6 +30,189 @@ using std::ostringstream; using std::dec; using std::hex; +#ifdef HAVE_SSL + +bool checkError(const char* call) { + unsigned long err = ERR_get_error(); + if (err) { + const char *const str = ERR_reason_error_string(err); + logError(lf_network, "SSL error %s: %ld=%s", call, err, str); + return true; + } + return false; +} + +bool isError(const char* call, bool result) { + if (checkError(call)) { + return true; + } + if (!result) { + logError(lf_network, "SSL error %s: invalid result", call); + return true; + } + return false; +} + +bool isError(const char* call, long result, long expected) { + if (checkError(call)) { + return true; + } + if (result!=expected) { + logError(lf_network, "SSL error %s: invalid result %d", call, result); + return true; + } + return false; +} + + +SSLSocket::~SSLSocket() { + BIO_free_all(m_bio); + if (m_ctx) { + SSL_CTX_free(m_ctx); + } +} + +ssize_t SSLSocket::send(const char* data, size_t len) { + do { + size_t part = 0; + int res = BIO_write_ex(m_bio, data, len, &part); + if (res==1) { + return static_cast(part); + } + if (!BIO_should_retry(m_bio)) { + if (isError("write", true)) { + return -1; + } + return 0; + } + usleep(50000); + } while (true); +} + +ssize_t SSLSocket::recv(char* data, size_t len) { + do { + size_t part = 0; + int res = BIO_read_ex(m_bio, data, len, &part); + if (res==1) { + return static_cast(part); + } + if (!BIO_should_retry(m_bio)) { + if (isError("read", true)) { + return -1; + } + return 0; + } + usleep(50000); + } while (true); +} + +bool SSLSocket::isValid() { + return !BIO_eof(m_bio); +} + +SSLSocket* SSLSocket::connect(const string& host, const uint16_t& port, const bool https, int timeout) { + BIO *bio = nullptr; + SSL_CTX *ctx = nullptr; + ostringstream ostr; + ostr << host << ':' << static_cast(port); + const string hostPort = ostr.str(); + if (!https) { + do { + bio = BIO_new_connect(hostPort.c_str()); + if (isError("connect", bio)) { + break; + } + BIO_set_nbio(bio, 1); // set non-blocking + return new SSLSocket(nullptr, bio); + } while (false); + } else { + SSL *ssl = nullptr; + do { + const SSL_METHOD *method = SSLv23_method(); + if (isError("method", method)) { + break; + } + ctx = SSL_CTX_new(method); + if (isError("ctx_new", ctx)) { + break; + } + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr); + if (isError("verify_loc", SSL_CTX_load_verify_locations(ctx, nullptr, "/etc/ssl/certs"), 1)) { + break; + } + SSL_CTX_set_verify_depth(ctx, 2); + const long flags = SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION; + SSL_CTX_set_options(ctx, flags); + bio = BIO_new_ssl_connect(ctx); + if (isError("new_ssl_connect", bio)) { + break; + } + if (isError("conn_hostname", BIO_set_conn_hostname(bio, hostPort.c_str()), 1)) { + break; + } + BIO_set_nbio(bio, 1); // set non-blocking + BIO_get_ssl(bio, &ssl); + if (isError("get_ssl", ssl)) { + break; + } + const char *hostname = host.c_str(); + if (isError("tlsext_host_name", SSL_set_tlsext_host_name(ssl, hostname), 1)) { + break; + } + time_t until = time(nullptr) + (timeout<=0 ? 1 : timeout); + long res = BIO_do_connect(bio); + while (res != 1 && BIO_should_retry(bio) && time(nullptr) 0)) { + break; + } + if (isError("subject", strcmp(peerName, hostname), 0)) { + break; + } + return new SSLSocket(ctx, bio); + } while (false); + } + if (bio) { + BIO_free_all(bio); + } + if (ctx) { + SSL_CTX_free(ctx); + } + return nullptr; +} + +void HttpClient::initialize() { + SSL_library_init(); + SSL_load_error_strings(); + signal(SIGPIPE, SIG_IGN); // needed to avoid SIGPIPE when writing to a closed pipe +} +#else // HAVE_SSL +void HttpClient::initialize() { + // empty +} +#endif // HAVE_SSL + bool HttpClient::parseUrl(const string& url, string* proto, string* host, uint16_t* port, string* uri) { size_t hostPos = url.find("://"); if (hostPos == string::npos) { @@ -35,9 +220,16 @@ bool HttpClient::parseUrl(const string& url, string* proto, string* host, uint16 } *proto = url.substr(0, hostPos); hostPos += 3; + bool isSsl = *proto == "https"; +#ifdef HAVE_SSL + if (!isSsl && *proto != "http") { + return false; + } +#else if (*proto != "http") { return false; } +#endif size_t pos = url.find('/', hostPos); if (pos == hostPos) { return false; @@ -56,7 +248,7 @@ bool HttpClient::parseUrl(const string& url, string* proto, string* host, uint16 if (pos == 0) { return false; } - *port = 80; + *port = isSsl ? 443 : 80; if (pos != string::npos) { char* strEnd = nullptr; unsigned long value = strtoul(host->c_str()+pos+1, &strEnd, 10); @@ -69,9 +261,16 @@ bool HttpClient::parseUrl(const string& url, string* proto, string* host, uint16 return true; } -bool HttpClient::connect(const string& host, const uint16_t port, const string& userAgent, const int timeout) { +bool HttpClient::connect(const string& host, const uint16_t port, const bool https, const string& userAgent, const int timeout) { disconnect(); - m_socket = m_client.connect(host, port, timeout); +#ifdef HAVE_SSL + m_socket = SSLSocket::connect(host, port, https, timeout); +#else + if (https) { + return false; + } + m_socket = TCPSocket::connect(host, port, timeout); +#endif if (!m_socket) { return false; } @@ -87,7 +286,7 @@ bool HttpClient::reconnect() { if (m_host.empty() || !m_port) { return false; } - m_socket = m_client.connect(m_host, m_port, m_timeout); + m_socket = SocketClass::connect(m_host, m_port, m_timeout); if (!m_socket) { return false; } diff --git a/src/lib/utils/httpclient.h b/src/lib/utils/httpclient.h index eaafbb69..27bdae02 100755 --- a/src/lib/utils/httpclient.h +++ b/src/lib/utils/httpclient.h @@ -19,11 +19,20 @@ #ifndef LIB_UTILS_HTTPCLIENT_H_ #define LIB_UTILS_HTTPCLIENT_H_ +#ifdef HAVE_CONFIG_H +# include +#endif + #include #include #include #include "lib/utils/tcpsocket.h" +#ifdef HAVE_SSL +# include +# include +# include +#endif /** typedef for referencing @a sockaddr_in within namespace. */ typedef struct sockaddr_in socketaddress; @@ -35,6 +44,70 @@ namespace ebusd { using std::string; using std::ifstream; +#ifdef HAVE_SSL +class SSLSocket { + private: + /** + * Constructor. + * @param ctx the SSL_CTX for cleanup, or nullptr. + * @param bio the BIO instance, or nullptr. + */ + SSLSocket(SSL_CTX *ctx, BIO *bio) : m_ctx(ctx), m_bio(bio) {} + + public: + /** + * Destructor. + */ + virtual ~SSLSocket(); + + /** + * Connect to the host on the specified port. + * @param host the host name or ip address to connect to. + * @param port the port number. + * @param https true for HTTPS, false for HTTP. + * @param timeout the connect, send, and receive timeout in seconds, or 0 for blocking mode. + * @return the connected SSLSocket, or nullptr on error. + */ + static SSLSocket* connect(const string& server, const uint16_t& port, const bool https, int timeout = 0); + + /** + * Write bytes to the socket. + * @param data the data to send. + * @param len number of bytes to send. + * @return number of bytes written, or -1 on error. + */ + ssize_t send(const char* data, size_t len); + + /** + * Read bytes from the socket. + * @param data the buffer for the received bytes. + * @param len size of the buffer. + * @return number of bytes read, or -1 on error. + */ + ssize_t recv(char* data, size_t len); + + /** + * Return whether the socket is still valid. + * @return true if the socket is still valid. + */ + bool isValid(); + + private: + /** the SSL_CTX for cleanup, or nullptr. */ + SSL_CTX *m_ctx; + + /** the BIO instance for communication. */ + BIO *m_bio; +}; + +#define SocketClass SSLSocket + +#else // HAVE_SSL + +#define SocketClass TCPSocket + +#endif // HAVE_SSL + /** * Helper class for handling HTTP client requests. */ @@ -43,7 +116,7 @@ class HttpClient { /** * Constructor. */ - HttpClient() : m_port(0), m_timeout(0), m_socket(nullptr), m_bufferSize(0), m_buffer(nullptr) {} + HttpClient() : m_socket(nullptr), m_port(0), m_timeout(0), m_bufferSize(0), m_buffer(nullptr) {} /** * Destructor. @@ -56,6 +129,11 @@ class HttpClient { } } + /** + * Initialize HttpClient. + */ + static void initialize(); + /** * Parse an HTTP URL. * @param url the URL to parse. @@ -71,11 +149,12 @@ class HttpClient { * Connect to the specified server. * @param host the host name to connect to. * @param port the port to connect to. + * @param https true for HTTPS, false for HTTP. * @param timeout the timeout in seconds, defaults to 5 seconds. * @param userAgent the optional user agent to send in the request header. * @return true on success, false on connect failure. */ - bool connect(const string& host, uint16_t port, const string& userAgent = "", int timeout = 5); + bool connect(const string& host, uint16_t port, const bool https = false, const string& userAgent = "", int timeout = 5); /** * Re-connect to the last specified server. @@ -135,8 +214,8 @@ class HttpClient { size_t readUntil(const string& delim, const size_t length, string* result); private: - /** the @a TCPClient handling the traffic. */ - TCPClient m_client; + /** the currently connected socket. */ + SocketClass* m_socket; /** the name of the host last successfully connected to. */ string m_host; @@ -150,9 +229,6 @@ class HttpClient { /** the optional user agent to send in the request header. */ string m_userAgent; - /** the currently connected socket. */ - TCPSocket* m_socket; - /** the size of the @a m_buffer. */ size_t m_bufferSize; diff --git a/src/lib/utils/tcpsocket.cpp b/src/lib/utils/tcpsocket.cpp index d35acb3d..b24ab1cd 100755 --- a/src/lib/utils/tcpsocket.cpp +++ b/src/lib/utils/tcpsocket.cpp @@ -45,7 +45,7 @@ bool TCPSocket::isValid() { } -TCPSocket* TCPClient::connect(const string& server, const uint16_t& port, int timeout) { +TCPSocket* TCPSocket::connect(const string& server, const uint16_t& port, int timeout) { socketaddress address; int ret; diff --git a/src/lib/utils/tcpsocket.h b/src/lib/utils/tcpsocket.h index 8cb321e3..06e7eab4 100755 --- a/src/lib/utils/tcpsocket.h +++ b/src/lib/utils/tcpsocket.h @@ -45,7 +45,6 @@ using std::string; * Class for low level TCP socket operations (open, close, send, receive). */ class TCPSocket { - friend class TCPClient; friend class TCPServer; private: @@ -62,6 +61,15 @@ class TCPSocket { */ ~TCPSocket() { close(m_sfd); } + /** + * initiate a tcp socket connection to a listening server. + * @param server the server name or ip address to connect. + * @param port the tcp port. + * @param timeout the connect, send, and receive timeout in seconds, or 0. + * @return pointer to an opened tcp socket. + */ + static TCPSocket* connect(const string& server, const uint16_t& port, int timeout = 0); + /** * Write bytes to opened file descriptor. * @param buffer data to send. @@ -125,21 +133,6 @@ class TCPSocket { string m_ip; }; -/** - * class to initiate a TCP socket connection to a listening server. - */ -class TCPClient { - public: - /** - * initiate a tcp socket connection to a listening server. - * @param server the server name or ip address to connect. - * @param port the tcp port. - * @param timeout the connect, send, and receive timeout in seconds, or 0. - * @return pointer to an opened tcp socket. - */ - TCPSocket* connect(const string& server, const uint16_t& port, int timeout = 0); -}; - /** * class for a TCP based network server. */ diff --git a/src/tools/ebusctl.cpp b/src/tools/ebusctl.cpp index 53e73212..95db494c 100755 --- a/src/tools/ebusctl.cpp +++ b/src/tools/ebusctl.cpp @@ -261,8 +261,7 @@ string fetchData(ebusd::TCPSocket* socket, bool &listening, uint16_t timeout, bo } bool connect(const char* host, uint16_t port, uint16_t timeout, char* const *args, int argCount) { - TCPClient* client = new TCPClient(); - TCPSocket* socket = client->connect(host, port, timeout); + TCPSocket* socket = TCPSocket::connect(host, port, timeout); bool ret; bool once = args != nullptr && argCount > 0;