xref: /openbmc/bmcweb/http/http2_connection.hpp (revision f2caadce)
1 #pragma once
2 #include "bmcweb_config.h"
3 
4 #include "async_resp.hpp"
5 #include "authentication.hpp"
6 #include "complete_response_fields.hpp"
7 #include "http_body.hpp"
8 #include "http_response.hpp"
9 #include "http_utility.hpp"
10 #include "logging.hpp"
11 #include "mutual_tls.hpp"
12 #include "nghttp2_adapters.hpp"
13 #include "ssl_key_handler.hpp"
14 #include "utility.hpp"
15 
16 #include <boost/asio/io_context.hpp>
17 #include <boost/asio/ip/tcp.hpp>
18 #include <boost/asio/ssl/stream.hpp>
19 #include <boost/asio/steady_timer.hpp>
20 #include <boost/beast/http/error.hpp>
21 #include <boost/beast/http/parser.hpp>
22 #include <boost/beast/http/read.hpp>
23 #include <boost/beast/http/serializer.hpp>
24 #include <boost/beast/http/write.hpp>
25 #include <boost/beast/ssl/ssl_stream.hpp>
26 #include <boost/beast/websocket.hpp>
27 #include <boost/system/error_code.hpp>
28 
29 #include <array>
30 #include <atomic>
31 #include <chrono>
32 #include <functional>
33 #include <memory>
34 #include <vector>
35 
36 namespace crow
37 {
38 
39 struct Http2StreamData
40 {
41     Request req;
42     std::optional<bmcweb::HttpBody::reader> reqReader;
43     Response res;
44     std::optional<bmcweb::HttpBody::writer> writer;
45 };
46 
47 template <typename Adaptor, typename Handler>
48 class HTTP2Connection :
49     public std::enable_shared_from_this<HTTP2Connection<Adaptor, Handler>>
50 {
51     using self_type = HTTP2Connection<Adaptor, Handler>;
52 
53   public:
54     HTTP2Connection(Adaptor&& adaptorIn, Handler* handlerIn,
55                     std::function<std::string()>& getCachedDateStrF) :
56         adaptor(std::move(adaptorIn)),
57         ngSession(initializeNghttp2Session()), handler(handlerIn),
58         getCachedDateStr(getCachedDateStrF)
59     {}
60 
61     void start()
62     {
63         // Create the control stream
64         streams[0];
65 
66         if (sendServerConnectionHeader() != 0)
67         {
68             BMCWEB_LOG_ERROR("send_server_connection_header failed");
69             return;
70         }
71         doRead();
72     }
73 
74     int sendServerConnectionHeader()
75     {
76         BMCWEB_LOG_DEBUG("send_server_connection_header()");
77 
78         uint32_t maxStreams = 4;
79         std::array<nghttp2_settings_entry, 2> iv = {
80             {{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, maxStreams},
81              {NGHTTP2_SETTINGS_ENABLE_PUSH, 0}}};
82         int rv = ngSession.submitSettings(iv);
83         if (rv != 0)
84         {
85             BMCWEB_LOG_ERROR("Fatal error: {}", nghttp2_strerror(rv));
86             return -1;
87         }
88         writeBuffer();
89         return 0;
90     }
91 
92     static ssize_t fileReadCallback(nghttp2_session* /* session */,
93                                     int32_t streamId, uint8_t* buf,
94                                     size_t length, uint32_t* dataFlags,
95                                     nghttp2_data_source* /*source*/,
96                                     void* userPtr)
97     {
98         self_type& self = userPtrToSelf(userPtr);
99 
100         auto streamIt = self.streams.find(streamId);
101         if (streamIt == self.streams.end())
102         {
103             return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
104         }
105         Http2StreamData& stream = streamIt->second;
106         BMCWEB_LOG_DEBUG("File read callback length: {}", length);
107         if (!stream.writer)
108         {
109             return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
110         }
111         boost::beast::error_code ec;
112         boost::optional<std::pair<boost::asio::const_buffer, bool>> out =
113             stream.writer->getWithMaxSize(ec, length);
114         if (ec)
115         {
116             BMCWEB_LOG_CRITICAL("Failed to get buffer");
117             return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
118         }
119         if (!out)
120         {
121             BMCWEB_LOG_ERROR("Empty file, setting EOF");
122             *dataFlags |= NGHTTP2_DATA_FLAG_EOF;
123             return 0;
124         }
125 
126         BMCWEB_LOG_DEBUG("Send chunk of size: {}", out->first.size());
127         if (length < out->first.size())
128         {
129             BMCWEB_LOG_CRITICAL(
130                 "Buffer overflow that should never happen happened");
131             // Should never happen because of length limit on get() above
132             return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
133         }
134         boost::asio::mutable_buffer writeableBuf(buf, length);
135         BMCWEB_LOG_DEBUG("Copying {} bytes to buf", out->first.size());
136         size_t copied = boost::asio::buffer_copy(writeableBuf, out->first);
137         if (copied != out->first.size())
138         {
139             BMCWEB_LOG_ERROR(
140                 "Couldn't copy all {} bytes into buffer, only copied {}",
141                 out->first.size(), copied);
142             return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
143         }
144 
145         if (!out->second)
146         {
147             BMCWEB_LOG_DEBUG("Setting EOF flag");
148             *dataFlags |= NGHTTP2_DATA_FLAG_EOF;
149         }
150         return static_cast<ssize_t>(copied);
151     }
152 
153     nghttp2_nv headerFromStringViews(std::string_view name,
154                                      std::string_view value, uint8_t flags)
155     {
156         uint8_t* nameData = std::bit_cast<uint8_t*>(name.data());
157         uint8_t* valueData = std::bit_cast<uint8_t*>(value.data());
158         return {nameData, valueData, name.size(), value.size(), flags};
159     }
160 
161     int sendResponse(Response& completedRes, int32_t streamId)
162     {
163         BMCWEB_LOG_DEBUG("send_response stream_id:{}", streamId);
164 
165         auto it = streams.find(streamId);
166         if (it == streams.end())
167         {
168             close();
169             return -1;
170         }
171         Response& thisRes = it->second.res;
172         thisRes = std::move(completedRes);
173         crow::Request& thisReq = it->second.req;
174         std::vector<nghttp2_nv> hdr;
175 
176         completeResponseFields(thisReq, thisRes);
177         thisRes.addHeader(boost::beast::http::field::date, getCachedDateStr());
178         thisRes.preparePayload();
179 
180         boost::beast::http::fields& fields = thisRes.fields();
181         std::string code = std::to_string(thisRes.resultInt());
182         hdr.emplace_back(
183             headerFromStringViews(":status", code, NGHTTP2_NV_FLAG_NONE));
184         for (const boost::beast::http::fields::value_type& header : fields)
185         {
186             hdr.emplace_back(headerFromStringViews(
187                 header.name_string(), header.value(), NGHTTP2_NV_FLAG_NONE));
188         }
189         Http2StreamData& stream = it->second;
190         crow::Response& res = stream.res;
191         http::response<bmcweb::HttpBody>& fbody = res.response;
192         stream.writer.emplace(fbody.base(), fbody.body());
193 
194         nghttp2_data_provider dataPrd{
195             .source = {.fd = 0},
196             .read_callback = fileReadCallback,
197         };
198 
199         int rv = ngSession.submitResponse(streamId, hdr, &dataPrd);
200         if (rv != 0)
201         {
202             BMCWEB_LOG_ERROR("Fatal error: {}", nghttp2_strerror(rv));
203             close();
204             return -1;
205         }
206         writeBuffer();
207 
208         return 0;
209     }
210 
211     nghttp2_session initializeNghttp2Session()
212     {
213         nghttp2_session_callbacks callbacks;
214         callbacks.setOnFrameRecvCallback(onFrameRecvCallbackStatic);
215         callbacks.setOnStreamCloseCallback(onStreamCloseCallbackStatic);
216         callbacks.setOnHeaderCallback(onHeaderCallbackStatic);
217         callbacks.setOnBeginHeadersCallback(onBeginHeadersCallbackStatic);
218         callbacks.setOnDataChunkRecvCallback(onDataChunkRecvStatic);
219 
220         nghttp2_session session(callbacks);
221         session.setUserData(this);
222 
223         return session;
224     }
225 
226     int onRequestRecv(int32_t streamId)
227     {
228         BMCWEB_LOG_DEBUG("on_request_recv");
229 
230         auto it = streams.find(streamId);
231         if (it == streams.end())
232         {
233             close();
234             return -1;
235         }
236         auto& reqReader = it->second.reqReader;
237         if (reqReader)
238         {
239             boost::beast::error_code ec;
240             reqReader->finish(ec);
241             if (ec)
242             {
243                 BMCWEB_LOG_CRITICAL("Failed to finalize payload");
244                 close();
245                 return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
246             }
247         }
248         crow::Request& thisReq = it->second.req;
249         thisReq.ioService = static_cast<decltype(thisReq.ioService)>(
250             &adaptor.get_executor().context());
251         BMCWEB_LOG_DEBUG("Handling {} \"{}\"", logPtr(&thisReq),
252                          thisReq.url().encoded_path());
253 
254         crow::Response& thisRes = it->second.res;
255 
256         thisRes.setCompleteRequestHandler(
257             [this, streamId](Response& completeRes) {
258             BMCWEB_LOG_DEBUG("res.completeRequestHandler called");
259             if (sendResponse(completeRes, streamId) != 0)
260             {
261                 close();
262                 return;
263             }
264         });
265         auto asyncResp =
266             std::make_shared<bmcweb::AsyncResp>(std::move(it->second.res));
267 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
268         thisReq.session = crow::authentication::authenticate(
269             {}, thisRes, thisReq.method(), thisReq.req, nullptr);
270         if (!crow::authentication::isOnAllowlist(thisReq.url().path(),
271                                                  thisReq.method()) &&
272             thisReq.session == nullptr)
273         {
274             BMCWEB_LOG_WARNING("Authentication failed");
275             forward_unauthorized::sendUnauthorized(
276                 thisReq.url().encoded_path(),
277                 thisReq.getHeaderValue("X-Requested-With"),
278                 thisReq.getHeaderValue("Accept"), thisRes);
279         }
280         else
281 #endif // BMCWEB_INSECURE_DISABLE_AUTHX
282         {
283             handler->handle(thisReq, asyncResp);
284         }
285         return 0;
286     }
287 
288     int onDataChunkRecvCallback(uint8_t /*flags*/, int32_t streamId,
289                                 const uint8_t* data, size_t len)
290     {
291         auto thisStream = streams.find(streamId);
292         if (thisStream == streams.end())
293         {
294             BMCWEB_LOG_ERROR("Unknown stream{}", streamId);
295             close();
296             return -1;
297         }
298 
299         std::optional<bmcweb::HttpBody::reader>& reqReader =
300             thisStream->second.reqReader;
301         if (!reqReader)
302         {
303             reqReader.emplace(
304                 bmcweb::HttpBody::reader(thisStream->second.req.req.base(),
305                                          thisStream->second.req.req.body()));
306         }
307         boost::beast::error_code ec;
308         reqReader->put(boost::asio::const_buffer(data, len), ec);
309         if (ec)
310         {
311             BMCWEB_LOG_CRITICAL("Failed to write payload");
312             return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
313         }
314         return 0;
315     }
316 
317     static int onDataChunkRecvStatic(nghttp2_session* /* session */,
318                                      uint8_t flags, int32_t streamId,
319                                      const uint8_t* data, size_t len,
320                                      void* userData)
321     {
322         BMCWEB_LOG_DEBUG("on_frame_recv_callback");
323         if (userData == nullptr)
324         {
325             BMCWEB_LOG_CRITICAL("user data was null?");
326             return NGHTTP2_ERR_CALLBACK_FAILURE;
327         }
328         return userPtrToSelf(userData).onDataChunkRecvCallback(flags, streamId,
329                                                                data, len);
330     }
331 
332     int onFrameRecvCallback(const nghttp2_frame& frame)
333     {
334         BMCWEB_LOG_DEBUG("frame type {}", static_cast<int>(frame.hd.type));
335         switch (frame.hd.type)
336         {
337             case NGHTTP2_DATA:
338             case NGHTTP2_HEADERS:
339                 // Check that the client request has finished
340                 if ((frame.hd.flags & NGHTTP2_FLAG_END_STREAM) != 0)
341                 {
342                     return onRequestRecv(frame.hd.stream_id);
343                 }
344                 break;
345             default:
346                 break;
347         }
348         return 0;
349     }
350 
351     static int onFrameRecvCallbackStatic(nghttp2_session* /* session */,
352                                          const nghttp2_frame* frame,
353                                          void* userData)
354     {
355         BMCWEB_LOG_DEBUG("on_frame_recv_callback");
356         if (userData == nullptr)
357         {
358             BMCWEB_LOG_CRITICAL("user data was null?");
359             return NGHTTP2_ERR_CALLBACK_FAILURE;
360         }
361         if (frame == nullptr)
362         {
363             BMCWEB_LOG_CRITICAL("frame was null?");
364             return NGHTTP2_ERR_CALLBACK_FAILURE;
365         }
366         return userPtrToSelf(userData).onFrameRecvCallback(*frame);
367     }
368 
369     static self_type& userPtrToSelf(void* userData)
370     {
371         // This method exists to keep the unsafe reinterpret cast in one
372         // place.
373         // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
374         return *reinterpret_cast<self_type*>(userData);
375     }
376 
377     static int onStreamCloseCallbackStatic(nghttp2_session* /* session */,
378                                            int32_t streamId,
379                                            uint32_t /*unused*/, void* userData)
380     {
381         BMCWEB_LOG_DEBUG("on_stream_close_callback stream {}", streamId);
382         if (userData == nullptr)
383         {
384             BMCWEB_LOG_CRITICAL("user data was null?");
385             return NGHTTP2_ERR_CALLBACK_FAILURE;
386         }
387         if (userPtrToSelf(userData).streams.erase(streamId) <= 0)
388         {
389             return -1;
390         }
391         return 0;
392     }
393 
394     int onHeaderCallback(const nghttp2_frame& frame,
395                          std::span<const uint8_t> name,
396                          std::span<const uint8_t> value)
397     {
398         // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
399         std::string_view nameSv(reinterpret_cast<const char*>(name.data()),
400                                 name.size());
401         // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
402         std::string_view valueSv(reinterpret_cast<const char*>(value.data()),
403                                  value.size());
404 
405         BMCWEB_LOG_DEBUG("on_header_callback name: {} value {}", nameSv,
406                          valueSv);
407         if (frame.hd.type != NGHTTP2_HEADERS)
408         {
409             return 0;
410         }
411         if (frame.headers.cat != NGHTTP2_HCAT_REQUEST)
412         {
413             return 0;
414         }
415         auto thisStream = streams.find(frame.hd.stream_id);
416         if (thisStream == streams.end())
417         {
418             BMCWEB_LOG_ERROR("Unknown stream{}", frame.hd.stream_id);
419             close();
420             return -1;
421         }
422 
423         crow::Request& thisReq = thisStream->second.req;
424 
425         if (nameSv == ":path")
426         {
427             thisReq.target(valueSv);
428         }
429         else if (nameSv == ":method")
430         {
431             boost::beast::http::verb verb =
432                 boost::beast::http::string_to_verb(valueSv);
433             if (verb == boost::beast::http::verb::unknown)
434             {
435                 BMCWEB_LOG_ERROR("Unknown http verb {}", valueSv);
436                 close();
437                 return -1;
438             }
439             thisReq.method(verb);
440         }
441         else if (nameSv == ":scheme")
442         {
443             // Nothing to check on scheme
444         }
445         else
446         {
447             thisReq.addHeader(nameSv, valueSv);
448         }
449         return 0;
450     }
451 
452     static int onHeaderCallbackStatic(nghttp2_session* /* session */,
453                                       const nghttp2_frame* frame,
454                                       const uint8_t* name, size_t namelen,
455                                       const uint8_t* value, size_t vallen,
456                                       uint8_t /* flags */, void* userData)
457     {
458         if (userData == nullptr)
459         {
460             BMCWEB_LOG_CRITICAL("user data was null?");
461             return NGHTTP2_ERR_CALLBACK_FAILURE;
462         }
463         if (frame == nullptr)
464         {
465             BMCWEB_LOG_CRITICAL("frame was null?");
466             return NGHTTP2_ERR_CALLBACK_FAILURE;
467         }
468         if (name == nullptr)
469         {
470             BMCWEB_LOG_CRITICAL("name was null?");
471             return NGHTTP2_ERR_CALLBACK_FAILURE;
472         }
473         if (value == nullptr)
474         {
475             BMCWEB_LOG_CRITICAL("value was null?");
476             return NGHTTP2_ERR_CALLBACK_FAILURE;
477         }
478         return userPtrToSelf(userData).onHeaderCallback(*frame, {name, namelen},
479                                                         {value, vallen});
480     }
481 
482     int onBeginHeadersCallback(const nghttp2_frame& frame)
483     {
484         if (frame.hd.type == NGHTTP2_HEADERS &&
485             frame.headers.cat == NGHTTP2_HCAT_REQUEST)
486         {
487             BMCWEB_LOG_DEBUG("create stream for id {}", frame.hd.stream_id);
488 
489             Http2StreamData& stream = streams[frame.hd.stream_id];
490             // http2 is by definition always tls
491             stream.req.isSecure = true;
492         }
493         return 0;
494     }
495 
496     static int onBeginHeadersCallbackStatic(nghttp2_session* /* session */,
497                                             const nghttp2_frame* frame,
498                                             void* userData)
499     {
500         BMCWEB_LOG_DEBUG("on_begin_headers_callback");
501         if (userData == nullptr)
502         {
503             BMCWEB_LOG_CRITICAL("user data was null?");
504             return NGHTTP2_ERR_CALLBACK_FAILURE;
505         }
506         if (frame == nullptr)
507         {
508             BMCWEB_LOG_CRITICAL("frame was null?");
509             return NGHTTP2_ERR_CALLBACK_FAILURE;
510         }
511         return userPtrToSelf(userData).onBeginHeadersCallback(*frame);
512     }
513 
514     static void afterWriteBuffer(const std::shared_ptr<self_type>& self,
515                                  const boost::system::error_code& ec,
516                                  size_t sendLength)
517     {
518         self->isWriting = false;
519         BMCWEB_LOG_DEBUG("Sent {}", sendLength);
520         if (ec)
521         {
522             self->close();
523             return;
524         }
525         self->writeBuffer();
526     }
527 
528     void writeBuffer()
529     {
530         if (isWriting)
531         {
532             return;
533         }
534         std::span<const uint8_t> data = ngSession.memSend();
535         if (data.empty())
536         {
537             return;
538         }
539         isWriting = true;
540         boost::asio::async_write(
541             adaptor, boost::asio::const_buffer(data.data(), data.size()),
542             std::bind_front(afterWriteBuffer, shared_from_this()));
543     }
544 
545     void close()
546     {
547         if constexpr (std::is_same_v<Adaptor,
548                                      boost::beast::ssl_stream<
549                                          boost::asio::ip::tcp::socket>>)
550         {
551             adaptor.next_layer().close();
552         }
553         else
554         {
555             adaptor.close();
556         }
557     }
558 
559     void afterDoRead(const std::shared_ptr<self_type>& /*self*/,
560                      const boost::system::error_code& ec,
561                      size_t bytesTransferred)
562     {
563         BMCWEB_LOG_DEBUG("{} async_read_some {} Bytes", logPtr(this),
564                          bytesTransferred);
565 
566         if (ec)
567         {
568             BMCWEB_LOG_ERROR("{} Error while reading: {}", logPtr(this),
569                              ec.message());
570             close();
571             BMCWEB_LOG_DEBUG("{} from read(1)", logPtr(this));
572             return;
573         }
574         std::span<uint8_t> bufferSpan{inBuffer.data(), bytesTransferred};
575 
576         ssize_t readLen = ngSession.memRecv(bufferSpan);
577         if (readLen < 0)
578         {
579             BMCWEB_LOG_ERROR("nghttp2_session_mem_recv returned {}", readLen);
580             close();
581             return;
582         }
583         writeBuffer();
584 
585         doRead();
586     }
587 
588     void doRead()
589     {
590         BMCWEB_LOG_DEBUG("{} doRead", logPtr(this));
591         adaptor.async_read_some(
592             boost::asio::buffer(inBuffer),
593             std::bind_front(&self_type::afterDoRead, this, shared_from_this()));
594     }
595 
596     // A mapping from http2 stream ID to Stream Data
597     std::map<int32_t, Http2StreamData> streams;
598 
599     std::array<uint8_t, 8192> inBuffer{};
600 
601     Adaptor adaptor;
602     bool isWriting = false;
603 
604     nghttp2_session ngSession;
605 
606     Handler* handler;
607     std::function<std::string()>& getCachedDateStr;
608 
609     using std::enable_shared_from_this<
610         HTTP2Connection<Adaptor, Handler>>::shared_from_this;
611 
612     using std::enable_shared_from_this<
613         HTTP2Connection<Adaptor, Handler>>::weak_from_this;
614 };
615 } // namespace crow
616