#pragma once #include "config.h" #include "http_response.h" #include "logging.h" #include "middleware_context.h" #include "timer_queue.h" #include "utility.h" #include "http_utility.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace crow { inline void prettyPrintJson(crow::Response& res) { std::string value = res.jsonValue.dump(4, ' ', true); utility::escapeHtml(value); utility::convertToLinks(value); res.body() = "\n" "\n" "Redfish API\n" "\n" "" "" "\n" "\n" "
\n" "\"redfish\"\n" "
\n" "
\n"
                 "" +
                 value +
                 "\n"
                 "
\n" "
\n" "\n" "\n"; res.addHeader("Content-Type", "text/html;charset=UTF-8"); } using namespace boost; using tcp = asio::ip::tcp; namespace detail { template struct CheckBeforeHandleArity3Const { template struct Get {}; }; template struct CheckBeforeHandleArity3 { template struct Get {}; }; template struct CheckAfterHandleArity3Const { template struct Get {}; }; template struct CheckAfterHandleArity3 { template struct Get {}; }; template struct IsBeforeHandleArity3Impl { template static std::true_type f(typename CheckBeforeHandleArity3Const::template Get*); template static std::true_type f(typename CheckBeforeHandleArity3::template Get*); template static std::false_type f(...); public: static constexpr bool value = decltype(f(nullptr))::value; }; template struct IsAfterHandleArity3Impl { template static std::true_type f(typename CheckAfterHandleArity3Const::template Get*); template static std::true_type f(typename CheckAfterHandleArity3::template Get*); template static std::false_type f(...); public: static constexpr bool value = decltype(f(nullptr))::value; }; template typename std::enable_if::value>::type beforeHandlerCall(MW& mw, Request& req, Response& res, Context& ctx, ParentContext& /*parent_ctx*/) { mw.beforeHandle(req, res, ctx.template get(), ctx); } template typename std::enable_if::value>::type beforeHandlerCall(MW& mw, Request& req, Response& res, Context& ctx, ParentContext& /*parent_ctx*/) { mw.beforeHandle(req, res, ctx.template get()); } template typename std::enable_if::value>::type afterHandlerCall(MW& mw, Request& req, Response& res, Context& ctx, ParentContext& /*parent_ctx*/) { mw.afterHandle(req, res, ctx.template get(), ctx); } template typename std::enable_if::value>::type afterHandlerCall(MW& mw, Request& req, Response& res, Context& ctx, ParentContext& /*parent_ctx*/) { mw.afterHandle(req, res, ctx.template get()); } template bool middlewareCallHelper(Container& middlewares, Request& req, Response& res, Context& ctx) { using parent_context_t = typename Context::template partial; beforeHandlerCall( std::get(middlewares), req, res, ctx, static_cast(ctx)); if (res.isCompleted()) { afterHandlerCall( std::get(middlewares), req, res, ctx, static_cast(ctx)); return true; } if (middlewareCallHelper( middlewares, req, res, ctx)) { afterHandlerCall( std::get(middlewares), req, res, ctx, static_cast(ctx)); return true; } return false; } template bool middlewareCallHelper(Container& /*middlewares*/, Request& /*req*/, Response& /*res*/, Context& /*ctx*/) { return false; } template typename std::enable_if<(N < 0)>::type afterHandlersCallHelper(Container& /*middlewares*/, Context& /*Context*/, Request& /*req*/, Response& /*res*/) {} template typename std::enable_if<(N == 0)>::type afterHandlersCallHelper(Container& middlewares, Context& ctx, Request& req, Response& res) { using parent_context_t = typename Context::template partial; using CurrentMW = typename std::tuple_element< N, typename std::remove_reference::type>::type; afterHandlerCall( std::get(middlewares), req, res, ctx, static_cast(ctx)); } template typename std::enable_if<(N > 0)>::type afterHandlersCallHelper(Container& middlewares, Context& ctx, Request& req, Response& res) { using parent_context_t = typename Context::template partial; using CurrentMW = typename std::tuple_element< N, typename std::remove_reference::type>::type; afterHandlerCall( std::get(middlewares), req, res, ctx, static_cast(ctx)); afterHandlersCallHelper(middlewares, ctx, req, res); } } // namespace detail #ifdef BMCWEB_ENABLE_DEBUG static std::atomic 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; template class Connection : public std::enable_shared_from_this< Connection> { public: Connection(boost::asio::io_context& ioService, Handler* handlerIn, const std::string& ServerNameIn, std::tuple* middlewaresIn, std::function& get_cached_date_str_f, detail::TimerQueue& timerQueueIn, Adaptor adaptorIn) : adaptor(std::move(adaptorIn)), handler(handlerIn), serverName(ServerNameIn), middlewares(middlewaresIn), getCachedDateStr(get_cached_date_str_f), timerQueue(timerQueueIn) { parser.emplace(std::piecewise_construct, std::make_tuple()); // Temporarily set by the BMCWEB_HTTP_REQ_BODY_LIMIT_MB variable; Need // to modify uploading/authentication mechanism to a better method that // disallows a DOS attack based on a large file size. parser->body_limit(httpReqBodyLimit); req.emplace(parser->get()); #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION auto ca_available = !std::filesystem::is_empty( std::filesystem::path(ensuressl::trustStorePath)); if (ca_available && crow::persistent_data::SessionStore::getInstance() .getAuthMethodsConfig() .tls) { adaptor.set_verify_mode(boost::asio::ssl::verify_peer); SSL_set_session_id_context( adaptor.native_handle(), reinterpret_cast(serverName.c_str()), static_cast(serverName.length())); BMCWEB_LOG_DEBUG << this << " TLS is enabled on this connection."; } adaptor.set_verify_callback([this]( bool preverified, boost::asio::ssl::verify_context& ctx) { // do nothing if TLS is disabled if (!crow::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( X509_get_ext_d2i(peerCert, NID_key_usage, NULL, NULL)); 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( X509_get_ext_d2i(peerCert, NID_ext_key_usage, NULL, NULL)); 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(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, crow::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(); // TODO(ed) Abstract this to a more clever class with the idea of an // asynchronous "start" if constexpr (std::is_same_v>) { 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); } } BMCWEB_LOG_INFO << "Request: " << " " << this << " HTTP/" << req->version() / 10 << "." << req->version() % 10 << ' ' << req->methodString() << " " << req->target(); needToCallAfterHandlers = false; if (!isInvalidRequest) { req->socket = [this, self = shared_from_this()]() -> Adaptor& { return self->socket(); }; res.completeRequestHandler = [] {}; res.isAliveHelper = [this]() -> bool { return isAlive(); }; ctx = detail::Context(); req->middlewareContext = static_cast(&ctx); req->ioService = static_castioService)>( &adaptor.get_executor().context()); #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION if (auto sp = session.lock()) { // set cookie only if this is req from the browser. if (req->getHeaderValue("User-Agent").empty()) { BMCWEB_LOG_DEBUG << this << " TLS session: " << sp->uniqueId << " will be used for this request."; req->session = sp; } else { std::string_view cookieValue = req->getHeaderValue("Cookie"); if (cookieValue.empty() || cookieValue.find("SESSION=") == std::string::npos) { res.addHeader("Set-Cookie", "XSRF-TOKEN=" + sp->csrfToken + "; Secure\r\nSet-Cookie: SESSION=" + sp->sessionToken + "; Secure; HttpOnly\r\nSet-Cookie: " "IsAuthenticated=true; Secure"); BMCWEB_LOG_DEBUG << this << " TLS session: " << sp->uniqueId << " with cookie will be used for this request."; req->session = sp; } } } #endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION detail::middlewareCallHelper< 0U, decltype(ctx), decltype(*middlewares), Middlewares...>( *middlewares, *req, res, ctx); if (!res.completed) { needToCallAfterHandlers = true; res.completeRequestHandler = [self(shared_from_this())] { 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>) { return adaptor.next_layer().is_open(); } else { return adaptor.is_open(); } } void close() { if constexpr (std::is_same_v>) { 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(); if (needToCallAfterHandlers) { needToCallAfterHandlers = false; // call all afterHandler of middlewares detail::afterHandlersCallHelper( *middlewares, ctx, *req, res); } 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::server, serverName); 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; } } if (errorWhileReading) { cancelDeadlineTimer(); close(); BMCWEB_LOG_DEBUG << this << " from read(1)"; return; } // Compute the url parameters for the request req->url = req->target(); std::size_t index = req->url.find("?"); if (index != std::string_view::npos) { req->url = req->url.substr(0, index); } req->urlParams = QueryString(std::string(req->target())); 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()) { errorWhileReading = true; } } if (errorWhileReading) { cancelDeadlineTimer(); close(); BMCWEB_LOG_DEBUG << this << " from read(1)"; return; } handle(); }); } void doWrite() { 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"; 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 = 0) { // drop all connections after 1 minute, this time limit was chosen // arbitrarily and can be adjusted later if needed constexpr const size_t maxReadAttempts = (60 / detail::timerQueueTimeoutSeconds); cancelDeadlineTimer(); timerCancelKey = timerQueue.add([this, self(shared_from_this()), readCount{parser->get().body().size()}, timerIterations{timerIterations + 1}] { // Mark timer as not active to avoid canceling it during // Connection destructor which leads to double free issue timerCancelKey.reset(); if (!isAlive()) { return; } // Restart timer if read is in progress. // With threshold can be used to drop slow connections // to protect against slow-rate DoS attack if ((parser->get().body().size() > readCount) && (timerIterations < maxReadAttempts)) { BMCWEB_LOG_DEBUG << this << " restart timer - read in progress"; startDeadline(timerIterations); return; } 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> parser; boost::beast::flat_static_buffer<8192> buffer; std::optional> serializer; std::optional req; crow::Response res; #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION std::weak_ptr session; #endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION const std::string& serverName; std::optional timerCancelKey; bool needToCallAfterHandlers{}; bool needToStartReadAfterComplete{}; std::tuple* middlewares; detail::Context ctx; std::function& getCachedDateStr; detail::TimerQueue& timerQueue; using std::enable_shared_from_this< Connection>::shared_from_this; }; } // namespace crow