]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#2583] Initial addition of src/lib/tcp
authorThomas Markwalder <tmark@isc.org>
Tue, 4 Oct 2022 10:51:58 +0000 (06:51 -0400)
committerThomas Markwalder <tmark@isc.org>
Thu, 10 Nov 2022 19:43:23 +0000 (14:43 -0500)
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

19 files changed:
configure.ac
src/lib/Makefile.am
src/lib/tcp/Makefile.am [new file with mode: 0644]
src/lib/tcp/README [new file with mode: 0644]
src/lib/tcp/tcp_connection.cc [new file with mode: 0644]
src/lib/tcp/tcp_connection.h [new file with mode: 0644]
src/lib/tcp/tcp_connection_acceptor.h [new file with mode: 0644]
src/lib/tcp/tcp_connection_pool.cc [new file with mode: 0644]
src/lib/tcp/tcp_connection_pool.h [new file with mode: 0644]
src/lib/tcp/tcp_listener.cc [new file with mode: 0644]
src/lib/tcp/tcp_listener.h [new file with mode: 0644]
src/lib/tcp/tcp_log.cc [new file with mode: 0644]
src/lib/tcp/tcp_log.h [new file with mode: 0644]
src/lib/tcp/tcp_messages.cc [new file with mode: 0644]
src/lib/tcp/tcp_messages.h [new file with mode: 0644]
src/lib/tcp/tcp_messages.mes [new file with mode: 0644]
src/lib/tcp/tests/.gitignore [new file with mode: 0644]
src/lib/tcp/tests/Makefile.am [new file with mode: 0644]
src/lib/tcp/tests/run_unittests.cc [new file with mode: 0644]

