diff --git a/config/v16/config-docker.json b/config/v16/config-docker.json index 2c66ecf5d1..3260133d9e 100644 --- a/config/v16/config-docker.json +++ b/config/v16/config-docker.json @@ -6,7 +6,8 @@ "ChargePointModel": "Yeti", "ChargePointVendor": "Pionix", "FirmwareVersion": "0.1", - "AllowChargingProfileWithoutStartSchedule": true + "AllowChargingProfileWithoutStartSchedule": true, + "UseTPM" : false }, "Core": { "AuthorizeRemoteTxRequests": false, diff --git a/config/v16/profile_schemas/Internal.json b/config/v16/profile_schemas/Internal.json index 68e0879fcc..d431ecaf10 100644 --- a/config/v16/profile_schemas/Internal.json +++ b/config/v16/profile_schemas/Internal.json @@ -104,6 +104,11 @@ "TLS_AES_128_GCM_SHA256" ] }, + "UseTPM": { + "type": "boolean", + "readOnly": true, + "default": false + }, "RetryBackoffRandomRange": { "$comment": "maximum value for the random part of the websocket reconnect back-off time", "type": "integer", diff --git a/dependencies.yaml b/dependencies.yaml index cdb55f0cb4..82fed0d162 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -31,3 +31,7 @@ websocketpp: libevse-security: git: https://github.com/EVerest/libevse-security.git git_tag: v0.3.0 +libwebsockets: + git: https://github.com/warmcat/libwebsockets.git + git_tag: v4.3.3 + \ No newline at end of file diff --git a/include/ocpp/common/websocket/websocket_base.hpp b/include/ocpp/common/websocket/websocket_base.hpp index df3c61ebf4..7a93225f14 100644 --- a/include/ocpp/common/websocket/websocket_base.hpp +++ b/include/ocpp/common/websocket/websocket_base.hpp @@ -35,6 +35,7 @@ struct WebsocketConnectionOptions { std::optional additional_root_certificate_check; std::optional hostName; bool verify_csms_common_name; + bool use_tpm_tls; }; /// diff --git a/include/ocpp/common/websocket/websocket_tls_tpm.hpp b/include/ocpp/common/websocket/websocket_tls_tpm.hpp new file mode 100644 index 0000000000..49b7720975 --- /dev/null +++ b/include/ocpp/common/websocket/websocket_tls_tpm.hpp @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +#ifndef OCPP_WEBSOCKET_TLS_TPM_HPP +#define OCPP_WEBSOCKET_TLS_TPM_HPP + +#include +#include + +#include +namespace ocpp { + +struct ConnectionData; +struct WebsocketMessage; + +/// \brief Experimental libwebsockets TLS connection +class WebsocketTlsTPM final : public WebsocketBase { +public: + /// \brief Creates a new Websocket object with the providede \p connection_options + explicit WebsocketTlsTPM(const WebsocketConnectionOptions& connection_options, + std::shared_ptr evse_security); + + ~WebsocketTlsTPM(); + + void set_connection_options(const WebsocketConnectionOptions& connection_options) override; + + /// \brief connect to a TLS websocket + /// \returns true if the websocket is initialized and a connection attempt is made + bool connect() override; + + /// \brief Reconnects the websocket using the delay, a reason for this reconnect can be provided with the + /// \param reason parameter + /// \param delay delay of the reconnect attempt + void reconnect(std::error_code reason, long delay) override; + + /// \brief closes the websocket + void close(websocketpp::close::status::value code, const std::string& reason) override; + + /// \brief send a \p message over the websocket + /// \returns true if the message was sent successfully + bool send(const std::string& message) override; + + /// \brief send a websocket ping + void ping() override; + +public: + int process_callback(void* wsi_ptr, int callback_reason, void* user, void* in, size_t len); + +private: + void tls_init(); + void client_loop(); + void recv_loop(); + + /// \brief Called when a TLS websocket connection is established, calls the connected callback + void on_conn_connected(); + + /// \brief Called when a TLS websocket connection is closed + void on_conn_close(); + + /// \brief Called when a TLS websocket connection fails to be established + void on_conn_fail(); + + /// \brief When the connection can send data + void on_writable(); + + /// \brief Called when a message is received over the TLS websocket, calls the message callback + void on_message(void* msg, size_t len); + + void request_write(); + + void poll_message(const std::shared_ptr& msg, bool wait_sendaf); + +private: + std::shared_ptr evse_security; + + // Connection related data + std::unique_ptr reconnect_timer_tpm; + std::unique_ptr websocket_thread; + std::shared_ptr conn_data; + std::condition_variable conn_cv; + + std::mutex queue_mutex; + std::queue> message_queue; + std::condition_variable msg_send_cv; + + std::unique_ptr recv_message_thread; + std::mutex recv_mutex; + std::queue recv_message_queue; + std::condition_variable recv_message_cv; +}; + +} // namespace ocpp +#endif // OCPP_WEBSOCKET_HPP diff --git a/include/ocpp/common/websocket/websocket_uri.hpp b/include/ocpp/common/websocket/websocket_uri.hpp index c91f51339d..63b72348d9 100644 --- a/include/ocpp/common/websocket/websocket_uri.hpp +++ b/include/ocpp/common/websocket/websocket_uri.hpp @@ -40,6 +40,14 @@ class Uri { return this->chargepoint_id; } + std::string get_path() { + return this->path_without_chargepoint_id; + } + + uint16_t get_port() { + return this->port; + } + std::string string() { auto uri = get_websocketpp_uri(); return uri.str(); diff --git a/include/ocpp/v16/charge_point_configuration.hpp b/include/ocpp/v16/charge_point_configuration.hpp index ed3464bb10..d0bad80583 100644 --- a/include/ocpp/v16/charge_point_configuration.hpp +++ b/include/ocpp/v16/charge_point_configuration.hpp @@ -81,6 +81,7 @@ class ChargePointConfiguration { KeyValue getUseSslDefaultVerifyPathsKeyValue(); bool getVerifyCsmsCommonName(); KeyValue getVerifyCsmsCommonNameKeyValue(); + bool getUseTPM(); int32_t getRetryBackoffRandomRange(); void setRetryBackoffRandomRange(int32_t retry_backoff_random_range); diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index e5c4a5176f..1bb4fbbe51 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -52,6 +52,7 @@ add_subdirectory(ocpp/v16/messages) add_subdirectory(ocpp/v201/messages) option(LIBOCPP_USE_BOOST_FILESYSTEM "Usage of boost/filesystem.hpp instead of std::filesystem" OFF) +option(LIBOCPP_ENABLE_LIBWEBSOCKETS "Usage of libwebsockets instead of websockets++" OFF) target_include_directories(ocpp PUBLIC @@ -103,6 +104,18 @@ target_link_libraries(ocpp date::date-tz ) +if(LIBOCPP_ENABLE_LIBWEBSOCKETS) + find_package(libwebsockets REQUIRED) + target_link_libraries(ocpp + PUBLIC + websockets + ) + target_compile_definitions(log + PRIVATE + LIBOCPP_ENABLE_LIBWEBSOCKETS + ) +endif() + if(LIBOCPP_USE_BOOST_FILESYSTEM) find_package(Boost REQUIRED COMPONENTS filesystem) target_link_libraries(ocpp diff --git a/lib/ocpp/common/websocket/CMakeLists.txt b/lib/ocpp/common/websocket/CMakeLists.txt index 896e376941..27133f9c82 100644 --- a/lib/ocpp/common/websocket/CMakeLists.txt +++ b/lib/ocpp/common/websocket/CMakeLists.txt @@ -7,3 +7,10 @@ target_sources(ocpp websocket_tls.cpp websocket.cpp ) + +if(LIBOCPP_ENABLE_LIBWEBSOCKETS) + target_sources(ocpp + PRIVATE + websocket_tls_tpm.cpp + ) +endif() \ No newline at end of file diff --git a/lib/ocpp/common/websocket/websocket.cpp b/lib/ocpp/common/websocket/websocket.cpp index 2f40896ced..c024eb986d 100644 --- a/lib/ocpp/common/websocket/websocket.cpp +++ b/lib/ocpp/common/websocket/websocket.cpp @@ -5,6 +5,10 @@ #include #include +#ifdef LIBOCPP_ENABLE_LIBWEBSOCKETS +#include +#endif + #include using json = nlohmann::json; @@ -14,11 +18,16 @@ namespace ocpp { Websocket::Websocket(const WebsocketConnectionOptions& connection_options, std::shared_ptr evse_security, std::shared_ptr logging) : logging(logging) { + +#ifdef LIBOCPP_ENABLE_LIBWEBSOCKETS + this->websocket = std::make_unique(connection_options, evse_security); +#else if (connection_options.security_profile <= 1) { this->websocket = std::make_unique(connection_options); } else if (connection_options.security_profile >= 2) { this->websocket = std::make_unique(connection_options, evse_security); } +#endif } Websocket::~Websocket() { diff --git a/lib/ocpp/common/websocket/websocket_tls_tpm.cpp b/lib/ocpp/common/websocket/websocket_tls_tpm.cpp new file mode 100644 index 0000000000..345226e093 --- /dev/null +++ b/lib/ocpp/common/websocket/websocket_tls_tpm.cpp @@ -0,0 +1,924 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +#include + +#include + +#include + +#include +#include + +#include +#include +#include +#include + +template <> class std::default_delete { +public: + void operator()(lws_context* ptr) const { + ::lws_context_destroy(ptr); + } +}; + +template <> class std::default_delete { +public: + void operator()(OSSL_LIB_CTX* ptr) const { + ::OPENSSL_thread_stop_ex(ptr); + ::OSSL_LIB_CTX_free(ptr); + } +}; + +template <> class std::default_delete { +public: + void operator()(SSL_CTX* ptr) const { + ::SSL_CTX_free(ptr); + } +}; + +namespace ocpp { + +enum class EConnectionState { + INITIALIZE, ///< Initialization state + CONNECTING, ///< Trying to connect + CONNECTED, ///< Successfully connected + ERROR, ///< We couldn't connect + FINALIZED, ///< We finalized the connection and we're never going to connect again +}; + +/// \brief Per thread connection data +struct ConnectionData { + ConnectionData() : is_running(true), state(EConnectionState::INITIALIZE), wsi(nullptr) { + } + + ~ConnectionData() { + state = EConnectionState::FINALIZED; + is_running = false; + wsi = nullptr; + } + + void bind_thread(std::thread::id id) { + lws_thread_id = id; + } + + std::thread::id get_lws_thread_id() { + return lws_thread_id; + } + + void update_state(EConnectionState in_state) { + state = in_state; + } + + void do_interrupt() { + is_running = false; + } + + bool is_interupted() { + return (is_running == false); + } + + bool is_connecting() { + return (state == EConnectionState::CONNECTING); + } + + auto get_state() { + return state; + } + + lws* get_conn() { + return wsi; + } + + lws_context* get_ctx() { + return lws_ctx.get(); + } + +public: + // openssl context + std::unique_ptr sec_context; + std::unique_ptr sec_lib_context; + + // libwebosockets state + std::unique_ptr lws_ctx; + lws* wsi; + +private: + std::thread::id lws_thread_id; + bool is_running; + EConnectionState state; +}; + +struct WebsocketMessage { + WebsocketMessage() : sent_bytes(0), message_sent(false) { + } + + virtual ~WebsocketMessage() { + } + +public: + std::string payload; + lws_write_protocol protocol; + + // How many bytes we have sent to libwebsockets, does not + // necessarily mean that all bytes have been sent over the wire, + // just that these were sent to liwebsockets + size_t sent_bytes; + // If libwebsockets has sent all the bytes through the wire + volatile bool message_sent; +}; + +WebsocketTlsTPM::WebsocketTlsTPM(const WebsocketConnectionOptions& connection_options, + std::shared_ptr evse_security) : + WebsocketBase(), evse_security(evse_security) { + + set_connection_options(connection_options); + + EVLOG_debug << "Initialised WebsocketTlsTPM with URI: " << this->connection_options.csms_uri.string(); +} + +WebsocketTlsTPM::~WebsocketTlsTPM() { + if (conn_data != nullptr) { + conn_data->do_interrupt(); + } + + if (websocket_thread != nullptr) { + websocket_thread->join(); + } + + if (recv_message_thread != nullptr) { + recv_message_thread->join(); + } +} + +void WebsocketTlsTPM::set_connection_options(const WebsocketConnectionOptions& connection_options) { + switch (connection_options.security_profile) { // `switch` used to lint on missing enum-values + case security::SecurityProfile::OCPP_1_6_ONLY_UNSECURED_TRANSPORT_WITHOUT_BASIC_AUTHENTICATION: + case security::SecurityProfile::UNSECURED_TRANSPORT_WITH_BASIC_AUTHENTICATION: + throw std::invalid_argument("`security_profile` is not a TLS-profile"); + [[fallthrough]]; + case security::SecurityProfile::TLS_WITH_BASIC_AUTHENTICATION: + case security::SecurityProfile::TLS_WITH_CLIENT_SIDE_CERTIFICATES: + break; + default: + throw std::invalid_argument("unknown `security_profile`, value = " + + std::to_string(connection_options.security_profile)); + } + + set_connection_options_base(connection_options); + + this->connection_options.csms_uri.set_secure(true); +} + +static int callback_minimal(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len) { + // Get user safely, since on some callbacks (void *user) can be different + if (wsi != nullptr) { + if (WebsocketTlsTPM* websocket = reinterpret_cast(lws_wsi_user(wsi))) { + return websocket->process_callback(wsi, static_cast(reason), user, in, len); + } + } + + return 0; +} + +constexpr auto local_protocol_name = "lws-everest-client"; +static const struct lws_protocols protocols[] = {{local_protocol_name, callback_minimal, 0, 0, 0, NULL, 0}, + LWS_PROTOCOL_LIST_TERM}; + +static void create_sec_context(bool use_tpm, OSSL_LIB_CTX*& out_libctx, SSL_CTX*& out_ctx) { + OSSL_LIB_CTX* libctx = OSSL_LIB_CTX_new(); + + if (libctx == nullptr) { + EVLOG_AND_THROW(std::runtime_error("Unable to create ssl lib ctx.")); + } + + out_libctx = libctx; + + if (use_tpm) { + OSSL_PROVIDER* prov_tpm2 = nullptr; + OSSL_PROVIDER* prov_default = nullptr; + + if ((prov_tpm2 = OSSL_PROVIDER_load(libctx, "tpm2")) == nullptr) { + EVLOG_AND_THROW(std::runtime_error("Can not load provider tpm2.")); + } + + if (!OSSL_PROVIDER_self_test(prov_tpm2)) { + EVLOG_AND_THROW(std::runtime_error("Can not self-test provider tpm2.")); + } + + if ((prov_default = OSSL_PROVIDER_load(libctx, "default")) == nullptr) { + EVLOG_AND_THROW(std::runtime_error("Can not load provider default.")); + } + + if (!OSSL_PROVIDER_self_test(prov_default)) { + EVLOG_AND_THROW(std::runtime_error("Can not self-test provider default.")); + } + } + + const SSL_METHOD* method = SSLv23_client_method(); + SSL_CTX* ctx = SSL_CTX_new_ex(libctx, nullptr, method); + + if (ctx == nullptr) { + EVLOG_AND_THROW(std::runtime_error("Unable to create ssl ctx.")); + } + + out_ctx = ctx; +} + +void WebsocketTlsTPM::tls_init() { + SSL_CTX* ctx = nullptr; + + if (auto* data = conn_data.get()) { + ctx = data->sec_context.get(); + } + + if (nullptr == ctx) { + EVLOG_AND_THROW(std::runtime_error("Invalid SSL context!")); + } + + auto rc = SSL_CTX_set_cipher_list(ctx, this->connection_options.supported_ciphers_12.c_str()); + if (rc != 1) { + EVLOG_debug << "SSL_CTX_set_cipher_list return value: " << rc; + EVLOG_AND_THROW(std::runtime_error("Could not set TLSv1.2 cipher list")); + } + + rc = SSL_CTX_set_ciphersuites(ctx, this->connection_options.supported_ciphers_13.c_str()); + if (rc != 1) { + EVLOG_debug << "SSL_CTX_set_cipher_list return value: " << rc; + } + + SSL_CTX_set_ecdh_auto(ctx, 1); + + if (this->connection_options.security_profile == 3) { + const char* path_key = nullptr; + const char* path_chain = nullptr; + + const auto certificate_key_pair = + this->evse_security->get_key_pair(CertificateSigningUseEnum::ChargingStationCertificate); + + if (!certificate_key_pair.has_value()) { + EVLOG_AND_THROW(std::runtime_error( + "Connecting with security profile 3 but no client side certificate is present or valid")); + } + + path_chain = certificate_key_pair.value().certificate_path.c_str(); + path_key = certificate_key_pair.value().key_path.c_str(); + + if (1 != SSL_CTX_use_certificate_chain_file(ctx, path_chain)) { + EVLOG_AND_THROW(std::runtime_error("Could not use client certificate file within SSL context")); + } + + if (SSL_CTX_use_PrivateKey_file(ctx, path_key, SSL_FILETYPE_PEM) != 1) { + EVLOG_AND_THROW(std::runtime_error("Could not set private key file within SSL context")); + } + + if (false == SSL_CTX_check_private_key(ctx)) { + EVLOG_AND_THROW(std::runtime_error("Could not check private key within SSL context")); + } + } + + if (this->evse_security->is_ca_certificate_installed(ocpp::CaCertificateType::CSMS)) { + std::string ca_csms = this->evse_security->get_verify_file(ocpp::CaCertificateType::CSMS); + + EVLOG_info << "Loading ca csms bundle to verify server certificate: " << ca_csms; + + rc = SSL_CTX_load_verify_locations(ctx, ca_csms.c_str(), NULL); + + if (rc != 1) { + EVLOG_error << "Could not load CA verify locations, error: " << ERR_error_string(ERR_get_error(), NULL); + EVLOG_AND_THROW(std::runtime_error("Could not load CA verify locations")); + } + } + + if (this->connection_options.use_ssl_default_verify_paths) { + rc = SSL_CTX_set_default_verify_paths(ctx); + if (rc != 1) { + EVLOG_error << "Could not load default CA verify path, error: " << ERR_error_string(ERR_get_error(), NULL); + EVLOG_AND_THROW(std::runtime_error("Could not load CA verify locations")); + } + } + + // Extra info + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); // to verify server certificate + // SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); +} + +void WebsocketTlsTPM::recv_loop() { + std::shared_ptr local_data = conn_data; + auto data = local_data.get(); + + if (data == nullptr) { + EVLOG_error << "Failed recv loop context init!"; + return; + } + + EVLOG_debug << "Init recv loop with ID: " << std::this_thread::get_id(); + + while (false == data->is_interupted()) { + if (false == recv_message_queue.empty()) { + std::string message{}; + + { + std::lock_guard lk(this->recv_mutex); + message = std::move(recv_message_queue.front()); + recv_message_queue.pop(); + } + + // Invoke our processing callback, that might trigger a send back that + // can cause a deadlock if is not managed on a different thread + this->message_callback(message); + } else { + std::unique_lock lock(this->recv_mutex); + recv_message_cv.wait_for(lock, std::chrono::seconds(10), + [&]() { return (false == recv_message_queue.empty()); }); + } + } + + EVLOG_debug << "Exit recv loop with ID: " << std::this_thread::get_id(); +} + +void WebsocketTlsTPM::client_loop() { + std::shared_ptr local_data = conn_data; + auto data = local_data.get(); + + if (data == nullptr) { + EVLOG_error << "Failed client loop context init!"; + return; + } + + // Bind thread for checks + local_data->bind_thread(std::this_thread::get_id()); + + // lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO | LLL_DEBUG | LLL_PARSER | LLL_HEADER + // | LLL_EXT | LLL_CLIENT | LLL_LATENCY | LLL_THREAD | LLL_USER, nullptr); + lws_set_log_level(LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE, nullptr); + + lws_context_creation_info info; + memset(&info, 0, sizeof(lws_context_creation_info)); + + info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; + info.port = CONTEXT_PORT_NO_LISTEN; /* we do not run any server */ + info.protocols = protocols; + info.user = this; + + info.fd_limit_per_thread = 1 + 1 + 1; + + bool use_tpm = connection_options.use_tpm_tls; + + // Setup context + OSSL_LIB_CTX* lib_ctx; + SSL_CTX* ssl_ctx; + + create_sec_context(use_tpm, lib_ctx, ssl_ctx); + + // Connection acquire the contexts + conn_data->sec_lib_context = std::unique_ptr(lib_ctx); + conn_data->sec_context = std::unique_ptr(ssl_ctx); + + // Init TLS data + tls_init(); + + // Setup our context + info.provided_client_ssl_ctx = ssl_ctx; + + lws_context* lws_ctx = lws_create_context(&info); + if (nullptr == lws_ctx) { + EVLOG_error << "lws init failed!"; + data->update_state(EConnectionState::FINALIZED); + } + + // Conn acquire the lws context + conn_data->lws_ctx = std::unique_ptr(lws_ctx); + + lws_client_connect_info i; + memset(&i, 0, sizeof(lws_client_connect_info)); + + int ssl_connection = LCCSCF_USE_SSL; + + // TODO: Completely remove after test + // ssl_connection |= LCCSCF_ALLOW_SELFSIGNED; + // ssl_connection |= LCCSCF_ALLOW_INSECURE; + // ssl_connection |= LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; + // ssl_connection |= LCCSCF_ALLOW_EXPIRED; + + auto& uri = this->connection_options.csms_uri; + + // TODO: No idea who releases the strdup? + i.context = lws_ctx; + i.port = uri.get_port(); + i.address = strdup(uri.get_hostname().c_str()); // Base address, as resolved by getnameinfo + i.path = strdup((uri.get_path() + uri.get_chargepoint_id()).c_str()); // Path of resource + i.host = i.address; + i.origin = i.address; + i.ssl_connection = ssl_connection; + i.protocol = strdup(conversions::ocpp_protocol_version_to_string(this->connection_options.ocpp_version).c_str()); + i.local_protocol_name = local_protocol_name; + i.pwsi = &conn_data->wsi; + i.userdata = this; + + // TODO (ioan): See if we need retry policy since we handle this manually + // i.retry_and_idle_policy = &retry; + + // Print data for debug + EVLOG_info << "LWS connect with info " + << "port: [" << i.port << "] address: [" << i.address << "] path: [" << i.path << "] protocol: [" + << i.protocol << "]"; + + lws* wsi = lws_client_connect_via_info(&i); + + if (nullptr == wsi) { + EVLOG_error << "LWS connect failed!"; + data->update_state(EConnectionState::FINALIZED); + } + + EVLOG_debug << "Init client loop with ID: " << std::this_thread::get_id(); + + // Process while we're running + int n = 0; + + while (n >= 0 && (false == data->is_interupted())) { + n = lws_service(data->get_ctx(), 0); + + if (false == message_queue.empty()) { + lws_callback_on_writable(data->get_conn()); + } + } + + // Client loop finished for our tid + EVLOG_debug << "Exit client loop with ID: " << std::this_thread::get_id(); +} + +bool WebsocketTlsTPM::connect() { + if (!this->initialized()) { + return false; + } + + EVLOG_info << "Connecting tpm TLS websocket to uri: " << this->connection_options.csms_uri.string() + << " with security-profile " << this->connection_options.security_profile + << " with TPM keys: " << this->connection_options.use_tpm_tls; + + // Interrupt any previous connection + if (this->conn_data) { + this->conn_data->do_interrupt(); + } + + auto conn_data = new ConnectionData(); + this->conn_data.reset(conn_data); + + // Wait old thread for a clean state + if (this->websocket_thread) { + this->websocket_thread->join(); + } + + if (this->recv_message_thread) { + this->recv_message_thread->join(); + } + + std::unique_lock lock(connection_mutex); + + // Release other threads + this->websocket_thread.reset(new std::thread(&WebsocketTlsTPM::client_loop, this)); + + // TODO(ioan): remove this thread when the fix will be moved into 'MessageQueue' + // The reason for having a received message processing thread is that because + // if we dispatch a message receive from the client_loop thread, then the callback + // will send back another message, and since we're waiting for that message to be + // sent over the wire on the client_loop, not giving the opportunity to the loop to + // advance we will have a dead-lock + this->recv_message_thread.reset(new std::thread(&WebsocketTlsTPM::recv_loop, this)); + + // Wait until connect or timeout + bool timeouted = !conn_cv.wait_for(lock, std::chrono::seconds(60), [&]() { + return false == conn_data->is_connecting() && EConnectionState::INITIALIZE != conn_data->get_state(); + }); + + EVLOG_info << "Connect finalized with state: " << (int)conn_data->get_state() << " Timeouted: " << timeouted; + bool connected = (conn_data->get_state() == EConnectionState::CONNECTED); + + if (false == connected) { + EVLOG_error << "Conn failed, interrupting."; + + // Interrupt and drop the connection data + this->conn_data->do_interrupt(); + this->conn_data.reset(); + } + + this->reconnect_callback = [this](const websocketpp::lib::error_code& ec) { + EVLOG_info << "Reconnecting to TLS websocket at uri: " << this->connection_options.csms_uri.string() + << " with security profile: " << this->connection_options.security_profile; + + // close connection before reconnecting + if (this->m_is_connected) { + EVLOG_info << "Closing websocket connection before reconnecting"; + this->close(websocketpp::close::status::abnormal_close, "Reconnect"); + } + + { + std::lock_guard lk(this->reconnect_mutex); + if (this->reconnect_timer_tpm) { + this->reconnect_timer_tpm->stop(); + } + this->reconnect_timer_tpm = nullptr; + } + + this->connect(); + }; + + return (connected); +} + +void WebsocketTlsTPM::reconnect(std::error_code reason, long delay) { + if (this->shutting_down) { + EVLOG_info << "Not reconnecting because the websocket is being shutdown."; + return; + } + + std::lock_guard lk(this->reconnect_mutex); + if (this->m_is_connected) { + EVLOG_info << "Closing websocket connection before reconnecting"; + this->close(websocketpp::close::status::abnormal_close, "Reconnect"); + } + + if (!this->reconnect_timer_tpm) { + EVLOG_info << "Reconnecting in: " << delay << "ms" + << ", attempt: " << this->connection_attempts; + + this->reconnect_timer_tpm = std::make_unique( + [this]() { this->reconnect_callback(websocketpp::lib::error_code()); }); + this->reconnect_timer_tpm->timeout(std::chrono::seconds(delay)); + } else { + EVLOG_info << "Reconnect timer already running"; + } +} + +void WebsocketTlsTPM::close(websocketpp::close::status::value code, const std::string& reason) { + EVLOG_info << "Closing TLS TPM websocket with reason: " << reason; + + { + std::lock_guard lk(this->reconnect_mutex); + if (this->reconnect_timer_tpm) { + this->reconnect_timer_tpm->stop(); + } + this->reconnect_timer_tpm = nullptr; + } + + if (conn_data) { + if (auto* data = conn_data.get()) { + // lws_close_reason(data->get_conn(), LWS_CLOSE_STATUS_NORMAL, NULL, 0); + data->do_interrupt(); + } + + // Release the connection data + conn_data.reset(); + } + + this->m_is_connected = false; + this->closed_callback(websocketpp::close::status::normal); +} + +void WebsocketTlsTPM::on_conn_connected() { + EVLOG_info << "OCPP client successfully connected to TLS websocket server"; + + this->connection_attempts = 1; // reset connection attempts + this->m_is_connected = true; + this->reconnecting = false; + + this->connected_callback(this->connection_options.security_profile); +} + +void WebsocketTlsTPM::on_conn_close() { + EVLOG_info << "OCPP client closed connection to TLS websocket server"; + + std::lock_guard lk(this->connection_mutex); + this->m_is_connected = false; + this->disconnected_callback(); + this->cancel_reconnect_timer(); + + this->closed_callback(websocketpp::close::status::normal); +} + +void WebsocketTlsTPM::on_conn_fail() { + EVLOG_error << "OCPP client connection failed to TLS websocket server"; + + std::lock_guard lk(this->connection_mutex); + this->m_is_connected = false; + this->disconnected_callback(); + this->connection_attempts += 1; + + // -1 indicates to always attempt to reconnect + if (this->connection_options.max_connection_attempts == -1 or + this->connection_attempts <= this->connection_options.max_connection_attempts) { + this->reconnect(std::error_code(), this->get_reconnect_interval()); + } else { + EVLOG_info << "Closed TLS websocket, reconnect attempts exhausted"; + this->close(websocketpp::close::status::normal, "Connection failed"); + } +} + +void WebsocketTlsTPM::on_message(void* msg, size_t len) { + if (!this->initialized()) { + EVLOG_error << "Message received but TLS websocket has not been correctly initialized. Discarding message."; + return; + } + + std::string message(reinterpret_cast(msg), len); + EVLOG_info << "Received message over TLS websocket polling for process: " << message; + + { + std::lock_guard lock(this->recv_mutex); + recv_message_queue.push(std::move(message)); + } + + recv_message_cv.notify_one(); +} + +static bool send_internal(lws* wsi, WebsocketMessage* msg) { + static std::vector buff; + + std::string& message = msg->payload; + size_t message_len = message.length(); + size_t buff_req_size = message_len + LWS_PRE; + + if (buff.size() < buff_req_size) + buff.resize(buff_req_size); + + // Copy data in send buffer + memcpy(&buff[LWS_PRE], message.data(), message_len); + + // TODO (ioan): if we require certain sending over the wire, + // we'll have to send chunked manually something like this: + + // Ethernet MTU: ~= 1500bytes + // size_t BUFF_SIZE = (MTU * 2); + // char *buff = alloca(LWS_PRE + BUFF_SIZE); + // memcpy(buff, message.data() + already_written, BUFF_SIZE); + // int flags = lws_write_ws_flags(proto, is_start, is_end); + // already_written += lws_write(wsi, buff + LWS_PRE, BUFF_SIZE - LWS_PRE, flags); + + auto sent = lws_write(wsi, reinterpret_cast(&buff[LWS_PRE]), message_len, msg->protocol); + + // Even if we written all the bytes to lws, it doesn't mean that it has been sent over + // the wire. According to the function comment (lws_write), until everything has been + // sent, the 'LWS_CALLBACK_CLIENT_WRITEABLE' callback will be suppressed. When we received + // another callback, it means that everything was sent and that we can mark the message + // as certainly 'sent' over the wire + msg->sent_bytes = sent; + + if (sent < 0) { + // Fatal error, conn closed + EVLOG_error << "Error sending message over TLS websocket, conn closed."; + return false; + } + + if (sent < message_len) { + EVLOG_error << "Error sending message over TLS websocket. Sent bytes: " << sent + << " Total to send: " << message_len; + return false; + } + + return true; +} + +void WebsocketTlsTPM::on_writable() { + if (!this->initialized() || !this->m_is_connected) { + EVLOG_error << "Message sending but TLS websocket has not been correctly initialized/connected."; + return; + } + + auto* data = conn_data.get(); + + if (nullptr == data) { + EVLOG_error << "Message sending TLS websocket with null connection data!"; + return; + } + + if (data->get_state() == EConnectionState::FINALIZED) { + EVLOG_error << "Trying to write message to finalized state!"; + return; + } + + while (false == message_queue.empty()) { + WebsocketMessage* message = nullptr; + + { + std::lock_guard lock(this->queue_mutex); + message = message_queue.front().get(); + } + + if (nullptr == message) { + EVLOG_AND_THROW(std::runtime_error("Null message in queue, fatal error!")); + } + + // Pop all sent messages + if (message->sent_bytes >= message->payload.length()) { + EVLOG_debug << "Message fully written, popping from queue!"; + + // If we have written all bytes to libwebsockets it means that if we received + // this writable callback everything is sent over the wire, mark the message + // as 'sent' and remove it from the queue + { + std::lock_guard lock(this->queue_mutex); + message->message_sent = true; + message_queue.pop(); + } + + // Notify any waiting thread to check it's state + msg_send_cv.notify_one(); + } else { + EVLOG_debug << "Client writable, sending message part!"; + + // Continue sending message part, for a single message only + bool sent = send_internal(data->get_conn(), message); + + // If we failed, attempt again later + if (false == sent) { + message->sent_bytes = 0; + } + + // Break loop + break; + } + } +} + +void WebsocketTlsTPM::request_write() { + if (this->m_is_connected) { + if (auto* data = conn_data.get()) { + if (data->get_conn()) + lws_callback_on_writable(data->get_conn()); + } + } +} + +void WebsocketTlsTPM::poll_message(const std::shared_ptr& msg, bool wait_send) { + if (std::this_thread::get_id() == conn_data->get_lws_thread_id()) { + EVLOG_AND_THROW(std::runtime_error("Deadlock detected, polling send from client lws thread!")); + } + + EVLOG_info << "Queueing message over TLS websocket: " << msg->payload; + + { + std::lock_guard lock(this->queue_mutex); + message_queue.emplace(msg); + } + + // Request a write callback + request_write(); + + if (wait_send) { + std::unique_lock lock(this->queue_mutex); + msg_send_cv.wait_for(lock, std::chrono::seconds(10), [&] { return (true == msg->message_sent); }); + } + + if (msg->message_sent) + EVLOG_debug << "Successfully sent last message over TLS websocket!"; + else + EVLOG_warning << "Could not send last message over TLS websocket!"; +} + +bool WebsocketTlsTPM::send(const std::string& message) { + if (!this->initialized()) { + EVLOG_error << "Could not send message because websocket is not properly initialized."; + return false; + } + + auto msg = std::make_shared(); + msg->payload = std::move(message); + msg->protocol = LWS_WRITE_TEXT; + + poll_message(msg, true); + + return msg->message_sent; +} + +void WebsocketTlsTPM::ping() { + if (!this->initialized()) { + EVLOG_error << "Could not send ping because websocket is not properly initialized."; + } + + auto msg = std::make_shared(); + msg->payload = this->connection_options.ping_payload; + msg->protocol = LWS_WRITE_PING; + + poll_message(msg, true); +} + +int WebsocketTlsTPM::process_callback(void* wsi_ptr, int callback_reason, void* user, void* in, size_t len) { + lws* wsi = reinterpret_cast(wsi_ptr); + + enum lws_callback_reasons reason = static_cast(callback_reason); + ConnectionData* data = this->conn_data.get(); + + // If we are in the process of deletion, just close socket and return + if (nullptr == data) + return -1; + + switch (reason) { + // TODO: If required in the future + case LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION: + break; + case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS: + break; + + case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER: { + unsigned char **ptr = reinterpret_cast(in), *end_header = (*ptr) + len; + + if (this->connection_options.hostName.has_value()) { + auto& str = this->connection_options.hostName.value(); + EVLOG_info << "User-Host is set to " << str; + + if (0 != lws_add_http_header_by_name(wsi, (unsigned char*)"User-Host", (unsigned char*)str.c_str(), + str.length(), ptr, end_header)) { + EVLOG_AND_THROW(std::runtime_error("Could not append authorization header.")); + } + } + + if (this->connection_options.security_profile == 2) { + std::optional authorization_header = this->getAuthorizationHeader(); + + if (authorization_header != std::nullopt) { + auto& str = authorization_header.value(); + if (0 != lws_add_http_header_by_name(wsi, (unsigned char*)"Authorization", (unsigned char*)str.c_str(), + str.length(), ptr, end_header)) { + EVLOG_AND_THROW(std::runtime_error("Could not append authorization header.")); + } + } else { + EVLOG_AND_THROW( + std::runtime_error("No authorization key provided when connecting with security profile 2 or 3.")); + } + } + + return 0; + } break; + + case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: + EVLOG_error << "CLIENT_CONNECTION_ERROR: " << (in ? reinterpret_cast(in) : "(null)"); + + if (data->get_state() == EConnectionState::CONNECTING) { + data->update_state(EConnectionState::ERROR); + conn_cv.notify_one(); + } + + on_conn_fail(); + return -1; + + case LWS_CALLBACK_CONNECTING: + EVLOG_info << "Client connecting..."; + data->update_state(EConnectionState::CONNECTING); + break; + + case LWS_CALLBACK_CLIENT_ESTABLISHED: + if (data->get_state() == EConnectionState::CONNECTING) { + data->update_state(EConnectionState::CONNECTED); + conn_cv.notify_one(); + } + + on_conn_connected(); + + // Attempt first write after connection + lws_callback_on_writable(wsi); + break; + + case LWS_CALLBACK_WS_PEER_INITIATED_CLOSE: { + std::string close_reason(reinterpret_cast(in), len); + unsigned char* pp = reinterpret_cast(in); + unsigned short close_code = (unsigned short)((pp[0] << 8) | pp[1]); + EVLOG_info << "Client close reason: " << close_reason << " close code: " << close_code; + // Return 0 to print peer close reason + return 0; + } + + case LWS_CALLBACK_CLIENT_CLOSED: + data->update_state(EConnectionState::FINALIZED); + data->do_interrupt(); + on_conn_close(); + break; + + case LWS_CALLBACK_WS_CLIENT_DROP_PROTOCOL: + break; + + case LWS_CALLBACK_CLIENT_WRITEABLE: + on_writable(); + + if (false == message_queue.empty()) + lws_callback_on_writable(wsi); + break; + + case LWS_CALLBACK_CLIENT_RECEIVE: + on_message(in, len); + break; + + default: + EVLOG_info << "Callback with unhandled reason: " << reason; + break; + } + + if (data->is_interupted()) { + EVLOG_info << "Conn interrupted, closing socket!"; + return -1; + } + + // Return -1 on fatal error (-1 is request to close the socket) + return 0; +} + +} // namespace ocpp diff --git a/lib/ocpp/v16/charge_point_configuration.cpp b/lib/ocpp/v16/charge_point_configuration.cpp index 554e2218b0..a4f57b2724 100644 --- a/lib/ocpp/v16/charge_point_configuration.cpp +++ b/lib/ocpp/v16/charge_point_configuration.cpp @@ -309,6 +309,10 @@ bool ChargePointConfiguration::getVerifyCsmsCommonName() { return this->config["Internal"]["VerifyCsmsCommonName"]; } +bool ChargePointConfiguration::getUseTPM() { + return this->config["Internal"]["UseTPM"]; +} + KeyValue ChargePointConfiguration::getChargePointIdKeyValue() { KeyValue kv; kv.key = "ChargePointId";