fix include names
cppcheck isn't smart enough to recognize these are c++ headers, not c
headers. Considering we're already inconsistent about our naming, it's
easier to just be consistent, and move the last few files to use .hpp
instead of .h.
Tested:
Code builds, no changes.
Signed-off-by: Ed Tanous <ed@tanous.net>
Change-Id: Ic348d695f8527fa4a0ded53f433e1558c319db40
diff --git a/http/http_connection.hpp b/http/http_connection.hpp
new file mode 100644
index 0000000..302d19b
--- /dev/null
+++ b/http/http_connection.hpp
@@ -0,0 +1,764 @@
+#pragma once
+#include "config.h"
+
+#include "authorization.hpp"
+#include "http_response.hpp"
+#include "http_utility.hpp"
+#include "logging.hpp"
+#include "timer_queue.hpp"
+#include "utility.hpp"
+
+#include <boost/algorithm/string.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/ssl/stream.hpp>
+#include <boost/beast/core/flat_static_buffer.hpp>
+#include <boost/beast/ssl/ssl_stream.hpp>
+#include <boost/beast/websocket.hpp>
+#include <json_html_serializer.hpp>
+#include <security_headers.hpp>
+#include <ssl_key_handler.hpp>
+
+#include <atomic>
+#include <chrono>
+#include <vector>
+
+namespace crow
+{
+
+inline void prettyPrintJson(crow::Response& res)
+{
+ json_html_util::dumpHtml(res.body(), res.jsonValue);
+
+ res.addHeader("Content-Type", "text/html;charset=UTF-8");
+}
+
+#ifdef BMCWEB_ENABLE_DEBUG
+static std::atomic<int> connectionCount;
+#endif
+
+// request body limit size set by the BMCWEB_HTTP_REQ_BODY_LIMIT_MB option
+constexpr unsigned int httpReqBodyLimit =
+ 1024 * 1024 * BMCWEB_HTTP_REQ_BODY_LIMIT_MB;
+
+constexpr uint64_t loggedOutPostBodyLimit = 4096;
+
+constexpr uint32_t httpHeaderLimit = 8192;
+
+// drop all connections after 1 minute, this time limit was chosen
+// arbitrarily and can be adjusted later if needed
+static constexpr const size_t loggedInAttempts =
+ (60 / timerQueueTimeoutSeconds);
+
+static constexpr const size_t loggedOutAttempts =
+ (15 / timerQueueTimeoutSeconds);
+
+template <typename Adaptor, typename Handler>
+class Connection :
+ public std::enable_shared_from_this<Connection<Adaptor, Handler>>
+{
+ public:
+ Connection(Handler* handlerIn,
+ std::function<std::string()>& get_cached_date_str_f,
+ detail::TimerQueue& timerQueueIn, Adaptor adaptorIn) :
+ adaptor(std::move(adaptorIn)),
+ handler(handlerIn), getCachedDateStr(get_cached_date_str_f),
+ timerQueue(timerQueueIn)
+ {
+ parser.emplace(std::piecewise_construct, std::make_tuple());
+ parser->body_limit(httpReqBodyLimit);
+ parser->header_limit(httpHeaderLimit);
+ req.emplace(parser->get());
+
+#ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
+ auto caAvailable = !std::filesystem::is_empty(
+ std::filesystem::path(ensuressl::trustStorePath));
+ if (caAvailable && persistent_data::SessionStore::getInstance()
+ .getAuthMethodsConfig()
+ .tls)
+ {
+ adaptor.set_verify_mode(boost::asio::ssl::verify_peer);
+ std::string id = "bmcweb";
+ int ret = SSL_set_session_id_context(
+ adaptor.native_handle(),
+ reinterpret_cast<const unsigned char*>(id.c_str()),
+ static_cast<unsigned int>(id.length()));
+ if (ret == 0)
+ {
+ BMCWEB_LOG_ERROR << this << " failed to set SSL id";
+ }
+ }
+
+ adaptor.set_verify_callback([this](
+ bool preverified,
+ boost::asio::ssl::verify_context& ctx) {
+ // do nothing if TLS is disabled
+ if (!persistent_data::SessionStore::getInstance()
+ .getAuthMethodsConfig()
+ .tls)
+ {
+ BMCWEB_LOG_DEBUG << this << " TLS auth_config is disabled";
+ return true;
+ }
+
+ // We always return true to allow full auth flow
+ if (!preverified)
+ {
+ BMCWEB_LOG_DEBUG << this << " TLS preverification failed.";
+ return true;
+ }
+
+ X509_STORE_CTX* cts = ctx.native_handle();
+ if (cts == nullptr)
+ {
+ BMCWEB_LOG_DEBUG << this << " Cannot get native TLS handle.";
+ return true;
+ }
+
+ // Get certificate
+ X509* peerCert =
+ X509_STORE_CTX_get_current_cert(ctx.native_handle());
+ if (peerCert == nullptr)
+ {
+ BMCWEB_LOG_DEBUG << this
+ << " Cannot get current TLS certificate.";
+ return true;
+ }
+
+ // Check if certificate is OK
+ int error = X509_STORE_CTX_get_error(cts);
+ if (error != X509_V_OK)
+ {
+ BMCWEB_LOG_INFO << this << " Last TLS error is: " << error;
+ return true;
+ }
+ // Check that we have reached final certificate in chain
+ int32_t depth = X509_STORE_CTX_get_error_depth(cts);
+ if (depth != 0)
+
+ {
+ BMCWEB_LOG_DEBUG
+ << this << " Certificate verification in progress (depth "
+ << depth << "), waiting to reach final depth";
+ return true;
+ }
+
+ BMCWEB_LOG_DEBUG << this
+ << " Certificate verification of final depth";
+
+ // Verify KeyUsage
+ bool isKeyUsageDigitalSignature = false;
+ bool isKeyUsageKeyAgreement = false;
+
+ ASN1_BIT_STRING* usage = static_cast<ASN1_BIT_STRING*>(
+ X509_get_ext_d2i(peerCert, NID_key_usage, nullptr, nullptr));
+
+ if (usage == nullptr)
+ {
+ BMCWEB_LOG_DEBUG << this << " TLS usage is null";
+ return true;
+ }
+
+ for (int i = 0; i < usage->length; i++)
+ {
+ if (KU_DIGITAL_SIGNATURE & usage->data[i])
+ {
+ isKeyUsageDigitalSignature = true;
+ }
+ if (KU_KEY_AGREEMENT & usage->data[i])
+ {
+ isKeyUsageKeyAgreement = true;
+ }
+ }
+
+ if (!isKeyUsageDigitalSignature || !isKeyUsageKeyAgreement)
+ {
+ BMCWEB_LOG_DEBUG << this
+ << " Certificate ExtendedKeyUsage does "
+ "not allow provided certificate to "
+ "be used for user authentication";
+ return true;
+ }
+ ASN1_BIT_STRING_free(usage);
+
+ // Determine that ExtendedKeyUsage includes Client Auth
+
+ stack_st_ASN1_OBJECT* extUsage =
+ static_cast<stack_st_ASN1_OBJECT*>(X509_get_ext_d2i(
+ peerCert, NID_ext_key_usage, nullptr, nullptr));
+
+ if (extUsage == nullptr)
+ {
+ BMCWEB_LOG_DEBUG << this << " TLS extUsage is null";
+ return true;
+ }
+
+ bool isExKeyUsageClientAuth = false;
+ for (int i = 0; i < sk_ASN1_OBJECT_num(extUsage); i++)
+ {
+ if (NID_client_auth ==
+ OBJ_obj2nid(sk_ASN1_OBJECT_value(extUsage, i)))
+ {
+ isExKeyUsageClientAuth = true;
+ break;
+ }
+ }
+ sk_ASN1_OBJECT_free(extUsage);
+
+ // Certificate has to have proper key usages set
+ if (!isExKeyUsageClientAuth)
+ {
+ BMCWEB_LOG_DEBUG << this
+ << " Certificate ExtendedKeyUsage does "
+ "not allow provided certificate to "
+ "be used for user authentication";
+ return true;
+ }
+ std::string sslUser;
+ // Extract username contained in CommonName
+ sslUser.resize(256, '\0');
+
+ int status = X509_NAME_get_text_by_NID(
+ X509_get_subject_name(peerCert), NID_commonName, sslUser.data(),
+ static_cast<int>(sslUser.size()));
+
+ if (status == -1)
+ {
+ BMCWEB_LOG_DEBUG
+ << this << " TLS cannot get username to create session";
+ return true;
+ }
+
+ size_t lastChar = sslUser.find('\0');
+ if (lastChar == std::string::npos || lastChar == 0)
+ {
+ BMCWEB_LOG_DEBUG << this << " Invalid TLS user name";
+ return true;
+ }
+ sslUser.resize(lastChar);
+
+ session =
+ persistent_data::SessionStore::getInstance()
+ .generateUserSession(
+ sslUser, persistent_data::PersistenceType::TIMEOUT);
+ if (auto sp = session.lock())
+ {
+ BMCWEB_LOG_DEBUG << this
+ << " Generating TLS session: " << sp->uniqueId;
+ }
+ return true;
+ });
+#endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
+
+#ifdef BMCWEB_ENABLE_DEBUG
+ connectionCount++;
+ BMCWEB_LOG_DEBUG << this << " Connection open, total "
+ << connectionCount;
+#endif
+ }
+
+ ~Connection()
+ {
+ res.completeRequestHandler = nullptr;
+ cancelDeadlineTimer();
+#ifdef BMCWEB_ENABLE_DEBUG
+ connectionCount--;
+ BMCWEB_LOG_DEBUG << this << " Connection closed, total "
+ << connectionCount;
+#endif
+ }
+
+ Adaptor& socket()
+ {
+ return adaptor;
+ }
+
+ void start()
+ {
+
+ startDeadline(0);
+ // TODO(ed) Abstract this to a more clever class with the idea of an
+ // asynchronous "start"
+ if constexpr (std::is_same_v<Adaptor,
+ boost::beast::ssl_stream<
+ boost::asio::ip::tcp::socket>>)
+ {
+ adaptor.async_handshake(boost::asio::ssl::stream_base::server,
+ [this, self(shared_from_this())](
+ const boost::system::error_code& ec) {
+ if (ec)
+ {
+ return;
+ }
+ doReadHeaders();
+ });
+ }
+ else
+ {
+ doReadHeaders();
+ }
+ }
+
+ void handle()
+ {
+ cancelDeadlineTimer();
+
+ bool isInvalidRequest = false;
+
+ // Check for HTTP version 1.1.
+ if (req->version() == 11)
+ {
+ if (req->getHeaderValue(boost::beast::http::field::host).empty())
+ {
+ isInvalidRequest = true;
+ res.result(boost::beast::http::status::bad_request);
+ }
+ }
+
+ // Copy the client's IP address
+ req->ipAddress =
+ boost::beast::get_lowest_layer(adaptor).remote_endpoint().address();
+
+ BMCWEB_LOG_INFO << "Request: "
+ << " " << this << " HTTP/" << req->version() / 10 << "."
+ << req->version() % 10 << ' ' << req->methodString()
+ << " " << req->target();
+
+ needToCallAfterHandlers = false;
+
+ if (!isInvalidRequest)
+ {
+ res.completeRequestHandler = [] {};
+ res.isAliveHelper = [this]() -> bool { return isAlive(); };
+
+ req->ioService = static_cast<decltype(req->ioService)>(
+ &adaptor.get_executor().context());
+
+ if (!res.completed)
+ {
+ needToCallAfterHandlers = true;
+ res.completeRequestHandler = [self(shared_from_this())] {
+ boost::asio::post(self->adaptor.get_executor(),
+ [self] { self->completeRequest(); });
+ };
+ if (req->isUpgrade() &&
+ boost::iequals(
+ req->getHeaderValue(boost::beast::http::field::upgrade),
+ "websocket"))
+ {
+ handler->handleUpgrade(*req, res, std::move(adaptor));
+ // delete lambda with self shared_ptr
+ // to enable connection destruction
+ res.completeRequestHandler = nullptr;
+ return;
+ }
+ handler->handle(*req, res);
+ }
+ else
+ {
+ completeRequest();
+ }
+ }
+ else
+ {
+ completeRequest();
+ }
+ }
+
+ bool isAlive()
+ {
+
+ if constexpr (std::is_same_v<Adaptor,
+ boost::beast::ssl_stream<
+ boost::asio::ip::tcp::socket>>)
+ {
+ return adaptor.next_layer().is_open();
+ }
+ else
+ {
+ return adaptor.is_open();
+ }
+ }
+ void close()
+ {
+ if constexpr (std::is_same_v<Adaptor,
+ boost::beast::ssl_stream<
+ boost::asio::ip::tcp::socket>>)
+ {
+ adaptor.next_layer().close();
+#ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
+ if (auto sp = session.lock())
+ {
+ BMCWEB_LOG_DEBUG << this
+ << " Removing TLS session: " << sp->uniqueId;
+ persistent_data::SessionStore::getInstance().removeSession(sp);
+ }
+#endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
+ }
+ else
+ {
+ adaptor.close();
+ }
+ }
+
+ void completeRequest()
+ {
+ BMCWEB_LOG_INFO << "Response: " << this << ' ' << req->url << ' '
+ << res.resultInt() << " keepalive=" << req->keepAlive();
+
+ addSecurityHeaders(res);
+
+ if (needToCallAfterHandlers)
+ {
+ crow::authorization::cleanupTempSession(*req);
+ }
+
+ if (!isAlive())
+ {
+ // BMCWEB_LOG_DEBUG << this << " delete (socket is closed) " <<
+ // isReading
+ // << ' ' << isWriting;
+ // delete this;
+
+ // delete lambda with self shared_ptr
+ // to enable connection destruction
+ res.completeRequestHandler = nullptr;
+ return;
+ }
+ if (res.body().empty() && !res.jsonValue.empty())
+ {
+ if (http_helpers::requestPrefersHtml(*req))
+ {
+ prettyPrintJson(res);
+ }
+ else
+ {
+ res.jsonMode();
+ res.body() = res.jsonValue.dump(2, ' ', true);
+ }
+ }
+
+ if (res.resultInt() >= 400 && res.body().empty())
+ {
+ res.body() = std::string(res.reason());
+ }
+
+ if (res.result() == boost::beast::http::status::no_content)
+ {
+ // Boost beast throws if content is provided on a no-content
+ // response. Ideally, this would never happen, but in the case that
+ // it does, we don't want to throw.
+ BMCWEB_LOG_CRITICAL
+ << this << " Response content provided but code was no-content";
+ res.body().clear();
+ }
+
+ res.addHeader(boost::beast::http::field::date, getCachedDateStr());
+
+ res.keepAlive(req->keepAlive());
+
+ doWrite();
+
+ // delete lambda with self shared_ptr
+ // to enable connection destruction
+ res.completeRequestHandler = nullptr;
+ }
+
+ private:
+ void doReadHeaders()
+ {
+ BMCWEB_LOG_DEBUG << this << " doReadHeaders";
+
+ // Clean up any previous Connection.
+ boost::beast::http::async_read_header(
+ adaptor, buffer, *parser,
+ [this,
+ self(shared_from_this())](const boost::system::error_code& ec,
+ std::size_t bytes_transferred) {
+ BMCWEB_LOG_ERROR << this << " async_read_header "
+ << bytes_transferred << " Bytes";
+ bool errorWhileReading = false;
+ if (ec)
+ {
+ errorWhileReading = true;
+ BMCWEB_LOG_ERROR
+ << this << " Error while reading: " << ec.message();
+ }
+ else
+ {
+ // if the adaptor isn't open anymore, and wasn't handed to a
+ // websocket, treat as an error
+ if (!isAlive() && !req->isUpgrade())
+ {
+ errorWhileReading = true;
+ }
+ }
+
+ cancelDeadlineTimer();
+
+ if (errorWhileReading)
+ {
+ close();
+ BMCWEB_LOG_DEBUG << this << " from read(1)";
+ return;
+ }
+
+ if (!req)
+ {
+ close();
+ return;
+ }
+
+ // Note, despite the bmcweb coding policy on use of exceptions
+ // for error handling, this one particular use of exceptions is
+ // deemed acceptible, as it solved a significant error handling
+ // problem that resulted in seg faults, the exact thing that the
+ // exceptions rule is trying to avoid. If at some point,
+ // boost::urls makes the parser object public (or we port it
+ // into bmcweb locally) this will be replaced with
+ // parser::parse, which returns a status code
+
+ try
+ {
+ req->urlView = boost::urls::url_view(req->target());
+ req->url = req->urlView.encoded_path();
+ }
+ catch (std::exception& p)
+ {
+ BMCWEB_LOG_ERROR << p.what();
+ }
+
+ crow::authorization::authenticate(*req, res, session);
+
+ bool loggedIn = req && req->session;
+ if (loggedIn)
+ {
+ startDeadline(loggedInAttempts);
+ BMCWEB_LOG_DEBUG << "Starting slow deadline";
+
+ req->urlParams = req->urlView.params();
+
+#ifdef BMCWEB_ENABLE_DEBUG
+ std::string paramList = "";
+ for (const auto param : req->urlParams)
+ {
+ paramList += param->key() + " " + param->value() + " ";
+ }
+ BMCWEB_LOG_DEBUG << "QueryParams: " << paramList;
+#endif
+ }
+ else
+ {
+ const boost::optional<uint64_t> contentLength =
+ parser->content_length();
+ if (contentLength &&
+ *contentLength > loggedOutPostBodyLimit)
+ {
+ BMCWEB_LOG_DEBUG << "Content length greater than limit "
+ << *contentLength;
+ close();
+ return;
+ }
+
+ startDeadline(loggedOutAttempts);
+ BMCWEB_LOG_DEBUG << "Starting quick deadline";
+ }
+ doRead();
+ });
+ }
+
+ void doRead()
+ {
+ BMCWEB_LOG_DEBUG << this << " doRead";
+
+ boost::beast::http::async_read(
+ adaptor, buffer, *parser,
+ [this,
+ self(shared_from_this())](const boost::system::error_code& ec,
+ std::size_t bytes_transferred) {
+ BMCWEB_LOG_DEBUG << this << " async_read " << bytes_transferred
+ << " Bytes";
+
+ bool errorWhileReading = false;
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR
+ << this << " Error while reading: " << ec.message();
+ errorWhileReading = true;
+ }
+ else
+ {
+ if (isAlive())
+ {
+ cancelDeadlineTimer();
+ bool loggedIn = req && req->session;
+ if (loggedIn)
+ {
+ startDeadline(loggedInAttempts);
+ }
+ else
+ {
+ startDeadline(loggedOutAttempts);
+ }
+ }
+ else
+ {
+ errorWhileReading = true;
+ }
+ }
+ if (errorWhileReading)
+ {
+ cancelDeadlineTimer();
+ close();
+ BMCWEB_LOG_DEBUG << this << " from read(1)";
+ return;
+ }
+ handle();
+ });
+ }
+
+ void doWrite()
+ {
+ bool loggedIn = req && req->session;
+ if (loggedIn)
+ {
+ startDeadline(loggedInAttempts);
+ }
+ else
+ {
+ startDeadline(loggedOutAttempts);
+ }
+ BMCWEB_LOG_DEBUG << this << " doWrite";
+ res.preparePayload();
+ serializer.emplace(*res.stringResponse);
+ boost::beast::http::async_write(
+ adaptor, *serializer,
+ [this,
+ self(shared_from_this())](const boost::system::error_code& ec,
+ std::size_t bytes_transferred) {
+ BMCWEB_LOG_DEBUG << this << " async_write " << bytes_transferred
+ << " bytes";
+
+ cancelDeadlineTimer();
+
+ if (ec)
+ {
+ BMCWEB_LOG_DEBUG << this << " from write(2)";
+ return;
+ }
+ if (!res.keepAlive())
+ {
+ close();
+ BMCWEB_LOG_DEBUG << this << " from write(1)";
+ return;
+ }
+
+ serializer.reset();
+ BMCWEB_LOG_DEBUG << this << " Clearing response";
+ res.clear();
+ parser.emplace(std::piecewise_construct, std::make_tuple());
+ parser->body_limit(httpReqBodyLimit); // reset body limit for
+ // newly created parser
+ buffer.consume(buffer.size());
+
+ req.emplace(parser->get());
+ doReadHeaders();
+ });
+ }
+
+ void cancelDeadlineTimer()
+ {
+ if (timerCancelKey)
+ {
+ BMCWEB_LOG_DEBUG << this << " timer cancelled: " << &timerQueue
+ << ' ' << *timerCancelKey;
+ timerQueue.cancel(*timerCancelKey);
+ timerCancelKey.reset();
+ }
+ }
+
+ void startDeadline(size_t timerIterations)
+ {
+ cancelDeadlineTimer();
+
+ if (timerIterations)
+ {
+ timerIterations--;
+ }
+
+ timerCancelKey =
+ timerQueue.add([self(shared_from_this()), timerIterations,
+ readCount{parser->get().body().size()}] {
+ // Mark timer as not active to avoid canceling it during
+ // Connection destructor which leads to double free issue
+ self->timerCancelKey.reset();
+ if (!self->isAlive())
+ {
+ return;
+ }
+
+ bool loggedIn = self->req && self->req->session;
+ // allow slow uploads for logged in users
+ if (loggedIn && self->parser->get().body().size() > readCount)
+ {
+ BMCWEB_LOG_DEBUG << self.get()
+ << " restart timer - read in progress";
+ self->startDeadline(timerIterations);
+ return;
+ }
+
+ // Threshold can be used to drop slow connections
+ // to protect against slow-rate DoS attack
+ if (timerIterations)
+ {
+ BMCWEB_LOG_DEBUG << self.get() << " restart timer";
+ self->startDeadline(timerIterations);
+ return;
+ }
+
+ self->close();
+ });
+
+ if (!timerCancelKey)
+ {
+ close();
+ return;
+ }
+ BMCWEB_LOG_DEBUG << this << " timer added: " << &timerQueue << ' '
+ << *timerCancelKey;
+ }
+
+ private:
+ Adaptor adaptor;
+ Handler* handler;
+
+ // Making this a std::optional allows it to be efficiently destroyed and
+ // re-created on Connection reset
+ std::optional<
+ boost::beast::http::request_parser<boost::beast::http::string_body>>
+ parser;
+
+ boost::beast::flat_static_buffer<8192> buffer;
+
+ std::optional<boost::beast::http::response_serializer<
+ boost::beast::http::string_body>>
+ serializer;
+
+ std::optional<crow::Request> req;
+ crow::Response res;
+
+ std::weak_ptr<persistent_data::UserSession> session;
+
+ std::optional<size_t> timerCancelKey;
+
+ bool needToCallAfterHandlers{};
+ bool needToStartReadAfterComplete{};
+
+ std::function<std::string()>& getCachedDateStr;
+ detail::TimerQueue& timerQueue;
+
+ using std::enable_shared_from_this<
+ Connection<Adaptor, Handler>>::shared_from_this;
+};
+} // namespace crow