summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/bmcweb_config.h.in2
-rw-r--r--config/meson.build2
-rw-r--r--http/http2_connection.hpp555
-rw-r--r--http/http_connection.hpp31
-rw-r--r--http/http_request.hpp2
-rw-r--r--http/nghttp2_adapters.hpp152
-rw-r--r--include/ssl_key_handler.hpp42
-rw-r--r--meson.build13
-rw-r--r--meson_options.txt9
-rw-r--r--subprojects/nghttp2.wrap5
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