From 32d7bf4e93b86f0650e709d0e9c427e6430bdedb Mon Sep 17 00:00:00 2001 From: Thomas Markwalder Date: Tue, 4 Oct 2022 06:51:58 -0400 Subject: [PATCH] [#2583] Initial addition of src/lib/tcp Added first draft of TcpListener classes. They compile but don't do anything useful. configure.ac src/lib/Makefile.am added tcp src/lib/tcp/README src/lib/tcp/tcp_connection.cc src/lib/tcp/tcp_connection.h src/lib/tcp/tcp_connection_acceptor.h src/lib/tcp/tcp_connection_pool.cc src/lib/tcp/tcp_connection_pool.h src/lib/tcp/tcp_listener.cc src/lib/tcp/tcp_listener.h src/lib/tcp/tcp_log.cc src/lib/tcp/tcp_log.h src/lib/tcp/tcp_messages.cc src/lib/tcp/tcp_messages.h src/lib/tcp/tcp_messages.mes src/lib/tcp/tests/.gitignore src/lib/tcp/tests/Makefile.am src/lib/tcp/tests/run_unittests.cc - new files --- configure.ac | 2 + src/lib/Makefile.am | 2 +- src/lib/tcp/Makefile.am | 76 ++++ src/lib/tcp/README | 1 + src/lib/tcp/tcp_connection.cc | 578 ++++++++++++++++++++++++++ src/lib/tcp/tcp_connection.h | 281 +++++++++++++ src/lib/tcp/tcp_connection_acceptor.h | 38 ++ src/lib/tcp/tcp_connection_pool.cc | 78 ++++ src/lib/tcp/tcp_connection_pool.h | 78 ++++ src/lib/tcp/tcp_listener.cc | 110 +++++ src/lib/tcp/tcp_listener.h | 122 ++++++ src/lib/tcp/tcp_log.cc | 21 + src/lib/tcp/tcp_log.h | 23 + src/lib/tcp/tcp_messages.cc | 55 +++ src/lib/tcp/tcp_messages.h | 31 ++ src/lib/tcp/tcp_messages.mes | 96 +++++ src/lib/tcp/tests/.gitignore | 1 + src/lib/tcp/tests/Makefile.am | 58 +++ src/lib/tcp/tests/run_unittests.cc | 19 + 19 files changed, 1669 insertions(+), 1 deletion(-) create mode 100644 src/lib/tcp/Makefile.am create mode 100644 src/lib/tcp/README create mode 100644 src/lib/tcp/tcp_connection.cc create mode 100644 src/lib/tcp/tcp_connection.h create mode 100644 src/lib/tcp/tcp_connection_acceptor.h create mode 100644 src/lib/tcp/tcp_connection_pool.cc create mode 100644 src/lib/tcp/tcp_connection_pool.h create mode 100644 src/lib/tcp/tcp_listener.cc create mode 100644 src/lib/tcp/tcp_listener.h create mode 100644 src/lib/tcp/tcp_log.cc create mode 100644 src/lib/tcp/tcp_log.h create mode 100644 src/lib/tcp/tcp_messages.cc create mode 100644 src/lib/tcp/tcp_messages.h create mode 100644 src/lib/tcp/tcp_messages.mes create mode 100644 src/lib/tcp/tests/.gitignore create mode 100644 src/lib/tcp/tests/Makefile.am create mode 100644 src/lib/tcp/tests/run_unittests.cc diff --git a/configure.ac b/configure.ac index d54a933ab3..3c330c9671 100644 --- a/configure.ac +++ b/configure.ac @@ -1583,6 +1583,8 @@ AC_CONFIG_FILES([src/lib/process/testutils/Makefile]) AC_CONFIG_FILES([src/lib/stats/Makefile]) AC_CONFIG_FILES([src/lib/stats/tests/Makefile]) AC_CONFIG_FILES([src/lib/stats/testutils/Makefile]) +AC_CONFIG_FILES([src/lib/tcp/Makefile]) +AC_CONFIG_FILES([src/lib/tcp/tests/Makefile]) AC_CONFIG_FILES([src/lib/testutils/Makefile]) AC_CONFIG_FILES([src/lib/testutils/dhcp_test_lib.sh], [chmod +x src/lib/testutils/dhcp_test_lib.sh]) diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am index 26944d166d..d7abdbce1d 100644 --- a/src/lib/Makefile.am +++ b/src/lib/Makefile.am @@ -9,7 +9,7 @@ if HAVE_PGSQL SUBDIRS += pgsql endif -SUBDIRS += config_backend hooks dhcp http config stats +SUBDIRS += config_backend hooks dhcp tcp http config stats if HAVE_NETCONF SUBDIRS += yang diff --git a/src/lib/tcp/Makefile.am b/src/lib/tcp/Makefile.am new file mode 100644 index 0000000000..c5ac22c3dd --- /dev/null +++ b/src/lib/tcp/Makefile.am @@ -0,0 +1,76 @@ +SUBDIRS = . tests + +AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib +AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) +AM_CXXFLAGS = $(KEA_CXXFLAGS) + +EXTRA_DIST = # tcp.dox + +# Ensure that the message file is included in the distribution +EXTRA_DIST += tcp_messages.mes + +CLEANFILES = *.gcno *.gcda + +lib_LTLIBRARIES = libkea-tcp.la + +libkea_tcp_la_SOURCES = tcp_connection.cc tcp_connection.h +libkea_tcp_la_SOURCES += tcp_connection_pool.cc tcp_connection_pool.h +libkea_tcp_la_SOURCES += tcp_listener.cc tcp_listener.h +libkea_tcp_la_SOURCES += tcp_log.cc tcp_log.h +libkea_tcp_la_SOURCES += tcp_messages.cc tcp_messages.h + +libkea_tcp_la_CXXFLAGS = $(AM_CXXFLAGS) +libkea_tcp_la_CPPFLAGS = $(AM_CPPFLAGS) +libkea_tcp_la_LDFLAGS = $(AM_LDFLAGS) +libkea_tcp_la_LDFLAGS += -no-undefined -version-info 52:0:0 + +libkea_tcp_la_LIBADD = +libkea_tcp_la_LIBADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la +libkea_tcp_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la +libkea_tcp_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la +libkea_tcp_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la +libkea_tcp_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la +libkea_tcp_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la +libkea_tcp_la_LIBADD += $(LOG4CPLUS_LIBS) $(BOOST_LIBS) $(CRYPTO_LIBS) + +# If we want to get rid of all generated messages files, we need to use +# make maintainer-clean. The proper way to introduce custom commands for +# that operation is to define maintainer-clean-local target. However, +# make maintainer-clean also removes Makefile, so running configure script +# is required. To make it easy to rebuild messages without going through +# reconfigure, a new target messages-clean has been added. +maintainer-clean-local: + rm -f tcp_messages.h tcp_messages.cc + +# To regenerate messages files, one can do: +# +# make messages-clean +# make messages +# +# This is needed only when a .mes file is modified. +messages-clean: maintainer-clean-local + +if GENERATE_MESSAGES + +# Define rule to build logging source files from message file +messages: tcp_messages.h tcp_messages.cc + @echo Message files regenerated + +tcp_messages.h tcp_messages.cc: tcp_messages.mes + $(top_builddir)/src/lib/log/compiler/kea-msg-compiler $(top_srcdir)/src/lib/tcp/tcp_messages.mes + +else + +messages tcp_messages.h tcp_messages.cc: + @echo Messages generation disabled. Configure with --enable-generate-messages to enable it. + +endif + +# Specify the headers for copying into the installation directory tree. +libkea_tcp_includedir = $(pkgincludedir)/tcp +libkea_tcp_include_HEADERS = \ + tcp_messages.h \ + tcp_connection.h \ + tcp_connection_pool.h \ + tcp_listener.h \ + tcp_log.h diff --git a/src/lib/tcp/README b/src/lib/tcp/README new file mode 100644 index 0000000000..68ef83fa67 --- /dev/null +++ b/src/lib/tcp/README @@ -0,0 +1 @@ +The tcp library is intended to provide support for TCP server/listeners. diff --git a/src/lib/tcp/tcp_connection.cc b/src/lib/tcp/tcp_connection.cc new file mode 100644 index 0000000000..cfecd9d7b1 --- /dev/null +++ b/src/lib/tcp/tcp_connection.cc @@ -0,0 +1,578 @@ +// Copyright (C) 2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#if 0 +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace isc::asiolink; +namespace ph = std::placeholders; + +namespace { + +/// @brief Maximum size of the HTTP message that can be logged. +/// +/// The part of the HTTP message beyond this value is truncated. +constexpr size_t MAX_LOGGED_MESSAGE_SIZE = 1024; + +} + +namespace isc { +namespace http { + +void +TcpConnection:: +SocketCallback::operator()(boost::system::error_code ec, size_t length) { + if (ec.value() == boost::asio::error::operation_aborted) { + return; + } + callback_(ec, length); +} + +TcpConnection::TcpConnection(asiolink::IOService& io_service, + const TcpAcceptorPtr& acceptor, + const TlsContextPtr& tls_context, + TcpConnectionPool& connection_pool, + const TcpResponseCreatorPtr& response_creator, + const TcpAcceptorCallback& callback, + const long request_timeout, + const long idle_timeout) + : request_timer_(io_service), + request_timeout_(request_timeout), + tls_context_(tls_context), + idle_timeout_(idle_timeout), + tcp_socket_(), + tls_socket_(), + acceptor_(acceptor), + connection_pool_(connection_pool), + response_creator_(response_creator), + acceptor_callback_(callback) { + if (!tls_context) { + tcp_socket_.reset(new asiolink::TCPSocket(io_service)); + } else { + tls_socket_.reset(new asiolink::TLSSocket(io_service, + tls_context)); + } +} + +TcpConnection::~TcpConnection() { + close(); +} + +void +TcpConnection::recordParameters(const TcpRequestPtr& request) const { + if (!request) { + // Should never happen. + return; + } + + // Record the remote address. + request->setRemote(getRemoteEndpointAddressAsText()); + + // Record TLS parameters. + if (!tls_socket_) { + return; + } + + // The connection uses HTTPS aka HTTP over TLS. + request->setTls(true); + + // Record the first commonName of the subjectName of the client + // certificate when wanted. + if (TcpRequest::recordSubject_) { + request->setSubject(tls_socket_->getTlsStream().getSubject()); + } + + // Record the first commonName of the issuerName of the client + // certificate when wanted. + if (TcpRequest::recordIssuer_) { + request->setIssuer(tls_socket_->getTlsStream().getIssuer()); + } +} + +void +TcpConnection::shutdownCallback(const boost::system::error_code&) { + tls_socket_->close(); +} + +void +TcpConnection::shutdown() { + request_timer_.cancel(); + if (tcp_socket_) { + tcp_socket_->close(); + return; + } + if (tls_socket_) { + // Create instance of the callback to close the socket. + SocketCallback cb(std::bind(&TcpConnection::shutdownCallback, + shared_from_this(), + ph::_1)); // error_code + tls_socket_->shutdown(cb); + return; + } + // Not reachable? + isc_throw(Unexpected, "internal error: unable to shutdown the socket"); +} + +void +TcpConnection::close() { + request_timer_.cancel(); + if (tcp_socket_) { + tcp_socket_->close(); + return; + } + if (tls_socket_) { + tls_socket_->close(); + return; + } + // Not reachable? + isc_throw(Unexpected, "internal error: unable to close the socket"); +} + +void +TcpConnection::shutdownConnection() { + try { + LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_BASIC, + TCP_CONNECTION_SHUTDOWN) + .arg(getRemoteEndpointAddressAsText()); + connection_pool_.shutdown(shared_from_this()); + } catch (...) { + LOG_ERROR(asiolink_logger, TCP_CONNECTION_SHUTDOWN_FAILED); + } +} + +void +TcpConnection::stopThisConnection() { + try { + LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_BASIC, + TCP_CONNECTION_STOP) + .arg(getRemoteEndpointAddressAsText()); + connection_pool_.stop(shared_from_this()); + } catch (...) { + LOG_ERROR(asiolink_logger, TCP_CONNECTION_STOP_FAILED); + } +} + +void +TcpConnection::asyncAccept() { + // Create instance of the callback. It is safe to pass the local instance + // of the callback, because the underlying boost functions make copies + // as needed. + TcpAcceptorCallback cb = std::bind(&TcpConnection::acceptorCallback, + shared_from_this(), + ph::_1); // error + try { + TcpsAcceptorPtr tls_acceptor = + boost::dynamic_pointer_cast(acceptor_); + if (!tls_acceptor) { + if (!tcp_socket_) { + isc_throw(Unexpected, "internal error: TCP socket is null"); + } + acceptor_->asyncAccept(*tcp_socket_, cb); + } else { + if (!tls_socket_) { + isc_throw(Unexpected, "internal error: TLS socket is null"); + } + tls_acceptor->asyncAccept(*tls_socket_, cb); + } + } catch (const std::exception& ex) { + isc_throw(TcpConnectionError, "unable to start accepting TCP " + "connections: " << ex.what()); + } +} + +void +TcpConnection::doHandshake() { + // Skip the handshake if the socket is not a TLS one. + if (!tls_socket_) { + doRead(); + return; + } + + // Create instance of the callback. It is safe to pass the local instance + // of the callback, because the underlying boost functions make copies + // as needed. + SocketCallback cb(std::bind(&TcpConnection::handshakeCallback, + shared_from_this(), + ph::_1)); // error + try { + tls_socket_->handshake(cb); + + } catch (const std::exception& ex) { + isc_throw(TcpConnectionError, "unable to perform TLS handshake: " + << ex.what()); + } +} + +void +TcpConnection::doRead(TransactionPtr transaction) { + try { + TCPEndpoint endpoint; + + // Transaction hasn't been created if we are starting to read the + // new request. + if (!transaction) { + transaction = Transaction::create(response_creator_); + recordParameters(transaction->getRequest()); + } + + // Create instance of the callback. It is safe to pass the local instance + // of the callback, because the underlying std functions make copies + // as needed. + SocketCallback cb(std::bind(&TcpConnection::socketReadCallback, + shared_from_this(), + transaction, + ph::_1, // error + ph::_2)); //bytes_transferred + if (tcp_socket_) { + tcp_socket_->asyncReceive(static_cast(transaction->getInputBufData()), + transaction->getInputBufSize(), + 0, &endpoint, cb); + return; + } + if (tls_socket_) { + tls_socket_->asyncReceive(static_cast(transaction->getInputBufData()), + transaction->getInputBufSize(), + 0, &endpoint, cb); + return; + } + } catch (...) { + stopThisConnection(); + } +} + +void +TcpConnection::doWrite(TcpConnection::TransactionPtr transaction) { + try { + if (transaction->outputDataAvail()) { + // Create instance of the callback. It is safe to pass the local instance + // of the callback, because the underlying std functions make copies + // as needed. + SocketCallback cb(std::bind(&TcpConnection::socketWriteCallback, + shared_from_this(), + transaction, + ph::_1, // error + ph::_2)); // bytes_transferred + if (tcp_socket_) { + tcp_socket_->asyncSend(transaction->getOutputBufData(), + transaction->getOutputBufSize(), + cb); + return; + } + if (tls_socket_) { + tls_socket_->asyncSend(transaction->getOutputBufData(), + transaction->getOutputBufSize(), + cb); + return; + } + } else { + // The isPersistent() function may throw if the request hasn't + // been created, i.e. the HTTP headers weren't parsed. We catch + // this exception below and close the connection since we're + // unable to tell if the connection should remain persistent + // or not. The default is to close it. + if (!transaction->getRequest()->isPersistent()) { + stopThisConnection(); + + } else { + // The connection is persistent and we are done sending + // the previous response. Start listening for the next + // requests. + setupIdleTimer(); + doRead(); + } + } + } catch (...) { + stopThisConnection(); + } +} + +void +TcpConnection::asyncSendResponse(const ConstTcpResponsePtr& response, + TransactionPtr transaction) { + transaction->setOutputBuf(response->toString()); + doWrite(transaction); +} + + +void +TcpConnection::acceptorCallback(const boost::system::error_code& ec) { + if (!acceptor_->isOpen()) { + return; + } + + if (ec) { + stopThisConnection(); + } + + acceptor_callback_(ec); + + if (!ec) { + if (!tls_context_) { + LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_DETAIL, + TCP_REQUEST_RECEIVE_START) + .arg(getRemoteEndpointAddressAsText()) + .arg(static_cast(request_timeout_/1000)); + } else { + LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_DETAIL, + TCP_CONNECTION_HANDSHAKE_START) + .arg(getRemoteEndpointAddressAsText()) + .arg(static_cast(request_timeout_/1000)); + } + + setupRequestTimer(); + doHandshake(); + } +} + +void +TcpConnection::handshakeCallback(const boost::system::error_code& ec) { + if (ec) { + LOG_INFO(asiolink_logger, TCP_CONNECTION_HANDSHAKE_FAILED) + .arg(getRemoteEndpointAddressAsText()) + .arg(ec.message()); + stopThisConnection(); + } else { + LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_DETAIL, + HTTPS_REQUEST_RECEIVE_START) + .arg(getRemoteEndpointAddressAsText()); + + doRead(); + } +} + +void +TcpConnection::socketReadCallback(TcpConnection::TransactionPtr transaction, + boost::system::error_code ec, size_t length) { + if (ec) { + // IO service has been stopped and the connection is probably + // going to be shutting down. + if (ec.value() == boost::asio::error::operation_aborted) { + return; + + // EWOULDBLOCK and EAGAIN are special cases. Everything else is + // treated as fatal error. + } else if ((ec.value() != boost::asio::error::try_again) && + (ec.value() != boost::asio::error::would_block)) { + stopThisConnection(); + + // We got EWOULDBLOCK or EAGAIN which indicate that we may be able to + // read something from the socket on the next attempt. Just make sure + // we don't try to read anything now in case there is any garbage + // passed in length. + } else { + length = 0; + } + } + + // Receiving is in progress, so push back the timeout. + setupRequestTimer(transaction); + + if (length != 0) { + LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_DETAIL_DATA, + TCP_DATA_RECEIVED) + .arg(length) + .arg(getRemoteEndpointAddressAsText()); + + transaction->getParser()->postBuffer(static_cast(transaction->getInputBufData()), + length); + transaction->getParser()->poll(); + } + + if (transaction->getParser()->needData()) { + // The parser indicates that the some part of the message being + // received is still missing, so continue to read. + doRead(transaction); + + } else { + try { + // The whole message has been received, so let's finalize it. + transaction->getRequest()->finalize(); + + LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_BASIC, + TCP_CLIENT_REQUEST_RECEIVED) + .arg(getRemoteEndpointAddressAsText()); + + LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_BASIC_DATA, + TCP_CLIENT_REQUEST_RECEIVED_DETAILS) + .arg(getRemoteEndpointAddressAsText()) + .arg(transaction->getParser()->getBufferAsString(MAX_LOGGED_MESSAGE_SIZE)); + + } catch (const std::exception& ex) { + LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_BASIC, + TCP_BAD_CLIENT_REQUEST_RECEIVED) + .arg(getRemoteEndpointAddressAsText()) + .arg(ex.what()); + + LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_BASIC_DATA, + TCP_BAD_CLIENT_REQUEST_RECEIVED_DETAILS) + .arg(getRemoteEndpointAddressAsText()) + .arg(transaction->getParser()->getBufferAsString(MAX_LOGGED_MESSAGE_SIZE)); + } + + // Don't want to timeout if creation of the response takes long. + request_timer_.cancel(); + + // Create the response from the received request using the custom + // response creator. + TcpResponsePtr response = response_creator_->createTcpResponse(transaction->getRequest()); + LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_BASIC, + TCP_SERVER_RESPONSE_SEND) + .arg(response->toBriefString()) + .arg(getRemoteEndpointAddressAsText()); + + LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_BASIC_DATA, + TCP_SERVER_RESPONSE_SEND_DETAILS) + .arg(getRemoteEndpointAddressAsText()) + .arg(TcpMessageParserBase::logFormatTcpMessage(response->toString(), + MAX_LOGGED_MESSAGE_SIZE)); + + // Response created. Activate the timer again. + setupRequestTimer(transaction); + + // Start sending the response. + asyncSendResponse(response, transaction); + } +} + +void +TcpConnection::socketWriteCallback(TcpConnection::TransactionPtr transaction, + boost::system::error_code ec, size_t length) { + if (ec) { + // IO service has been stopped and the connection is probably + // going to be shutting down. + if (ec.value() == boost::asio::error::operation_aborted) { + return; + + // EWOULDBLOCK and EAGAIN are special cases. Everything else is + // treated as fatal error. + } else if ((ec.value() != boost::asio::error::try_again) && + (ec.value() != boost::asio::error::would_block)) { + stopThisConnection(); + + // We got EWOULDBLOCK or EAGAIN which indicate that we may be able to + // read something from the socket on the next attempt. + } else { + // Sending is in progress, so push back the timeout. + setupRequestTimer(transaction); + + doWrite(transaction); + } + } + + // Since each transaction has its own output buffer, it is not really + // possible that the number of bytes written is larger than the size + // of the buffer. But, let's be safe and set the length to the size + // of the buffer if that unexpected condition occurs. + if (length > transaction->getOutputBufSize()) { + length = transaction->getOutputBufSize(); + } + + if (length <= transaction->getOutputBufSize()) { + // Sending is in progress, so push back the timeout. + setupRequestTimer(transaction); + } + + // Eat the 'length' number of bytes from the output buffer and only + // leave the part of the response that hasn't been sent. + transaction->consumeOutputBuf(length); + + // Schedule the write of the unsent data. + doWrite(transaction); +} + +void +TcpConnection::setupRequestTimer(TransactionPtr transaction) { + // Pass raw pointer rather than shared_ptr to this object, + // because IntervalTimer already passes shared pointer to the + // IntervalTimerImpl to make sure that the callback remains + // valid. + request_timer_.setup(std::bind(&TcpConnection::requestTimeoutCallback, + this, transaction), + request_timeout_, IntervalTimer::ONE_SHOT); +} + +void +TcpConnection::setupIdleTimer() { + request_timer_.setup(std::bind(&TcpConnection::idleTimeoutCallback, + this), + idle_timeout_, IntervalTimer::ONE_SHOT); +} + +void +TcpConnection::requestTimeoutCallback(TransactionPtr transaction) { + LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_DETAIL, + TCP_CLIENT_REQUEST_TIMEOUT_OCCURRED) + .arg(getRemoteEndpointAddressAsText()); + + // We need to differentiate the transactions between a normal response and the + // timeout. We create new transaction from the current transaction. It is + // to preserve the request we're responding to. + auto spawned_transaction = Transaction::spawn(response_creator_, transaction); + + // The new transaction inherits the request from the original transaction + // if such transaction exists. + auto request = spawned_transaction->getRequest(); + + // Depending on when the timeout occurred, the TCP version of the request + // may or may not be available. Therefore we check if the HTTP version is + // set in the request. If it is not available, we need to create a dummy + // request with the default HTTP/1.0 version. This version will be used + // in the response. + if (request->context()->http_version_major_ == 0) { + request.reset(new TcpRequest(TcpRequest::Method::HTTP_POST, "/", + TcpVersion::HTTP_10(), + HostTcpHeader("dummy"))); + request->finalize(); + } + + // Create the timeout response. + TcpResponsePtr response = + response_creator_->createStockTcpResponse(request, + TcpStatusCode::REQUEST_TIMEOUT); + + // Send the HTTP 408 status. + asyncSendResponse(response, spawned_transaction); +} + +void +TcpConnection::idleTimeoutCallback() { + LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_DETAIL, + TCP_IDLE_CONNECTION_TIMEOUT_OCCURRED) + .arg(getRemoteEndpointAddressAsText()); + // In theory we should shutdown first and stop/close after but + // it is better to put the connection management responsibility + // on the client... so simply drop idle connections. + stopThisConnection(); +} + +std::string +TcpConnection::getRemoteEndpointAddressAsText() const { + try { + if (tcp_socket_) { + if (tcp_socket_->getASIOSocket().is_open()) { + return (tcp_socket_->getASIOSocket().remote_endpoint().address().to_string()); + } + } else if (tls_socket_) { + if (tls_socket_->getASIOSocket().is_open()) { + return (tls_socket_->getASIOSocket().remote_endpoint().address().to_string()); + } + } + } catch (...) { + } + return ("(unknown address)"); +} + +} // end of namespace isc::http +} // end of namespace isc +#endif diff --git a/src/lib/tcp/tcp_connection.h b/src/lib/tcp/tcp_connection.h new file mode 100644 index 0000000000..70c1a6d219 --- /dev/null +++ b/src/lib/tcp/tcp_connection.h @@ -0,0 +1,281 @@ +// Copyright (C) 2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef TCP_CONNECTION_H +#define TCP_CONNECTION_H + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace isc { +namespace tcp { + +/// @todo TKM these are place holders while I think output how it should work +typedef util::InputBuffer TcpRequest; +typedef boost::shared_ptr TcpRequestPtr; + +typedef util::OutputBuffer TcpResponse; +typedef boost::shared_ptr TcpResponsePtr; + + +/// @brief Generic error reported within @ref TcpConnection class. +class TcpConnectionError : public Exception { +public: + TcpConnectionError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief Forward declaration to the @ref TcpConnectionPool. +/// +/// This declaration is needed because we don't include the header file +/// declaring @ref TcpConnectionPool to avoid circular inclusion. +class TcpConnectionPool; + +class TcpConnection; +/// @brief Pointer to the @ref TcpConnection. +typedef boost::shared_ptr TcpConnectionPtr; + +/// @brief Accepts and handles a single TCP connection. +class TcpConnection : public boost::enable_shared_from_this { +private: + + /// @brief Type of the function implementing a callback invoked by the + /// @c SocketCallback functor. + typedef std::function + SocketCallbackFunction; + + /// @brief Functor associated with the socket object. + /// + /// This functor calls a callback function specified in the constructor. + class SocketCallback { + public: + + /// @brief Constructor. + /// + /// @param socket_callback Callback to be invoked by the functor upon + /// an event associated with the socket. + SocketCallback(SocketCallbackFunction socket_callback) + : callback_(socket_callback) { + } + + /// @brief Operator called when event associated with a socket occurs. + /// + /// This operator returns immediately when received error code is + /// @c boost::system::error_code is equal to + /// @c boost::asio::error::operation_aborted, i.e. the callback is not + /// invoked. + /// + /// @param ec Error code. + /// @param length Data length. + void operator()(boost::system::error_code ec, size_t length = 0); + + private: + /// @brief Supplied callback. + SocketCallbackFunction callback_; + }; + +public: + + /// @brief Constructor. + /// + /// @param io_service IO service to be used by the connection. + /// @param acceptor Pointer to the TCP acceptor object used to listen for + /// new TCP connections. + /// @param tls_context TLS context. + /// @param connection_pool Connection pool in which this connection is + /// stored. + /// @param response_creator Pointer to the response creator object used to + /// create TCP response from the TCP request received. + /// @param callback Callback invoked when new connection is accepted. + /// @param request_timeout Configured timeout for a TCP request. + /// @param idle_timeout Timeout after which persistent TCP connection is + /// closed by the server. + TcpConnection(asiolink::IOService& io_service, + const TcpConnectionAcceptorPtr& acceptor, + const asiolink::TlsContextPtr& tls_context, + TcpConnectionPool& connection_pool, + const TcpConnectionAcceptorCallback& callback, + const long idle_timeout); + + /// @brief Destructor. + /// + /// Closes current connection. + virtual ~TcpConnection(); + + /// @brief Asynchronously accepts new connection. + /// + /// When the connection is established successfully, the timeout timer is + /// setup and the asynchronous handshake with client is performed. + void asyncAccept(); + + /// @brief Shutdown the socket. + void shutdown(); + + /// @brief Closes the socket. + void close(); + + /// @brief Asynchronously performs TLS handshake. + /// + /// When the handshake is performed successfully or skipped because TLS + /// was not enabled, the asynchronous read from the socket is started. + void doHandshake(); + + /// @brief Starts asynchronous read from the socket. + /// + /// The data received over the socket are supplied to the TCP parser until + /// the parser signals that the entire request has been received or until + /// the parser signals an error. In the former case the server creates an + /// TCP response using supplied response creator object. + /// + /// In case of error the connection is stopped. + /// + /// @param request Pointer to the request for which the read + /// operation should be performed. It defaults to null pointer which + /// indicates that this function should create new request. + void doRead(); + +protected: + + /// @brief Starts asynchronous write to the socket. + /// + /// The @c output_buf_ must contain the data to be sent. + /// + /// In case of error the connection is stopped. + /// + /// @param request Pointer to the request for which the write + /// operation should be performed. + void doWrite(); + + /// @brief Sends TCP response asynchronously. + /// + /// Internally it calls @ref TcpConnection::doWrite to send the data. + /// + /// @param response Pointer to the TCP response to be sent. + /// @param request Pointer to the request. + void asyncSendResponse(const TcpResponsePtr& response); + + /// @brief Local callback invoked when new connection is accepted. + /// + /// It invokes external (supplied via constructor) acceptor callback. If + /// the acceptor is not opened it returns immediately. If the connection + /// is accepted successfully the @ref TcpConnection::doRead or + /// @ref TcpConnection::doHandshake is called. + /// + /// @param ec Error code. + void acceptorCallback(const boost::system::error_code& ec); + + /// @brief Local callback invoked when TLS handshake is performed. + /// + /// If the handshake is performed successfully the @ref + /// TcpConnection::doRead is called. + /// + /// @param ec Error code. + void handshakeCallback(const boost::system::error_code& ec); + + /// @brief Callback invoked when new data is received over the socket. + /// + /// This callback supplies the data to the TCP parser and continues + /// parsing. When the parser signals end of the TCP request the callback + /// prepares a response and starts asynchronous send over the socket. + /// + /// @param request Pointer to the request for which the callback + /// is invoked. + /// @param ec Error code. + /// @param length Length of the received data. + void socketReadCallback(TcpRequestPtr request, + boost::system::error_code ec, + size_t length); + + /// @brief Callback invoked when data is sent over the socket. + /// + /// @param request Pointer to the request for which the callback + /// is invoked. + /// @param ec Error code. + /// @param length Length of the data sent. + virtual void socketWriteCallback(TcpRequestPtr request, + boost::system::error_code ec, + size_t length); + + /// @brief Callback invoked when TLS shutdown is performed. + /// + /// The TLS socket is unconditionally closed but the callback is called + /// only when the peer has answered so the connection should be + /// explicitly closed in all cases, i.e. do not rely on this handler. + /// + /// @param ec Error code (ignored). + void shutdownCallback(const boost::system::error_code& ec); + + /// @brief Reset timer for detecting request timeouts. + /// + /// @param request Pointer to the request to be guarded by the timeout. + void setupRequestTimer(TcpRequestPtr request = TcpRequestPtr()); + + /// @brief Reset timer for detecting idle timeout in persistent connections. + void setupIdleTimer(); + + /// @brief Callback invoked when the TCP Request Timeout occurs. + /// + /// This callback creates TCP response with Request Timeout error code + /// and sends it to the client. + /// + /// @param request Pointer to the request for which timeout occurs. + void requestTimeoutCallback(TcpRequestPtr request); + + void idleTimeoutCallback(); + + /// @brief Shuts down current connection. + /// + /// Copied from the next method @ref stopThisConnection + void shutdownConnection(); + + /// @brief Stops current connection. + void stopThisConnection(); + + /// @brief returns remote address in textual form + std::string getRemoteEndpointAddressAsText() const; + + /// @brief Timer used to detect Request Timeout. + asiolink::IntervalTimer request_timer_; + + /// @brief Configured Request Timeout in milliseconds. + long request_timeout_; + + /// @brief TLS context. + asiolink::TlsContextPtr tls_context_; + + /// @brief Timeout after which the persistent TCP connection is shut + /// down by the server. + long idle_timeout_; + + /// @brief TCP socket used by this connection. + std::unique_ptr > tcp_socket_; + + /// @brief TLS socket used by this connection. + std::unique_ptr > tls_socket_; + + /// @brief Pointer to the TCP acceptor used to accept new connections. + TcpConnectionAcceptorPtr acceptor_; + + /// @brief Connection pool holding this connection. + TcpConnectionPool& connection_pool_; + + /// @brief External TCP acceptor callback. + TcpConnectionAcceptorCallback acceptor_callback_; +}; + +} // end of namespace isc::tcp +} // end of namespace isc + +#endif diff --git a/src/lib/tcp/tcp_connection_acceptor.h b/src/lib/tcp/tcp_connection_acceptor.h new file mode 100644 index 0000000000..26ca543a55 --- /dev/null +++ b/src/lib/tcp/tcp_connection_acceptor.h @@ -0,0 +1,38 @@ +// Copyright (C) 2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef HTTP_ACCEPTOR_H +#define HTTP_ACCEPTOR_H + +#include +#include + +#include +#include +#include + +namespace isc { +namespace tcp { + +/// @brief Type of the callback for the TCP acceptor used in this library. +typedef std::function TcpConnectionAcceptorCallback; + +/// @brief Type of the TCP acceptor used in this library. +typedef asiolink::TCPAcceptor TcpConnectionAcceptor; + +/// @brief Type of shared pointer to TCP acceptors. +typedef boost::shared_ptr TcpConnectionAcceptorPtr; + +/// @brief Type of the TLS acceptor used in this library. +typedef asiolink::TLSAcceptor TlsConnectionAcceptor; + +/// @brief Type of shared pointer to TLS acceptors. +typedef boost::shared_ptr TlsConnectionAcceptorPtr; + +} // end of namespace isc::tcp +} // end of namespace isc + +#endif diff --git a/src/lib/tcp/tcp_connection_pool.cc b/src/lib/tcp/tcp_connection_pool.cc new file mode 100644 index 0000000000..b3776097a0 --- /dev/null +++ b/src/lib/tcp/tcp_connection_pool.cc @@ -0,0 +1,78 @@ +// Copyright (C) 2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#if 0 + +#include + +#include +#include +#include + +namespace isc { +namespace tcp { + +void +TcpConnectionPool::start(const TcpConnectionPtr& connection) { + if (util::MultiThreadingMgr::instance().getMode()) { + std::lock_guard lk(mutex_); + connections_.insert(connections_.end(), connection); + } else { + connections_.insert(connections_.end(), connection); + } + + connection->asyncAccept(); +} + +void +TcpConnectionPool::stop(const TcpConnectionPtr& connection) { + if (util::MultiThreadingMgr::instance().getMode()) { + std::lock_guard lk(mutex_); + connections_.remove(connection); + } else { + connections_.remove(connection); + } + + connection->close(); +} + +void +TcpConnectionPool::shutdown(const TcpConnectionPtr& connection) { + if (util::MultiThreadingMgr::instance().getMode()) { + std::lock_guard lk(mutex_); + connections_.remove(connection); + } else { + connections_.remove(connection); + } + + connection->shutdown(); +} + +void +TcpConnectionPool::stopAll() { + if (util::MultiThreadingMgr::instance().getMode()) { + std::lock_guard lk(mutex_); + stopAllInternal(); + } else { + stopAllInternal(); + } +} + +void +TcpConnectionPool::stopAllInternal() { + for (auto connection = connections_.begin(); + connection != connections_.end(); + ++connection) { + (*connection)->close(); + } + + connections_.clear(); +} + +} +} + +#endif diff --git a/src/lib/tcp/tcp_connection_pool.h b/src/lib/tcp/tcp_connection_pool.h new file mode 100644 index 0000000000..e749f18120 --- /dev/null +++ b/src/lib/tcp/tcp_connection_pool.h @@ -0,0 +1,78 @@ +// Copyright (C) 2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef TCP_CONNECTION_POOL_H +#define TCP_CONNECTION_POOL_H + +#include + +#include +#include + +namespace isc { +namespace tcp { + +/// @brief Pool of active TCP connections. +/// +/// The TCP server is designed to handle many connections simultaneously. +/// The communication between the client and the server may take long time +/// and the server must be able to react on other events while the communication +/// with the clients is in progress. Thus, the server must track active +/// connections and gracefully close them when needed. An obvious case when the +/// connections must be terminated by the server is when the shutdown signal +/// is received. +/// +/// This object is a simple container for the server connections which provides +/// means to terminate them on request. +class TcpConnectionPool { +public: + + /// @brief Start new connection. + /// + /// The connection is inserted to the pool and the + /// @ref TcpConnection::asyncAccept is invoked. + /// + /// @param connection Pointer to the new connection. + void start(const TcpConnectionPtr& connection); + + /// @brief Removes a connection from the pool and shutdown it. + /// + /// Shutdown is specific to TLS and is a first part of graceful close (note it is + /// NOT the same as TCP shutdown system call). + /// + /// @note if the TLS connection stalls e.g. the peer does not try I/O + /// on it the connection has to be explicitly stopped. + /// + /// @param connection Pointer to the connection. + void shutdown(const TcpConnectionPtr& connection); + + /// @brief Removes a connection from the pool and stops it. + /// + /// @param connection Pointer to the connection. + void stop(const TcpConnectionPtr& connection); + + /// @brief Stops all connections and removes them from the pool. + void stopAll(); + +protected: + + /// @brief Stops all connections and removes them from the pool. + /// + /// Must be called from with a thread-safe context. + void stopAllInternal(); + + /// @brief Set of connections. + std::list connections_; + + /// @brief Mutex to protect the internal state. + std::mutex mutex_; +}; + +} +} + +#endif + diff --git a/src/lib/tcp/tcp_listener.cc b/src/lib/tcp/tcp_listener.cc new file mode 100644 index 0000000000..9a0dd753c7 --- /dev/null +++ b/src/lib/tcp/tcp_listener.cc @@ -0,0 +1,110 @@ +// Copyright (C) 2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include + +using namespace isc::asiolink; +namespace ph = std::placeholders; + +namespace isc { +namespace tcp { + +TcpListener::TcpListener(IOService& io_service, + const IOAddress& server_address, + const unsigned short server_port, + const TlsContextPtr& tls_context, + const long idle_timeout) + : io_service_(io_service), tls_context_(tls_context), acceptor_(), + endpoint_(), connections_(), idle_timeout_(idle_timeout) { + + // Create the TCP or TLS acceptor. + // @todo TKM - hmmm.... need to understand this better.. + if (!tls_context) { + acceptor_.reset(new TcpConnectionAcceptor(io_service)); + } else { + acceptor_.reset(new TlsConnectionAcceptor(io_service)); + } + + // Try creating an endpoint. This may cause exceptions. + try { + endpoint_.reset(new TCPEndpoint(server_address, server_port)); + + } catch (...) { + isc_throw(TcpListenerError, "unable to create TCP endpoint for " + << server_address << ":" << server_port); + } + + // Idle persistent connection timeout is signed and must be greater than 0. + if (idle_timeout_ <= 0) { + isc_throw(TcpListenerError, "Invalid desired TCP idle persistent connection" + " timeout " << idle_timeout_); + } +} + +const TCPEndpoint& +TcpListener::getEndpoint() const { + return (*endpoint_); +} + +void +TcpListener::start() { + try { + acceptor_->open(*endpoint_); + acceptor_->setOption(TcpConnectionAcceptor::ReuseAddress(true)); + acceptor_->bind(*endpoint_); + acceptor_->listen(); + + } catch (const boost::system::system_error& ex) { + stop(); + isc_throw(TcpListenerError, "unable to setup TCP acceptor for " + "listening for incoming TCP clients: " << ex.what()); + } + + accept(); +} + +void +TcpListener::stop() { + connections_.stopAll(); + acceptor_->close(); +} + +void +TcpListener::accept() { + TcpConnectionAcceptorCallback acceptor_callback = + std::bind(&TcpListener::acceptHandler, this, ph::_1); + + TcpConnectionPtr conn = createConnection(acceptor_callback); + + // Add this new connection to the pool. + connections_.start(conn); +} + +void +TcpListener::acceptHandler(const boost::system::error_code&) { + // The new connection has arrived. Set the acceptor to continue + // accepting new connections. + accept(); +} + +TcpConnectionPtr +TcpListener::createConnection(const TcpConnectionAcceptorCallback& /* callback */) { +#if 1 + isc_throw(NotImplemented, "TcpListener::createConnection:"); +#else + TcpConnectionPtr +/// @todo TKM - I think what we want is to define TcpConnectionFactory +/// instead of a response creator. Let TcpListener accept a factory +/// for that, which is used here to create for BLQ an LeaseQueryConnection + return (connection_factory_(io_service_, acceptor_, tls_context_, + connections_, callback, idle_timeout_)); +#endif +} + +} // end of namespace isc::tcp +} // end of namespace isc diff --git a/src/lib/tcp/tcp_listener.h b/src/lib/tcp/tcp_listener.h new file mode 100644 index 0000000000..62cfc1ace7 --- /dev/null +++ b/src/lib/tcp/tcp_listener.h @@ -0,0 +1,122 @@ +// Copyright (C) 2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef TCP_LISTENER_H +#define TCP_LISTENER_H + +#include +#include +#include +#include +#include + +namespace isc { +namespace tcp { + +/// @brief A generic error raised by the @ref TcpListener class. +class TcpListenerError : public Exception { +public: + TcpListenerError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief ementation of the @ref TcpListener. +class TcpListener { +public: + + /// @brief Constructor. + /// + /// This constructor creates new server endpoint using the specified IP + /// address and port. It also validates other specified parameters. + /// + /// This constructor does not start accepting new connections! To start + /// accepting connections run @ref TcpListener::start. + /// + /// @param io_service IO service to be used by the listener. + /// @param server_address Address on which the TCP service should run. + /// @param server_port Port number on which the TCP service should run. + /// @param tls_context TLS context. + /// @param idle_timeout Timeout after which an idle persistent TCP + /// connection is closed by the server. + /// + /// @throw TcpListenerError when any of the specified parameters is + /// invalid. + TcpListener(asiolink::IOService& io_service, + const asiolink::IOAddress& server_address, + const unsigned short server_port, + const asiolink::TlsContextPtr& tls_context, + const long idle_timeout); + + /// @brief Virtual destructor. + virtual ~TcpListener() { + } + + /// @brief Returns reference to the current listener endpoint. + const asiolink::TCPEndpoint& getEndpoint() const; + + /// @brief Starts accepting new connections. + /// + /// This method starts accepting and handling new TCP connections on + /// the IP address and port number specified in the constructor. + /// + /// If the method is invoked successfully, it must not be invoked again + /// until @ref TcpListener::stop is called. + /// + /// @throw TcpListenerError if an error occurred. + void start(); + + /// @brief Stops all active connections and shuts down the service. + void stop(); + +protected: + + /// @brief Creates @ref TcpConnection instance and adds it to the + /// pool of active connections. + /// + /// The next accepted connection will be handled by this instance. + void accept(); + + /// @brief Callback invoked when the new connection is accepted. + /// + /// It calls @c TcpListener::accept to create new @c TcpConnection + /// instance. + /// + /// @param ec Error code passed to the handler. This is currently ignored. + void acceptHandler(const boost::system::error_code& ec); + + /// @brief Creates an instance of the @c TcpConnection. + /// + /// This method is virtual so as it can be overridden when customized + /// connections are to be used, e.g. in case of unit testing. + /// + /// @return Pointer to the created connection. + virtual TcpConnectionPtr createConnection(const TcpConnectionAcceptorCallback& callback); + + /// @brief Reference to the IO service. + asiolink::IOService& io_service_; + + /// @brief TLS context. + asiolink::TlsContextPtr tls_context_; + + /// @brief Acceptor instance. + TcpConnectionAcceptorPtr acceptor_; + + /// @brief Pointer to the endpoint representing IP address and port on + /// which the service is running. + boost::scoped_ptr endpoint_; + + /// @brief Pool of active connections. + TcpConnectionPool connections_; + + /// @brief Timeout after which idle persistent connection is closed by + /// the server. + long idle_timeout_; +}; + +} // end of namespace isc::asiolink +} // end of namespace isc + +#endif // TCP_LISTENER_H diff --git a/src/lib/tcp/tcp_log.cc b/src/lib/tcp/tcp_log.cc new file mode 100644 index 0000000000..959ae0ccda --- /dev/null +++ b/src/lib/tcp/tcp_log.cc @@ -0,0 +1,21 @@ +// Copyright (C) 2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at tcp://mozilla.org/MPL/2.0/. + +/// Defines the logger used by the libkea-tcp library. + +#include + +#include + +namespace isc { +namespace tcp { + +/// @brief Defines the logger used within libkea-tcp library. +isc::log::Logger tcp_logger("tcp"); + +} // namespace tcp +} // namespace isc + diff --git a/src/lib/tcp/tcp_log.h b/src/lib/tcp/tcp_log.h new file mode 100644 index 0000000000..ec3dce3a33 --- /dev/null +++ b/src/lib/tcp/tcp_log.h @@ -0,0 +1,23 @@ +// Copyright (C) 2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at tcp://mozilla.org/MPL/2.0/. + +#ifndef TCP_LOG_H +#define TCP_LOG_H + +#include +#include +#include + +namespace isc { +namespace tcp { + +/// Define the logger used within libkea-tcp library. +extern isc::log::Logger tcp_logger; + +} // namespace tcp +} // namespace isc + +#endif // TCP_LOG_H diff --git a/src/lib/tcp/tcp_messages.cc b/src/lib/tcp/tcp_messages.cc new file mode 100644 index 0000000000..b8fcbe2bf5 --- /dev/null +++ b/src/lib/tcp/tcp_messages.cc @@ -0,0 +1,55 @@ +// File created from ../../../src/lib/tcp/tcp_messages.mes + +#include +#include +#include + +namespace isc { +namespace asiolink { + +extern const isc::log::MessageID TCP_BAD_CLIENT_REQUEST_RECEIVED = "TCP_BAD_CLIENT_REQUEST_RECEIVED"; +extern const isc::log::MessageID TCP_CLIENT_REQUEST_RECEIVED = "TCP_CLIENT_REQUEST_RECEIVED"; +extern const isc::log::MessageID TCP_CLIENT_REQUEST_RECEIVED_DETAILS = "TCP_CLIENT_REQUEST_RECEIVED_DETAILS"; +extern const isc::log::MessageID TCP_CONNECTION_CLOSE_CALLBACK_FAILED = "TCP_CONNECTION_CLOSE_CALLBACK_FAILED"; +extern const isc::log::MessageID TCP_CONNECTION_HANDSHAKE_FAILED = "TCP_CONNECTION_HANDSHAKE_FAILED"; +extern const isc::log::MessageID TCP_CONNECTION_HANDSHAKE_START = "TCP_CONNECTION_HANDSHAKE_START"; +extern const isc::log::MessageID TCP_CONNECTION_SHUTDOWN = "TCP_CONNECTION_SHUTDOWN"; +extern const isc::log::MessageID TCP_CONNECTION_SHUTDOWN_FAILED = "TCP_CONNECTION_SHUTDOWN_FAILED"; +extern const isc::log::MessageID TCP_CONNECTION_STOP = "TCP_CONNECTION_STOP"; +extern const isc::log::MessageID TCP_CONNECTION_STOP_FAILED = "TCP_CONNECTION_STOP_FAILED"; +extern const isc::log::MessageID TCP_DATA_RECEIVED = "TCP_DATA_RECEIVED"; +extern const isc::log::MessageID TCP_IDLE_CONNECTION_TIMEOUT_OCCURRED = "TCP_IDLE_CONNECTION_TIMEOUT_OCCURRED"; +extern const isc::log::MessageID TCP_PREMATURE_CONNECTION_TIMEOUT_OCCURRED = "TCP_PREMATURE_CONNECTION_TIMEOUT_OCCURRED"; +extern const isc::log::MessageID TCP_REQUEST_RECEIVE_START = "TCP_REQUEST_RECEIVE_START"; +extern const isc::log::MessageID TCP_SERVER_RESPONSE_SEND = "TCP_SERVER_RESPONSE_SEND"; +extern const isc::log::MessageID TCP_SERVER_RESPONSE_SEND_DETAILS = "TCP_SERVER_RESPONSE_SEND_DETAILS"; + +} // namespace asiolink +} // namespace isc + +namespace { + +const char* values[] = { + "TCP_BAD_CLIENT_REQUEST_RECEIVED", "bad request received from %1: %2", + "TCP_CLIENT_REQUEST_RECEIVED", "received TCP request from %1", + "TCP_CLIENT_REQUEST_RECEIVED_DETAILS", "detailed information about well-formed request received from %1:\n%2", + "TCP_CONNECTION_CLOSE_CALLBACK_FAILED", "Connection close callback threw an exception", + "TCP_CONNECTION_HANDSHAKE_FAILED", "TLS handshake with %1 failed with %2", + "TCP_CONNECTION_HANDSHAKE_START", "start TLS handshake with %1 with timeout %2", + "TCP_CONNECTION_SHUTDOWN", "shutting down TCP connection from %1", + "TCP_CONNECTION_SHUTDOWN_FAILED", "shutting down TCP connection failed", + "TCP_CONNECTION_STOP", "stopping TCP connection from %1", + "TCP_CONNECTION_STOP_FAILED", "stopping TCP connection failed", + "TCP_DATA_RECEIVED", "received %1 bytes from %2", + "TCP_IDLE_CONNECTION_TIMEOUT_OCCURRED", "closing persistent connection with %1 as a result of a timeout", + "TCP_PREMATURE_CONNECTION_TIMEOUT_OCCURRED", "premature connection timeout occurred: in transaction ? %1, transid: %2, current_transid: %3", + "TCP_REQUEST_RECEIVE_START", "start receiving request from %1 with timeout %2", + "TCP_SERVER_RESPONSE_SEND", "sending TCP response %1 to %2", + "TCP_SERVER_RESPONSE_SEND_DETAILS", "detailed information about response sent to %1:\n%2", + NULL +}; + +const isc::log::MessageInitializer initializer(values); + +} // Anonymous namespace + diff --git a/src/lib/tcp/tcp_messages.h b/src/lib/tcp/tcp_messages.h new file mode 100644 index 0000000000..946b2fcfd0 --- /dev/null +++ b/src/lib/tcp/tcp_messages.h @@ -0,0 +1,31 @@ +// File created from ../../../src/lib/tcp/tcp_messages.mes + +#ifndef TCP_MESSAGES_H +#define TCP_MESSAGES_H + +#include + +namespace isc { +namespace asiolink { + +extern const isc::log::MessageID TCP_BAD_CLIENT_REQUEST_RECEIVED; +extern const isc::log::MessageID TCP_CLIENT_REQUEST_RECEIVED; +extern const isc::log::MessageID TCP_CLIENT_REQUEST_RECEIVED_DETAILS; +extern const isc::log::MessageID TCP_CONNECTION_CLOSE_CALLBACK_FAILED; +extern const isc::log::MessageID TCP_CONNECTION_HANDSHAKE_FAILED; +extern const isc::log::MessageID TCP_CONNECTION_HANDSHAKE_START; +extern const isc::log::MessageID TCP_CONNECTION_SHUTDOWN; +extern const isc::log::MessageID TCP_CONNECTION_SHUTDOWN_FAILED; +extern const isc::log::MessageID TCP_CONNECTION_STOP; +extern const isc::log::MessageID TCP_CONNECTION_STOP_FAILED; +extern const isc::log::MessageID TCP_DATA_RECEIVED; +extern const isc::log::MessageID TCP_IDLE_CONNECTION_TIMEOUT_OCCURRED; +extern const isc::log::MessageID TCP_PREMATURE_CONNECTION_TIMEOUT_OCCURRED; +extern const isc::log::MessageID TCP_REQUEST_RECEIVE_START; +extern const isc::log::MessageID TCP_SERVER_RESPONSE_SEND; +extern const isc::log::MessageID TCP_SERVER_RESPONSE_SEND_DETAILS; + +} // namespace asiolink +} // namespace isc + +#endif // TCP_MESSAGES_H diff --git a/src/lib/tcp/tcp_messages.mes b/src/lib/tcp/tcp_messages.mes new file mode 100644 index 0000000000..6184dc8a52 --- /dev/null +++ b/src/lib/tcp/tcp_messages.mes @@ -0,0 +1,96 @@ +# Copyright (C) 2022 Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +$NAMESPACE isc::asiolink + +% TCP_BAD_CLIENT_REQUEST_RECEIVED bad request received from %1: %2 +This debug message is issued when an TCP client sends malformed request to +the server. This includes TCP requests using unexpected content types, +including malformed JSON etc. The first argument specifies an address of +the remote endpoint which sent the request. The second argument provides +a detailed error message. + +% TCP_CLIENT_REQUEST_RECEIVED received TCP request from %1 +This debug message is issued when the server finished receiving a TCP +request from the remote endpoint. The address of the remote endpoint is +specified as an argument. + +% TCP_CLIENT_REQUEST_RECEIVED_DETAILS detailed information about well-formed request received from %1:\n%2 +This debug message is issued when the TCP server receives a well-formed +request. It includes detailed information about the received request. The +first argument specifies an address of the remote endpoint which sent the +request. The second argument provides the request in the textual format. +The request is truncated by the logger if it is too large to be printed. + +% TCP_CONNECTION_CLOSE_CALLBACK_FAILED Connection close callback threw an exception +This is an error message emitted when the close connection callback +registered on the connection failed unexpectedly. This is a programmatic +error that should be submitted as a bug. + +% TCP_CONNECTION_HANDSHAKE_FAILED TLS handshake with %1 failed with %2 +This information message is issued when the TLS handshake failed at the +server side. The client address and the error message are displayed. + +% TCP_CONNECTION_HANDSHAKE_START start TLS handshake with %1 with timeout %2 +This debug message is issued when the server starts the TLS handshake +with the remote endpoint. The first argument specifies the address +of the remote endpoint. The second argument specifies request timeout in +seconds. + +% TCP_CONNECTION_SHUTDOWN shutting down TCP connection from %1 +This debug message is issued when one of the TCP connections is shut down. +The connection can be stopped as a result of an error or after the +successful message exchange with a client. + +% TCP_CONNECTION_SHUTDOWN_FAILED shutting down TCP connection failed +This error message is issued when an error occurred during shutting down +a TCP connection with a client. + +% TCP_CONNECTION_STOP stopping TCP connection from %1 +This debug message is issued when one of the TCP connections is stopped. +The connection can be stopped as a result of an error or after the +successful message exchange with a client. + +% TCP_CONNECTION_STOP_FAILED stopping TCP connection failed +This error message is issued when an error occurred during closing a +TCP connection with a client. + +% TCP_DATA_RECEIVED received %1 bytes from %2 +This debug message is issued when the server receives a chunk of data from +the remote endpoint. This may include the whole request or only a part +of the request. The first argument specifies the amount of received data. +The second argument specifies an address of the remote endpoint which +produced the data. + +% TCP_IDLE_CONNECTION_TIMEOUT_OCCURRED closing persistent connection with %1 as a result of a timeout +This debug message is issued when the persistent TCP connection is being +closed as a result of being idle. + +% TCP_PREMATURE_CONNECTION_TIMEOUT_OCCURRED premature connection timeout occurred: in transaction ? %1, transid: %2, current_transid: %3 +This warning message is issued when unexpected timeout occurred during the +transaction. This is proven to occur when the system clock is moved manually +or as a result of synchronization with a time server. Any ongoing transactions +will be interrupted. New transactions should be conducted normally. + +% TCP_REQUEST_RECEIVE_START start receiving request from %1 with timeout %2 +This debug message is issued when the server starts receiving new request +over the established connection. The first argument specifies the address +of the remote endpoint. The second argument specifies request timeout in +seconds. + +% TCP_SERVER_RESPONSE_SEND sending TCP response %1 to %2 +This debug message is issued when the server is starting to send a TCP +response to a remote endpoint. The first argument holds basic information +about the response (TCP version number and status code). The second +argument specifies an address of the remote endpoint. + +% TCP_SERVER_RESPONSE_SEND_DETAILS detailed information about response sent to %1:\n%2 +This debug message is issued right before the server sends a TCP response +to the client. It includes detailed information about the response. The +first argument specifies an address of the remote endpoint to which the +response is being sent. The second argument provides a response in the +textual form. The response is truncated by the logger if it is too large +to be printed. diff --git a/src/lib/tcp/tests/.gitignore b/src/lib/tcp/tests/.gitignore new file mode 100644 index 0000000000..4f7a6bfe0d --- /dev/null +++ b/src/lib/tcp/tests/.gitignore @@ -0,0 +1 @@ +run_unittests diff --git a/src/lib/tcp/tests/Makefile.am b/src/lib/tcp/tests/Makefile.am new file mode 100644 index 0000000000..140d6ee296 --- /dev/null +++ b/src/lib/tcp/tests/Makefile.am @@ -0,0 +1,58 @@ +AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib +AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) +TEST_CA_DIR = $(abs_srcdir)/../testutils/ca +AM_CPPFLAGS += -DTEST_CA_DIR=\"$(TEST_CA_DIR)\" + +AM_CXXFLAGS = $(KEA_CXXFLAGS) + +if USE_STATIC_LINK +AM_LDFLAGS = -static +endif + +CLEANFILES = *.gcno *.gcda test-socket + +DISTCLEANFILES = + +noinst_SCRIPTS = + +TESTS_ENVIRONMENT = $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND) + +TESTS = +if HAVE_GTEST +TESTS += run_unittests +run_unittests_SOURCES = run_unittests.cc +#run_unittests_SOURCES += tcp_listner_unittest.cc + +#if HAVE_OPENSSL +#run_unittests_SOURCES += tls_unittest.cc +#run_unittests_SOURCES += tls_acceptor_unittest.cc +#run_unittests_SOURCES += tls_socket_unittest.cc +#endif +#if HAVE_BOTAN_BOOST +#run_unittests_SOURCES += tls_unittest.cc +#run_unittests_SOURCES += tls_acceptor_unittest.cc +#run_unittests_SOURCES += tls_socket_unittest.cc +#endif + +run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) + +run_unittests_LDADD = $(top_builddir)/src/lib/tcp/libkea-tcp.la +run_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la +run_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la +run_unittests_LDADD += $(top_builddir)/src/lib/util/unittests/libutil_unittests.la +run_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la +run_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la +run_unittests_LDADD += $(LOG4CPLUS_LIBS) $(BOOST_LIBS) $(CRYPTO_LIBS) +run_unittests_LDADD += $(GTEST_LDADD) + +run_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS) + +# Note: the ordering matters: -Wno-... must follow -Wextra (defined in +# KEA_CXXFLAGS) +run_unittests_CXXFLAGS = $(AM_CXXFLAGS) +if USE_GXX +run_unittests_CXXFLAGS += -Wno-unused-parameter -Wno-unused-private-field +endif +endif + +noinst_PROGRAMS = $(TESTS) diff --git a/src/lib/tcp/tests/run_unittests.cc b/src/lib/tcp/tests/run_unittests.cc new file mode 100644 index 0000000000..70b03766b1 --- /dev/null +++ b/src/lib/tcp/tests/run_unittests.cc @@ -0,0 +1,19 @@ +// Copyright (C) 2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include +#include +#include + +int +main(int argc, char* argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); // Initialize Google test + isc::log::LoggerManager::init("unittest"); // Set a root logger name + return (isc::util::unittests::run_all()); +}