diff options
Diffstat (limited to '')
-rw-r--r-- | src/lib/http/connection.cc | 600 |
1 files changed, 600 insertions, 0 deletions
diff --git a/src/lib/http/connection.cc b/src/lib/http/connection.cc new file mode 100644 index 0000000..b1e57bd --- /dev/null +++ b/src/lib/http/connection.cc @@ -0,0 +1,600 @@ +// Copyright (C) 2017-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 <http/connection.h> +#include <http/connection_pool.h> +#include <http/http_log.h> +#include <http/http_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 { + +HttpConnection::Transaction::Transaction(const HttpResponseCreatorPtr& response_creator, + const HttpRequestPtr& request) + : request_(request ? request : response_creator->createNewHttpRequest()), + parser_(new HttpRequestParser(*request_)), + input_buf_(), + output_buf_() { + parser_->initModel(); +} + +HttpConnection::TransactionPtr +HttpConnection::Transaction::create(const HttpResponseCreatorPtr& response_creator) { + return (boost::make_shared<Transaction>(response_creator)); +} + +HttpConnection::TransactionPtr +HttpConnection::Transaction::spawn(const HttpResponseCreatorPtr& response_creator, + const TransactionPtr& transaction) { + if (transaction) { + return (boost::make_shared<Transaction>(response_creator, + transaction->getRequest())); + } + return (create(response_creator)); +} + +void +HttpConnection:: +SocketCallback::operator()(boost::system::error_code ec, size_t length) { + if (ec.value() == boost::asio::error::operation_aborted) { + return; + } + callback_(ec, length); +} + +HttpConnection::HttpConnection(asiolink::IOService& io_service, + const HttpAcceptorPtr& acceptor, + const TlsContextPtr& tls_context, + HttpConnectionPool& connection_pool, + const HttpResponseCreatorPtr& response_creator, + const HttpAcceptorCallback& 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)); + } +} + +HttpConnection::~HttpConnection() { + close(); +} + +void +HttpConnection::recordParameters(const HttpRequestPtr& 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 (HttpRequest::recordSubject_) { + request->setSubject(tls_socket_->getTlsStream().getSubject()); + } + + // Record the first commonName of the issuerName of the client + // certificate when wanted. + if (HttpRequest::recordIssuer_) { + request->setIssuer(tls_socket_->getTlsStream().getIssuer()); + } +} + +void +HttpConnection::shutdownCallback(const boost::system::error_code&) { + tls_socket_->close(); +} + +void +HttpConnection::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(&HttpConnection::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 +HttpConnection::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 +HttpConnection::shutdownConnection() { + try { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC, + HTTP_CONNECTION_SHUTDOWN) + .arg(getRemoteEndpointAddressAsText()); + connection_pool_.shutdown(shared_from_this()); + } catch (...) { + LOG_ERROR(http_logger, HTTP_CONNECTION_SHUTDOWN_FAILED); + } +} + +void +HttpConnection::stopThisConnection() { + try { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC, + HTTP_CONNECTION_STOP) + .arg(getRemoteEndpointAddressAsText()); + connection_pool_.stop(shared_from_this()); + } catch (...) { + LOG_ERROR(http_logger, HTTP_CONNECTION_STOP_FAILED); + } +} + +void +HttpConnection::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. + HttpAcceptorCallback cb = std::bind(&HttpConnection::acceptorCallback, + shared_from_this(), + ph::_1); // error + try { + HttpsAcceptorPtr tls_acceptor = + boost::dynamic_pointer_cast<HttpsAcceptor>(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(HttpConnectionError, "unable to start accepting TCP " + "connections: " << ex.what()); + } +} + +void +HttpConnection::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(&HttpConnection::handshakeCallback, + shared_from_this(), + ph::_1)); // error + try { + tls_socket_->handshake(cb); + + } catch (const std::exception& ex) { + isc_throw(HttpConnectionError, "unable to perform TLS handshake: " + << ex.what()); + } +} + +void +HttpConnection::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(&HttpConnection::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 +HttpConnection::doWrite(HttpConnection::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(&HttpConnection::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 +HttpConnection::asyncSendResponse(const ConstHttpResponsePtr& response, + TransactionPtr transaction) { + transaction->setOutputBuf(response->toString()); + doWrite(transaction); +} + + +void +HttpConnection::acceptorCallback(const boost::system::error_code& ec) { + if (!acceptor_->isOpen()) { + return; + } + + if (ec) { + stopThisConnection(); + } + + acceptor_callback_(ec); + + if (!ec) { + if (!tls_context_) { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL, + HTTP_REQUEST_RECEIVE_START) + .arg(getRemoteEndpointAddressAsText()) + .arg(static_cast<unsigned>(request_timeout_/1000)); + } else { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL, + HTTP_CONNECTION_HANDSHAKE_START) + .arg(getRemoteEndpointAddressAsText()) + .arg(static_cast<unsigned>(request_timeout_/1000)); + } + + setupRequestTimer(); + doHandshake(); + } +} + +void +HttpConnection::handshakeCallback(const boost::system::error_code& ec) { + if (ec) { + LOG_INFO(http_logger, HTTP_CONNECTION_HANDSHAKE_FAILED) + .arg(getRemoteEndpointAddressAsText()) + .arg(ec.message()); + stopThisConnection(); + } else { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL, + HTTPS_REQUEST_RECEIVE_START) + .arg(getRemoteEndpointAddressAsText()); + + doRead(); + } +} + +void +HttpConnection::socketReadCallback(HttpConnection::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(http_logger, isc::log::DBGLVL_TRACE_DETAIL_DATA, + HTTP_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(http_logger, isc::log::DBGLVL_TRACE_BASIC, + HTTP_CLIENT_REQUEST_RECEIVED) + .arg(getRemoteEndpointAddressAsText()); + + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC_DATA, + HTTP_CLIENT_REQUEST_RECEIVED_DETAILS) + .arg(getRemoteEndpointAddressAsText()) + .arg(transaction->getParser()->getBufferAsString(MAX_LOGGED_MESSAGE_SIZE)); + + } catch (const std::exception& ex) { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC, + HTTP_BAD_CLIENT_REQUEST_RECEIVED) + .arg(getRemoteEndpointAddressAsText()) + .arg(ex.what()); + + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC_DATA, + HTTP_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. + HttpResponsePtr response = response_creator_->createHttpResponse(transaction->getRequest()); + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC, + HTTP_SERVER_RESPONSE_SEND) + .arg(response->toBriefString()) + .arg(getRemoteEndpointAddressAsText()); + + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC_DATA, + HTTP_SERVER_RESPONSE_SEND_DETAILS) + .arg(getRemoteEndpointAddressAsText()) + .arg(HttpMessageParserBase::logFormatHttpMessage(response->toString(), + MAX_LOGGED_MESSAGE_SIZE)); + + // Response created. Activate the timer again. + setupRequestTimer(transaction); + + // Start sending the response. + asyncSendResponse(response, transaction); + } +} + +void +HttpConnection::socketWriteCallback(HttpConnection::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 +HttpConnection::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(&HttpConnection::requestTimeoutCallback, + this, transaction), + request_timeout_, IntervalTimer::ONE_SHOT); +} + +void +HttpConnection::setupIdleTimer() { + request_timer_.setup(std::bind(&HttpConnection::idleTimeoutCallback, + this), + idle_timeout_, IntervalTimer::ONE_SHOT); +} + +void +HttpConnection::requestTimeoutCallback(TransactionPtr transaction) { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL, + HTTP_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 HTTP 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 HttpRequest(HttpRequest::Method::HTTP_POST, "/", + HttpVersion::HTTP_10(), + HostHttpHeader("dummy"))); + request->finalize(); + } + + // Create the timeout response. + HttpResponsePtr response = + response_creator_->createStockHttpResponse(request, + HttpStatusCode::REQUEST_TIMEOUT); + + // Send the HTTP 408 status. + asyncSendResponse(response, spawned_transaction); +} + +void +HttpConnection::idleTimeoutCallback() { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL, + HTTP_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 +HttpConnection::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 |