diff options
-rw-r--r-- | config/bmcweb_config.h.in | 2 | ||||
-rw-r--r-- | config/meson.build | 2 | ||||
-rw-r--r-- | http/http2_connection.hpp | 555 | ||||
-rw-r--r-- | http/http_connection.hpp | 31 | ||||
-rw-r--r-- | http/http_request.hpp | 2 | ||||
-rw-r--r-- | http/nghttp2_adapters.hpp | 152 | ||||
-rw-r--r-- | include/ssl_key_handler.hpp | 42 | ||||
-rw-r--r-- | meson.build | 13 | ||||
-rw-r--r-- | meson_options.txt | 9 | ||||
-rw-r--r-- | subprojects/nghttp2.wrap | 5 |
10 files changed, 812 insertions, 1 deletions
diff --git a/config/bmcweb_config.h.in b/config/bmcweb_config.h.in index 933c6e8a2b..3fd3ea8433 100644 --- a/config/bmcweb_config.h.in +++ b/config/bmcweb_config.h.in @@ -22,4 +22,6 @@ constexpr const bool bmcwebEnableHealthPopulate = @BMCWEB_ENABLE_HEALTH_POPULATE constexpr const bool bmcwebEnableProcMemStatus = @BMCWEB_ENABLE_PROC_MEM_STATUS@ == 1; constexpr const bool bmcwebEnableMultiHost = @BMCWEB_ENABLE_MULTI_HOST@ == 1; + +constexpr const bool bmcwebEnableHTTP2 = @BMCWEB_ENABLE_HTTP2@ == 1; // clang-format on diff --git a/config/meson.build b/config/meson.build index 11ef95c6e5..8a72a63e37 100644 --- a/config/meson.build +++ b/config/meson.build @@ -18,6 +18,8 @@ enable_proc_mem_status = get_option('redfish-enable-proccessor-memory-status') conf_data.set10('BMCWEB_ENABLE_PROC_MEM_STATUS', enable_proc_mem_status.enabled()) enable_multi_host = get_option('experimental-redfish-multi-computer-system') conf_data.set10('BMCWEB_ENABLE_MULTI_HOST', enable_multi_host.enabled()) +enable_http2 = get_option('experimental-http2') +conf_data.set10('BMCWEB_ENABLE_HTTP2', enable_http2.enabled()) # Logging level loglvlopt = get_option('bmcweb-logging') diff --git a/http/http2_connection.hpp b/http/http2_connection.hpp new file mode 100644 index 0000000000..dad5089731 --- /dev/null +++ b/http/http2_connection.hpp @@ -0,0 +1,555 @@ +#pragma once +#include "bmcweb_config.h" + +#include "async_resp.hpp" +#include "authentication.hpp" +#include "complete_response_fields.hpp" +#include "http_response.hpp" +#include "http_utility.hpp" +#include "logging.hpp" +#include "mutual_tls.hpp" +#include "nghttp2_adapters.hpp" +#include "ssl_key_handler.hpp" +#include "utility.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/asio/steady_timer.hpp> +#include <boost/beast/core/multi_buffer.hpp> +#include <boost/beast/http/error.hpp> +#include <boost/beast/http/parser.hpp> +#include <boost/beast/http/read.hpp> +#include <boost/beast/http/serializer.hpp> +#include <boost/beast/http/string_body.hpp> +#include <boost/beast/http/write.hpp> +#include <boost/beast/ssl/ssl_stream.hpp> +#include <boost/beast/websocket.hpp> + +#include <atomic> +#include <chrono> +#include <vector> + +namespace crow +{ + +struct Http2StreamData +{ + crow::Request req{}; + crow::Response res{}; + size_t sentSofar = 0; +}; + +template <typename Adaptor, typename Handler> +class HTTP2Connection : + public std::enable_shared_from_this<HTTP2Connection<Adaptor, Handler>> +{ + using self_type = HTTP2Connection<Adaptor, Handler>; + + public: + HTTP2Connection(Adaptor&& adaptorIn, Handler* handlerIn, + std::function<std::string()>& getCachedDateStrF + + ) : + adaptor(std::move(adaptorIn)), + + ngSession(initializeNghttp2Session()), + + handler(handlerIn), getCachedDateStr(getCachedDateStrF) + {} + + void start() + { + // Create the control stream + streams.emplace(0, std::make_unique<Http2StreamData>()); + + if (sendServerConnectionHeader() != 0) + { + BMCWEB_LOG_ERROR << "send_server_connection_header failed"; + return; + } + doRead(); + } + + int sendServerConnectionHeader() + { + BMCWEB_LOG_DEBUG << "send_server_connection_header()"; + + uint32_t maxStreams = 4; + std::array<nghttp2_settings_entry, 2> iv = { + {{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, maxStreams}, + {NGHTTP2_SETTINGS_ENABLE_PUSH, 0}}}; + int rv = ngSession.submitSettings(iv); + if (rv != 0) + { + BMCWEB_LOG_ERROR << "Fatal error: " << nghttp2_strerror(rv); + return -1; + } + return 0; + } + + static ssize_t fileReadCallback(nghttp2_session* /* session */, + int32_t /* stream_id */, uint8_t* buf, + size_t length, uint32_t* dataFlags, + nghttp2_data_source* source, + void* /*unused*/) + { + if (source == nullptr || source->ptr == nullptr) + { + BMCWEB_LOG_DEBUG << "Source was null???"; + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + BMCWEB_LOG_DEBUG << "File read callback length: " << length; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + Http2StreamData* str = reinterpret_cast<Http2StreamData*>(source->ptr); + crow::Response& res = str->res; + + BMCWEB_LOG_DEBUG << "total: " << res.body().size() + << " send_sofar: " << str->sentSofar; + + size_t toSend = std::min(res.body().size() - str->sentSofar, length); + BMCWEB_LOG_DEBUG << "Copying " << toSend << " bytes to buf"; + + std::string::iterator bodyBegin = res.body().begin(); + std::advance(bodyBegin, str->sentSofar); + + memcpy(buf, &*bodyBegin, toSend); + str->sentSofar += toSend; + + if (str->sentSofar >= res.body().size()) + { + BMCWEB_LOG_DEBUG << "Setting OEF flag"; + *dataFlags |= NGHTTP2_DATA_FLAG_EOF; + //*dataFlags |= NGHTTP2_DATA_FLAG_NO_COPY; + } + return static_cast<ssize_t>(toSend); + } + + nghttp2_nv headerFromStringViews(std::string_view name, + std::string_view value) + { + uint8_t* nameData = std::bit_cast<uint8_t*>(name.data()); + uint8_t* valueData = std::bit_cast<uint8_t*>(value.data()); + return {nameData, valueData, name.size(), value.size(), + NGHTTP2_NV_FLAG_NONE}; + } + + int sendResponse(Response& completedRes, int32_t streamId) + { + BMCWEB_LOG_DEBUG << "send_response stream_id:" << streamId; + + auto it = streams.find(streamId); + if (it == streams.end()) + { + close(); + return -1; + } + Response& thisRes = it->second->res; + thisRes = std::move(completedRes); + crow::Request& thisReq = it->second->req; + std::vector<nghttp2_nv> hdr; + + completeResponseFields(thisReq, thisRes); + thisRes.addHeader(boost::beast::http::field::date, getCachedDateStr()); + + boost::beast::http::fields& fields = thisRes.stringResponse->base(); + std::string code = std::to_string(thisRes.stringResponse->result_int()); + hdr.emplace_back(headerFromStringViews(":status", code)); + for (const boost::beast::http::fields::value_type& header : fields) + { + hdr.emplace_back( + headerFromStringViews(header.name_string(), header.value())); + } + Http2StreamData* streamPtr = it->second.get(); + streamPtr->sentSofar = 0; + + nghttp2_data_provider dataPrd{ + .source{ + .ptr = streamPtr, + }, + .read_callback = fileReadCallback, + }; + + int rv = ngSession.submitResponse(streamId, hdr, &dataPrd); + if (rv != 0) + { + BMCWEB_LOG_ERROR << "Fatal error: " << nghttp2_strerror(rv); + close(); + return -1; + } + ngSession.send(); + + return 0; + } + + nghttp2_session initializeNghttp2Session() + { + nghttp2_session_callbacks callbacks; + callbacks.setOnFrameRecvCallback(onFrameRecvCallbackStatic); + callbacks.setOnStreamCloseCallback(onStreamCloseCallbackStatic); + callbacks.setOnHeaderCallback(onHeaderCallbackStatic); + callbacks.setOnBeginHeadersCallback(onBeginHeadersCallbackStatic); + callbacks.setSendCallback(onSendCallbackStatic); + + nghttp2_session session(callbacks); + session.setUserData(this); + + return session; + } + + int onRequestRecv(int32_t streamId) + { + BMCWEB_LOG_DEBUG << "on_request_recv"; + + auto it = streams.find(streamId); + if (it == streams.end()) + { + close(); + return -1; + } + + crow::Request& thisReq = it->second->req; + BMCWEB_LOG_DEBUG << "Handling " << &thisReq << " \"" + << thisReq.url().encoded_path() << "\""; + + crow::Response& thisRes = it->second->res; + + thisRes.setCompleteRequestHandler( + [this, streamId](Response& completeRes) { + BMCWEB_LOG_DEBUG << "res.completeRequestHandler called"; + if (sendResponse(completeRes, streamId) != 0) + { + close(); + return; + } + }); + auto asyncResp = + std::make_shared<bmcweb::AsyncResp>(std::move(it->second->res)); + handler->handle(thisReq, asyncResp); + + return 0; + } + + int onFrameRecvCallback(const nghttp2_frame& frame) + { + BMCWEB_LOG_DEBUG << "frame type " << static_cast<int>(frame.hd.type); + switch (frame.hd.type) + { + case NGHTTP2_DATA: + case NGHTTP2_HEADERS: + // Check that the client request has finished + if ((frame.hd.flags & NGHTTP2_FLAG_END_STREAM) != 0) + { + return onRequestRecv(frame.hd.stream_id); + } + break; + default: + break; + } + return 0; + } + + static int onFrameRecvCallbackStatic(nghttp2_session* /* session */, + const nghttp2_frame* frame, + void* userData) + { + BMCWEB_LOG_DEBUG << "on_frame_recv_callback"; + if (userData == nullptr) + { + BMCWEB_LOG_CRITICAL << "user data was null?"; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + if (frame == nullptr) + { + BMCWEB_LOG_CRITICAL << "frame was null?"; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return userPtrToSelf(userData).onFrameRecvCallback(*frame); + } + + static self_type& userPtrToSelf(void* userData) + { + // This method exists to keep the unsafe reinterpret cast in one + // place. + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return *reinterpret_cast<self_type*>(userData); + } + + static int onStreamCloseCallbackStatic(nghttp2_session* /* session */, + int32_t streamId, + uint32_t /*unused*/, void* userData) + { + BMCWEB_LOG_DEBUG << "on_stream_close_callback stream " << streamId; + if (userData == nullptr) + { + BMCWEB_LOG_CRITICAL << "user data was null?"; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + auto stream = userPtrToSelf(userData).streams.find(streamId); + if (stream == userPtrToSelf(userData).streams.end()) + { + return -1; + } + + userPtrToSelf(userData).streams.erase(streamId); + return 0; + } + + int onHeaderCallback(const nghttp2_frame& frame, + std::span<const uint8_t> name, + std::span<const uint8_t> value) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + std::string_view nameSv(reinterpret_cast<const char*>(name.data()), + name.size()); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + std::string_view valueSv(reinterpret_cast<const char*>(value.data()), + value.size()); + + BMCWEB_LOG_DEBUG << "on_header_callback name: " << nameSv << " value " + << valueSv; + + switch (frame.hd.type) + { + case NGHTTP2_HEADERS: + if (frame.headers.cat != NGHTTP2_HCAT_REQUEST) + { + break; + } + auto thisStream = streams.find(frame.hd.stream_id); + if (thisStream == streams.end()) + { + BMCWEB_LOG_ERROR << "Unknown stream" << frame.hd.stream_id; + close(); + return -1; + } + + crow::Request& thisReq = thisStream->second->req; + + if (nameSv == ":path") + { + thisReq.target(valueSv); + } + else if (nameSv == ":method") + { + boost::beast::http::verb verb = + boost::beast::http::string_to_verb(valueSv); + if (verb == boost::beast::http::verb::unknown) + { + BMCWEB_LOG_ERROR << "Unknown http verb " << valueSv; + close(); + return -1; + } + thisReq.req.method(verb); + } + else if (nameSv == ":scheme") + { + // Nothing to check on scheme + } + else + { + thisReq.req.set(nameSv, valueSv); + } + break; + } + return 0; + } + + static int onHeaderCallbackStatic(nghttp2_session* /* session */, + const nghttp2_frame* frame, + const uint8_t* name, size_t namelen, + const uint8_t* value, size_t vallen, + uint8_t /* flags */, void* userData) + { + if (userData == nullptr) + { + BMCWEB_LOG_CRITICAL << "user data was null?"; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + if (frame == nullptr) + { + BMCWEB_LOG_CRITICAL << "frame was null?"; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + if (name == nullptr) + { + BMCWEB_LOG_CRITICAL << "name was null?"; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + if (value == nullptr) + { + BMCWEB_LOG_CRITICAL << "value was null?"; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return userPtrToSelf(userData).onHeaderCallback(*frame, {name, namelen}, + {value, vallen}); + } + + int onBeginHeadersCallback(const nghttp2_frame& frame) + { + if (frame.hd.type == NGHTTP2_HEADERS && + frame.headers.cat == NGHTTP2_HCAT_REQUEST) + { + BMCWEB_LOG_DEBUG << "create stream for id " << frame.hd.stream_id; + + std::pair<boost::container::flat_map< + int32_t, std::unique_ptr<Http2StreamData>>::iterator, + bool> + stream = streams.emplace(frame.hd.stream_id, + std::make_unique<Http2StreamData>()); + // http2 is by definition always tls + stream.first->second->req.isSecure = true; + } + return 0; + } + + static int onBeginHeadersCallbackStatic(nghttp2_session* /* session */, + const nghttp2_frame* frame, + void* userData) + { + BMCWEB_LOG_DEBUG << "on_begin_headers_callback"; + if (userData == nullptr) + { + BMCWEB_LOG_CRITICAL << "user data was null?"; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + if (frame == nullptr) + { + BMCWEB_LOG_CRITICAL << "frame was null?"; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + return userPtrToSelf(userData).onBeginHeadersCallback(*frame); + } + + static void afterWriteBuffer(const std::shared_ptr<self_type>& self, + const boost::system::error_code& ec, + size_t sendLength) + { + self->isWriting = false; + BMCWEB_LOG_DEBUG << "Sent " << sendLength; + if (ec) + { + self->close(); + return; + } + self->sendBuffer.consume(sendLength); + self->writeBuffer(); + } + + void writeBuffer() + { + if (isWriting) + { + return; + } + if (sendBuffer.size() <= 0) + { + return; + } + isWriting = true; + adaptor.async_write_some( + sendBuffer.data(), + std::bind_front(afterWriteBuffer, shared_from_this())); + } + + ssize_t onSendCallback(nghttp2_session* /*session */, const uint8_t* data, + size_t length, int /* flags */) + { + BMCWEB_LOG_DEBUG << "On send callback size=" << length; + size_t copied = boost::asio::buffer_copy( + sendBuffer.prepare(length), boost::asio::buffer(data, length)); + sendBuffer.commit(copied); + writeBuffer(); + return static_cast<ssize_t>(length); + } + + static ssize_t onSendCallbackStatic(nghttp2_session* session, + const uint8_t* data, size_t length, + int flags /* flags */, void* userData) + { + return userPtrToSelf(userData).onSendCallback(session, data, length, + flags); + } + + void close() + { + if constexpr (std::is_same_v<Adaptor, + boost::beast::ssl_stream< + boost::asio::ip::tcp::socket>>) + { + adaptor.next_layer().close(); + } + else + { + adaptor.close(); + } + } + + void doRead() + { + BMCWEB_LOG_DEBUG << this << " doRead"; + adaptor.async_read_some( + inBuffer.prepare(8192), + [this, self(shared_from_this())]( + const boost::system::error_code& ec, size_t bytesTransferred) { + BMCWEB_LOG_DEBUG << this << " async_read_some " << bytesTransferred + << " Bytes"; + + if (ec) + { + BMCWEB_LOG_ERROR << this + << " Error while reading: " << ec.message(); + close(); + BMCWEB_LOG_DEBUG << this << " from read(1)"; + return; + } + inBuffer.commit(bytesTransferred); + + size_t consumed = 0; + for (const auto bufferIt : inBuffer.data()) + { + std::span<const uint8_t> bufferSpan{ + std::bit_cast<const uint8_t*>(bufferIt.data()), + bufferIt.size()}; + BMCWEB_LOG_DEBUG << "http2 is getting " << bufferSpan.size() + << " bytes"; + ssize_t readLen = ngSession.memRecv(bufferSpan); + if (readLen <= 0) + { + BMCWEB_LOG_ERROR << "nghttp2_session_mem_recv returned " + << readLen; + close(); + return; + } + consumed += static_cast<size_t>(readLen); + } + inBuffer.consume(consumed); + + doRead(); + }); + } + + // A mapping from http2 stream ID to Stream Data + boost::container::flat_map<int32_t, std::unique_ptr<Http2StreamData>> + streams; + + boost::beast::multi_buffer sendBuffer; + boost::beast::multi_buffer inBuffer; + + Adaptor adaptor; + bool isWriting = false; + + nghttp2_session ngSession; + + Handler* handler; + std::function<std::string()>& getCachedDateStr; + + using std::enable_shared_from_this< + HTTP2Connection<Adaptor, Handler>>::shared_from_this; + + using std::enable_shared_from_this< + HTTP2Connection<Adaptor, Handler>>::weak_from_this; +}; +} // namespace crow diff --git a/http/http_connection.hpp b/http/http_connection.hpp index 3548870418..cb252f96aa 100644 --- a/http/http_connection.hpp +++ b/http/http_connection.hpp @@ -4,6 +4,7 @@ #include "async_resp.hpp" #include "authentication.hpp" #include "complete_response_fields.hpp" +#include "http2_connection.hpp" #include "http_response.hpp" #include "http_utility.hpp" #include "logging.hpp" @@ -161,7 +162,7 @@ class Connection : { return; } - doReadHeaders(); + afterSslHandshake(); }); } else @@ -170,6 +171,34 @@ class Connection : } } + void afterSslHandshake() + { + // If http2 is enabled, negotiate the protocol + if constexpr (bmcwebEnableHTTP2) + { + const unsigned char* alpn = nullptr; + unsigned int alpnlen = 0; + SSL_get0_alpn_selected(adaptor.native_handle(), &alpn, &alpnlen); + if (alpn != nullptr) + { + std::string_view selectedProtocol( + std::bit_cast<const char*>(alpn), alpnlen); + BMCWEB_LOG_DEBUG << "ALPN selected protocol \"" + << selectedProtocol << "\" len: " << alpnlen; + if (selectedProtocol == "h2") + { + auto http2 = + std::make_shared<HTTP2Connection<Adaptor, Handler>>( + std::move(adaptor), handler, getCachedDateStr); + http2->start(); + return; + } + } + } + + doReadHeaders(); + } + void handle() { std::error_code reqEc; diff --git a/http/http_request.hpp b/http/http_request.hpp index 3fd9d4d753..5ce434b921 100644 --- a/http/http_request.hpp +++ b/http/http_request.hpp @@ -51,6 +51,8 @@ struct Request } } + Request() = default; + Request(const Request& other) = default; Request(Request&& other) = default; diff --git a/http/nghttp2_adapters.hpp b/http/nghttp2_adapters.hpp new file mode 100644 index 0000000000..3c1f549e85 --- /dev/null +++ b/http/nghttp2_adapters.hpp @@ -0,0 +1,152 @@ +#pragma once + +extern "C" +{ +#include <nghttp2/nghttp2.h> +} + +#include "logging.hpp" + +#include <span> + +/* This file contains RAII compatible adapters for nghttp2 structures. They + * attempt to be as close to a direct call as possible, while keeping the RAII + * lifetime safety for the various classes.*/ + +struct nghttp2_session; + +struct nghttp2_session_callbacks +{ + friend nghttp2_session; + nghttp2_session_callbacks() + { + nghttp2_session_callbacks_new(&ptr); + } + + ~nghttp2_session_callbacks() + { + nghttp2_session_callbacks_del(ptr); + } + + nghttp2_session_callbacks(const nghttp2_session_callbacks&) = delete; + nghttp2_session_callbacks& + operator=(const nghttp2_session_callbacks&) = delete; + nghttp2_session_callbacks(nghttp2_session_callbacks&&) = delete; + nghttp2_session_callbacks& operator=(nghttp2_session_callbacks&&) = delete; + + void setSendCallback(nghttp2_send_callback sendCallback) + { + nghttp2_session_callbacks_set_send_callback(ptr, sendCallback); + } + + void setOnFrameRecvCallback(nghttp2_on_frame_recv_callback recvCallback) + { + nghttp2_session_callbacks_set_on_frame_recv_callback(ptr, recvCallback); + } + + void setOnStreamCloseCallback(nghttp2_on_stream_close_callback onClose) + { + nghttp2_session_callbacks_set_on_stream_close_callback(ptr, onClose); + } + + void setOnHeaderCallback(nghttp2_on_header_callback onHeader) + { + nghttp2_session_callbacks_set_on_header_callback(ptr, onHeader); + } + + void setOnBeginHeadersCallback( + nghttp2_on_begin_headers_callback onBeginHeaders) + { + nghttp2_session_callbacks_set_on_begin_headers_callback(ptr, + onBeginHeaders); + } + + void setSendDataCallback(nghttp2_send_data_callback onSendData) + { + nghttp2_session_callbacks_set_send_data_callback(ptr, onSendData); + } + void setBeforeFrameSendCallback( + nghttp2_before_frame_send_callback beforeSendFrame) + { + nghttp2_session_callbacks_set_before_frame_send_callback( + ptr, beforeSendFrame); + } + void + setAfterFrameSendCallback(nghttp2_on_frame_send_callback afterSendFrame) + { + nghttp2_session_callbacks_set_on_frame_send_callback(ptr, + afterSendFrame); + } + void setAfterFrameNoSendCallback( + nghttp2_on_frame_not_send_callback afterSendFrame) + { + nghttp2_session_callbacks_set_on_frame_not_send_callback( + ptr, afterSendFrame); + } + + private: + nghttp2_session_callbacks* get() + { + return ptr; + } + + nghttp2_session_callbacks* ptr = nullptr; +}; + +struct nghttp2_session +{ + explicit nghttp2_session(nghttp2_session_callbacks& callbacks) + { + if (nghttp2_session_server_new(&ptr, callbacks.get(), nullptr) != 0) + { + BMCWEB_LOG_ERROR << "nghttp2_session_server_new failed"; + return; + } + } + + ~nghttp2_session() + { + nghttp2_session_del(ptr); + } + + // explicitly uncopyable + nghttp2_session(const nghttp2_session&) = delete; + nghttp2_session& operator=(const nghttp2_session&) = delete; + + nghttp2_session(nghttp2_session&& other) noexcept : ptr(other.ptr) + { + other.ptr = nullptr; + } + + nghttp2_session& operator=(nghttp2_session&& other) noexcept = delete; + + int submitSettings(std::span<nghttp2_settings_entry> iv) + { + return nghttp2_submit_settings(ptr, NGHTTP2_FLAG_NONE, iv.data(), + iv.size()); + } + void setUserData(void* object) + { + nghttp2_session_set_user_data(ptr, object); + } + + ssize_t memRecv(std::span<const uint8_t> buffer) + { + return nghttp2_session_mem_recv(ptr, buffer.data(), buffer.size()); + } + + ssize_t send() + { + return nghttp2_session_send(ptr); + } + + int submitResponse(int32_t streamId, std::span<const nghttp2_nv> headers, + const nghttp2_data_provider* dataPrd) + { + return nghttp2_submit_response(ptr, streamId, headers.data(), + headers.size(), dataPrd); + } + + private: + nghttp2_session* ptr = nullptr; +}; diff --git a/include/ssl_key_handler.hpp b/include/ssl_key_handler.hpp index db61db9f83..0794fdcfac 100644 --- a/include/ssl_key_handler.hpp +++ b/include/ssl_key_handler.hpp @@ -3,6 +3,10 @@ #include "logging.hpp" #include "random.hpp" +extern "C" +{ +#include <nghttp2/nghttp2.h> +} #include <openssl/bio.h> #include <openssl/dh.h> #include <openssl/dsa.h> @@ -423,6 +427,36 @@ inline void ensureOpensslKeyPresentAndValid(const std::string& filepath) } } +inline int nextProtoCallback(SSL* /*unused*/, const unsigned char** data, + unsigned int* len, void* /*unused*/) +{ + // First byte is the length. + constexpr std::string_view h2 = "\x02h2"; + *data = std::bit_cast<const unsigned char*>(h2.data()); + *len = static_cast<unsigned int>(h2.size()); + return SSL_TLSEXT_ERR_OK; +} + +inline int alpnSelectProtoCallback(SSL* /*unused*/, const unsigned char** out, + unsigned char* outlen, + const unsigned char* in, unsigned int inlen, + void* /*unused*/) +{ + // There's a mismatch in constness for nghttp2_select_next_protocol. The + // examples in nghttp2 don't show this problem. Unclear what the right fix + // is here. + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) + unsigned char** outNew = const_cast<unsigned char**>(out); + int rv = nghttp2_select_next_protocol(outNew, outlen, in, inlen); + if (rv != 1) + { + return SSL_TLSEXT_ERR_NOACK; + } + + return SSL_TLSEXT_ERR_OK; +} + inline std::shared_ptr<boost::asio::ssl::context> getSslContext(const std::string& sslPemFile) { @@ -450,6 +484,14 @@ inline std::shared_ptr<boost::asio::ssl::context> mSslContext->use_private_key_file(sslPemFile, boost::asio::ssl::context::pem); + if constexpr (bmcwebEnableHTTP2) + { + SSL_CTX_set_next_protos_advertised_cb(mSslContext->native_handle(), + nextProtoCallback, nullptr); + + SSL_CTX_set_alpn_select_cb(mSslContext->native_handle(), + alpnSelectProtoCallback, nullptr); + } // Set up EC curves to auto (boost asio doesn't have a method for this) // There is a pull request to add this. Once this is included in an asio // drop, use the right way diff --git a/meson.build b/meson.build index f9d2825d38..7bc594b132 100644 --- a/meson.build +++ b/meson.build @@ -259,6 +259,19 @@ atomic = cxx.find_library('atomic', required: true) openssl = dependency('openssl', required : true) bmcweb_dependencies += [pam, atomic, openssl] +nghttp2 = dependency('libnghttp2', version: '>=1.52.0', required : false) +if not nghttp2.found() + cmake = import('cmake') + opt_var = cmake.subproject_options() + opt_var.add_cmake_defines({ + 'ENABLE_LIB_ONLY': true, + 'ENABLE_STATIC_LIB': true + }) + nghttp2_ex = cmake.subproject('nghttp2', options: opt_var) + nghttp2 = nghttp2_ex.dependency('nghttp2') +endif +bmcweb_dependencies += nghttp2 + sdbusplus = dependency('sdbusplus', required : false, include_type: 'system') if not sdbusplus.found() sdbusplus_proj = subproject('sdbusplus', required: true) diff --git a/meson_options.txt b/meson_options.txt index dbfa00d0fe..017c16bd68 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -281,6 +281,15 @@ option( production environment, or where API stability is required.''' ) +option( + 'experimental-http2', + type: 'feature', + value: 'disabled', + description: '''Enable HTTP/2 protocol support using nghttp2. Do not rely + on this option for any production systems. It may have + behavior changes or be removed at any time.''' +) + # Insecure options. Every option that starts with a `insecure` flag should # not be enabled by default for any platform, unless the author fully comprehends # the implications of doing so.In general, enabling these options will cause security diff --git a/subprojects/nghttp2.wrap b/subprojects/nghttp2.wrap new file mode 100644 index 0000000000..48290c4a39 --- /dev/null +++ b/subprojects/nghttp2.wrap @@ -0,0 +1,5 @@ +[wrap-file] +directory = nghttp2-1.54.0 +source_url = https://github.com/nghttp2/nghttp2/releases/download/v1.54.0/nghttp2-1.54.0.tar.xz +source_filename = nghttp2-1.54.0.tar.xz +source_hash = 20533c9354fbb6aa689b6aa0ddb77f91da1d242587444502832e1864308152df |