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