index d54a933ab38b8bff8f0171db7b1ed3e1f2666396..3c330c96713288abdd55f113668fd871b95402ec 100644 (file)
@@ -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])
index 26944d166dbd1f2d1e2edfe66143c56c8685e1c5..d7abdbce1d4e26bc3eeafb811cf334be7ceaa73a 100644 (file)
@@ -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 (file)
index 0000000..c5ac22c
--- /dev/null
@@ -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 (file)
index 0000000..68ef83f
--- /dev/null
@@ -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 (file)
index 0000000..cfecd9d
--- /dev/null
@@ -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 <config.h>
+
+#include <asiolink/asio_wrapper.h>
+#include <tcp/tcp_connection.h>
+#include <tcp/tcp_connection_pool.h>
+#include <tcp/tcp_log.h>
+#include <tcp/tcp_messages.h>
+#include <boost/make_shared.hpp>
+#include <functional>
+
+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<SocketCallback>(io_service));
+    } else {
+        tls_socket_.reset(new asiolink::TLSSocket<SocketCallback>(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<TcpsAcceptor>(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<void*>(transaction->getInputBufData()),
+                                      transaction->getInputBufSize(),
+                                      0, &endpoint, cb);
+            return;
+        }
+        if (tls_socket_) {
+            tls_socket_->asyncReceive(static_cast<void*>(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<unsigned>(request_timeout_/1000));
+        } else {
+            LOG_DEBUG(asiolink_logger, isc::log::DBGLVL_TRACE_DETAIL,
+                      TCP_CONNECTION_HANDSHAKE_START)
+                .arg(getRemoteEndpointAddressAsText())
+                .arg(static_cast<unsigned>(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<void*>(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 (file)
index 0000000..70c1a6d
--- /dev/null
@@ -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 <asiolink/interval_timer.h>
+#include <asiolink/io_service.h>
+#include <tcp/tcp_connection_acceptor.h>
+
+#include <boost/enable_shared_from_this.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/shared_ptr.hpp>
+
+#include <array>
+#include <functional>
+#include <string>
+
+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<TcpRequest> TcpRequestPtr;
+
+typedef util::OutputBuffer TcpResponse;
+typedef boost::shared_ptr<TcpResponse> 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<TcpConnection> TcpConnectionPtr;
+
+/// @brief Accepts and handles a single TCP connection.
+class TcpConnection : public boost::enable_shared_from_this<TcpConnection> {
+private:
+
+    /// @brief Type of the function implementing a callback invoked by the
+    /// @c SocketCallback functor.
+    typedef std::function<void(boost::system::error_code ec, size_t length)>
+    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<asiolink::TCPSocket<SocketCallback> > tcp_socket_;
+
+    /// @brief TLS socket used by this connection.
+    std::unique_ptr<asiolink::TLSSocket<SocketCallback> > 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 (file)
index 0000000..26ca543
--- /dev/null
@@ -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 <asiolink/tcp_acceptor.h>
+#include <asiolink/tls_acceptor.h>
+
+#include <boost/shared_ptr.hpp>
+#include <boost/system/system_error.hpp>
+#include <functional>
+
+namespace isc {
+namespace tcp {
+
+/// @brief Type of the callback for the TCP acceptor used in this library.
+typedef std::function<void(const boost::system::error_code&)> TcpConnectionAcceptorCallback;
+
+/// @brief Type of the TCP acceptor used in this library.
+typedef asiolink::TCPAcceptor<TcpConnectionAcceptorCallback> TcpConnectionAcceptor;
+
+/// @brief Type of shared pointer to TCP acceptors.
+typedef boost::shared_ptr<TcpConnectionAcceptor> TcpConnectionAcceptorPtr;
+
+/// @brief Type of the TLS acceptor used in this library.
+typedef asiolink::TLSAcceptor<TcpConnectionAcceptorCallback> TlsConnectionAcceptor;
+
+/// @brief Type of shared pointer to TLS acceptors.
+typedef boost::shared_ptr<TlsConnectionAcceptor> 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 (file)
index 0000000..b377609
--- /dev/null
@@ -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 <config.h>
+
+#include <asiolink/asio_wrapper.h>
+#include <tcp/tcp_connection_pool.h>
+#include <util/multi_threading_mgr.h>
+
+namespace isc {
+namespace tcp {
+
+void
+TcpConnectionPool::start(const TcpConnectionPtr& connection) {
+    if (util::MultiThreadingMgr::instance().getMode()) {
+        std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> lk(mutex_);
+        connections_.remove(connection);
+    } else {
+        connections_.remove(connection);
+    }
+
+    connection->shutdown();
+}
+
+void
+TcpConnectionPool::stopAll() {
+    if (util::MultiThreadingMgr::instance().getMode()) {
+        std::lock_guard<std::mutex> 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 (file)
index 0000000..e749f18
--- /dev/null
@@ -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 <tcp/tcp_connection.h>
+
+#include <list>
+#include <mutex>
+
+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<TcpConnectionPtr> 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 (file)
index 0000000..9a0dd75
--- /dev/null
@@ -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 <config.h>
+#include <asiolink/asio_wrapper.h>
+#include <tcp/tcp_listener.h>
+
+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 (file)
index 0000000..62cfc1a
--- /dev/null
@@ -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 <asiolink/io_service.h>
+#include <asiolink/io_address.h>
+#include <asiolink/tcp_endpoint.h>
+#include <tcp/tcp_connection_pool.h>
+#include <boost/scoped_ptr.hpp>
+
+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<asiolink::TCPEndpoint> 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 (file)
index 0000000..959ae0c
--- /dev/null
@@ -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 <config.h>
+
+#include <tcp/tcp_log.h>
+
+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 (file)
index 0000000..ec3dce3
--- /dev/null
@@ -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 <log/logger_support.h>
+#include <log/macros.h>
+#include <tcp/tcp_messages.h>
+
+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 (file)
index 0000000..b8fcbe2
--- /dev/null
@@ -0,0 +1,55 @@
+// File created from ../../../src/lib/tcp/tcp_messages.mes
+
+#include <cstddef>
+#include <log/message_types.h>
+#include <log/message_initializer.h>
+
+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 (file)
index 0000000..946b2fc
--- /dev/null
@@ -0,0 +1,31 @@
+// File created from ../../../src/lib/tcp/tcp_messages.mes
+
+#ifndef TCP_MESSAGES_H
+#define TCP_MESSAGES_H
+
+#include <log/message_types.h>
+
+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 (file)
index 0000000..6184dc8
--- /dev/null
@@ -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 (file)
index 0000000..4f7a6bf
--- /dev/null
@@ -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 (file)
index 0000000..140d6ee
--- /dev/null
@@ -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 (file)
index 0000000..70b0376
--- /dev/null
@@ -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 <config.h>
+
+#include <gtest/gtest.h>
+#include <util/unittests/run_all.h>
+#include <log/logger_manager.h>
+
+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());
+}