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