xref: /openbmc/bmcweb/http/http2_connection.hpp (revision bd79bce8c3f1deb1fb2773868b9ece25233cf27b)
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 <string>
34 #include <vector>
35 
36 namespace crow
37 {
38 
39 struct Http2StreamData
40 {
41     std::shared_ptr<Request> req = std::make_shared<Request>();
42     std::optional<bmcweb::HttpBody::reader> reqReader;
43     std::string accept;
44     Response res;
45     std::optional<bmcweb::HttpBody::writer> writer;
46 };
47 
48 template <typename Adaptor, typename Handler>
49 class HTTP2Connection :
50     public std::enable_shared_from_this<HTTP2Connection<Adaptor, Handler>>
51 {
52     using self_type = HTTP2Connection<Adaptor, Handler>;
53 
54   public:
55     HTTP2Connection(Adaptor&& adaptorIn, Handler* handlerIn,
56                     std::function<std::string()>& getCachedDateStrF) :
57         adaptor(std::move(adaptorIn)), ngSession(initializeNghttp2Session()),
58         handler(handlerIn), 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
93         fileReadCallback(nghttp2_session* /* session */, int32_t streamId,
94                          uint8_t* buf, size_t length, uint32_t* dataFlags,
95                          nghttp2_data_source* /*source*/, 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         Http2StreamData& stream = it->second;
171         Response& res = stream.res;
172         res = std::move(completedRes);
173 
174         completeResponseFields(stream.accept, res);
175         res.addHeader(boost::beast::http::field::date, getCachedDateStr());
176         res.preparePayload();
177 
178         boost::beast::http::fields& fields = res.fields();
179         std::string code = std::to_string(res.resultInt());
180         std::vector<nghttp2_nv> hdr;
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         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         auto& reqReader = it->second.reqReader;
234         if (reqReader)
235         {
236             boost::beast::error_code ec;
237             reqReader->finish(ec);
238             if (ec)
239             {
240                 BMCWEB_LOG_CRITICAL("Failed to finalize payload");
241                 close();
242                 return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
243             }
244         }
245         crow::Request& thisReq = *it->second.req;
246         thisReq.ioService = static_cast<decltype(thisReq.ioService)>(
247             &adaptor.get_executor().context());
248 
249         it->second.accept = thisReq.getHeaderValue("Accept");
250 
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         if constexpr (!BMCWEB_INSECURE_DISABLE_AUTH)
268         {
269             thisReq.session = crow::authentication::authenticate(
270                 {}, asyncResp->res, thisReq.method(), thisReq.req, nullptr);
271             if (!crow::authentication::isOnAllowlist(thisReq.url().path(),
272                                                      thisReq.method()) &&
273                 thisReq.session == nullptr)
274             {
275                 BMCWEB_LOG_WARNING("Authentication failed");
276                 forward_unauthorized::sendUnauthorized(
277                     thisReq.url().encoded_path(),
278                     thisReq.getHeaderValue("X-Requested-With"),
279                     thisReq.getHeaderValue("Accept"), asyncResp->res);
280                 return 0;
281             }
282         }
283         std::string_view expected =
284             thisReq.getHeaderValue(boost::beast::http::field::if_none_match);
285         BMCWEB_LOG_DEBUG("Setting expected hash {}", expected);
286         if (!expected.empty())
287         {
288             asyncResp->res.setExpectedHash(expected);
289         }
290         handler->handle(it->second.req, asyncResp);
291         return 0;
292     }
293 
294     int onDataChunkRecvCallback(uint8_t /*flags*/, int32_t streamId,
295                                 const uint8_t* data, size_t len)
296     {
297         auto thisStream = streams.find(streamId);
298         if (thisStream == streams.end())
299         {
300             BMCWEB_LOG_ERROR("Unknown stream{}", streamId);
301             close();
302             return -1;
303         }
304 
305         std::optional<bmcweb::HttpBody::reader>& reqReader =
306             thisStream->second.reqReader;
307         if (!reqReader)
308         {
309             reqReader.emplace(
310                 bmcweb::HttpBody::reader(thisStream->second.req->req.base(),
311                                          thisStream->second.req->req.body()));
312         }
313         boost::beast::error_code ec;
314         reqReader->put(boost::asio::const_buffer(data, len), ec);
315         if (ec)
316         {
317             BMCWEB_LOG_CRITICAL("Failed to write payload");
318             return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
319         }
320         return 0;
321     }
322 
323     static int onDataChunkRecvStatic(
324         nghttp2_session* /* session */, uint8_t flags, int32_t streamId,
325         const uint8_t* data, size_t len, void* userData)
326     {
327         BMCWEB_LOG_DEBUG("on_frame_recv_callback");
328         if (userData == nullptr)
329         {
330             BMCWEB_LOG_CRITICAL("user data was null?");
331             return NGHTTP2_ERR_CALLBACK_FAILURE;
332         }
333         return userPtrToSelf(userData).onDataChunkRecvCallback(
334             flags, streamId, data, len);
335     }
336 
337     int onFrameRecvCallback(const nghttp2_frame& frame)
338     {
339         BMCWEB_LOG_DEBUG("frame type {}", static_cast<int>(frame.hd.type));
340         switch (frame.hd.type)
341         {
342             case NGHTTP2_DATA:
343             case NGHTTP2_HEADERS:
344                 // Check that the client request has finished
345                 if ((frame.hd.flags & NGHTTP2_FLAG_END_STREAM) != 0)
346                 {
347                     return onRequestRecv(frame.hd.stream_id);
348                 }
349                 break;
350             default:
351                 break;
352         }
353         return 0;
354     }
355 
356     static int onFrameRecvCallbackStatic(nghttp2_session* /* session */,
357                                          const nghttp2_frame* frame,
358                                          void* userData)
359     {
360         BMCWEB_LOG_DEBUG("on_frame_recv_callback");
361         if (userData == nullptr)
362         {
363             BMCWEB_LOG_CRITICAL("user data was null?");
364             return NGHTTP2_ERR_CALLBACK_FAILURE;
365         }
366         if (frame == nullptr)
367         {
368             BMCWEB_LOG_CRITICAL("frame was null?");
369             return NGHTTP2_ERR_CALLBACK_FAILURE;
370         }
371         return userPtrToSelf(userData).onFrameRecvCallback(*frame);
372     }
373 
374     static self_type& userPtrToSelf(void* userData)
375     {
376         // This method exists to keep the unsafe reinterpret cast in one
377         // place.
378         // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
379         return *reinterpret_cast<self_type*>(userData);
380     }
381 
382     static int onStreamCloseCallbackStatic(nghttp2_session* /* session */,
383                                            int32_t streamId,
384                                            uint32_t /*unused*/, void* userData)
385     {
386         BMCWEB_LOG_DEBUG("on_stream_close_callback stream {}", streamId);
387         if (userData == nullptr)
388         {
389             BMCWEB_LOG_CRITICAL("user data was null?");
390             return NGHTTP2_ERR_CALLBACK_FAILURE;
391         }
392         if (userPtrToSelf(userData).streams.erase(streamId) <= 0)
393         {
394             return -1;
395         }
396         return 0;
397     }
398 
399     int onHeaderCallback(const nghttp2_frame& frame,
400                          std::span<const uint8_t> name,
401                          std::span<const uint8_t> value)
402     {
403         // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
404         std::string_view nameSv(reinterpret_cast<const char*>(name.data()),
405                                 name.size());
406         // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
407         std::string_view valueSv(reinterpret_cast<const char*>(value.data()),
408                                  value.size());
409 
410         BMCWEB_LOG_DEBUG("on_header_callback name: {} value {}", nameSv,
411                          valueSv);
412         if (frame.hd.type != NGHTTP2_HEADERS)
413         {
414             return 0;
415         }
416         if (frame.headers.cat != NGHTTP2_HCAT_REQUEST)
417         {
418             return 0;
419         }
420         auto thisStream = streams.find(frame.hd.stream_id);
421         if (thisStream == streams.end())
422         {
423             BMCWEB_LOG_ERROR("Unknown stream{}", frame.hd.stream_id);
424             close();
425             return -1;
426         }
427 
428         crow::Request& thisReq = *thisStream->second.req;
429 
430         if (nameSv == ":path")
431         {
432             thisReq.target(valueSv);
433         }
434         else if (nameSv == ":method")
435         {
436             boost::beast::http::verb verb =
437                 boost::beast::http::string_to_verb(valueSv);
438             if (verb == boost::beast::http::verb::unknown)
439             {
440                 BMCWEB_LOG_ERROR("Unknown http verb {}", valueSv);
441                 close();
442                 return -1;
443             }
444             thisReq.method(verb);
445         }
446         else if (nameSv == ":scheme")
447         {
448             // Nothing to check on scheme
449         }
450         else
451         {
452             thisReq.addHeader(nameSv, valueSv);
453         }
454         return 0;
455     }
456 
457     static int onHeaderCallbackStatic(
458         nghttp2_session* /* session */, const nghttp2_frame* frame,
459         const uint8_t* name, size_t namelen, const uint8_t* value,
460         size_t vallen, uint8_t /* flags */, void* userData)
461     {
462         if (userData == nullptr)
463         {
464             BMCWEB_LOG_CRITICAL("user data was null?");
465             return NGHTTP2_ERR_CALLBACK_FAILURE;
466         }
467         if (frame == nullptr)
468         {
469             BMCWEB_LOG_CRITICAL("frame was null?");
470             return NGHTTP2_ERR_CALLBACK_FAILURE;
471         }
472         if (name == nullptr)
473         {
474             BMCWEB_LOG_CRITICAL("name was null?");
475             return NGHTTP2_ERR_CALLBACK_FAILURE;
476         }
477         if (value == nullptr)
478         {
479             BMCWEB_LOG_CRITICAL("value was null?");
480             return NGHTTP2_ERR_CALLBACK_FAILURE;
481         }
482         return userPtrToSelf(userData).onHeaderCallback(*frame, {name, namelen},
483                                                         {value, vallen});
484     }
485 
486     int onBeginHeadersCallback(const nghttp2_frame& frame)
487     {
488         if (frame.hd.type == NGHTTP2_HEADERS &&
489             frame.headers.cat == NGHTTP2_HCAT_REQUEST)
490         {
491             BMCWEB_LOG_DEBUG("create stream for id {}", frame.hd.stream_id);
492 
493             streams.emplace(frame.hd.stream_id, Http2StreamData());
494         }
495         return 0;
496     }
497 
498     static int onBeginHeadersCallbackStatic(nghttp2_session* /* session */,
499                                             const nghttp2_frame* frame,
500                                             void* userData)
501     {
502         BMCWEB_LOG_DEBUG("on_begin_headers_callback");
503         if (userData == nullptr)
504         {
505             BMCWEB_LOG_CRITICAL("user data was null?");
506             return NGHTTP2_ERR_CALLBACK_FAILURE;
507         }
508         if (frame == nullptr)
509         {
510             BMCWEB_LOG_CRITICAL("frame was null?");
511             return NGHTTP2_ERR_CALLBACK_FAILURE;
512         }
513         return userPtrToSelf(userData).onBeginHeadersCallback(*frame);
514     }
515 
516     static void afterWriteBuffer(const std::shared_ptr<self_type>& self,
517                                  const boost::system::error_code& ec,
518                                  size_t sendLength)
519     {
520         self->isWriting = false;
521         BMCWEB_LOG_DEBUG("Sent {}", sendLength);
522         if (ec)
523         {
524             self->close();
525             return;
526         }
527         self->writeBuffer();
528     }
529 
530     void writeBuffer()
531     {
532         if (isWriting)
533         {
534             return;
535         }
536         std::span<const uint8_t> data = ngSession.memSend();
537         if (data.empty())
538         {
539             return;
540         }
541         isWriting = true;
542         boost::asio::async_write(
543             adaptor, boost::asio::const_buffer(data.data(), data.size()),
544             std::bind_front(afterWriteBuffer, shared_from_this()));
545     }
546 
547     void close()
548     {
549         if constexpr (std::is_same_v<Adaptor,
550                                      boost::asio::ssl::stream<
551                                          boost::asio::ip::tcp::socket>>)
552         {
553             adaptor.next_layer().close();
554         }
555         else
556         {
557             adaptor.close();
558         }
559     }
560 
561     void afterDoRead(const std::shared_ptr<self_type>& /*self*/,
562                      const boost::system::error_code& ec,
563                      size_t bytesTransferred)
564     {
565         BMCWEB_LOG_DEBUG("{} async_read_some {} Bytes", logPtr(this),
566                          bytesTransferred);
567 
568         if (ec)
569         {
570             BMCWEB_LOG_ERROR("{} Error while reading: {}", logPtr(this),
571                              ec.message());
572             close();
573             BMCWEB_LOG_DEBUG("{} from read(1)", logPtr(this));
574             return;
575         }
576         std::span<uint8_t> bufferSpan{inBuffer.data(), bytesTransferred};
577 
578         ssize_t readLen = ngSession.memRecv(bufferSpan);
579         if (readLen < 0)
580         {
581             BMCWEB_LOG_ERROR("nghttp2_session_mem_recv returned {}", readLen);
582             close();
583             return;
584         }
585         writeBuffer();
586 
587         doRead();
588     }
589 
590     void doRead()
591     {
592         BMCWEB_LOG_DEBUG("{} doRead", logPtr(this));
593         adaptor.async_read_some(
594             boost::asio::buffer(inBuffer),
595             std::bind_front(&self_type::afterDoRead, this, shared_from_this()));
596     }
597 
598     // A mapping from http2 stream ID to Stream Data
599     std::map<int32_t, Http2StreamData> streams;
600 
601     std::array<uint8_t, 8192> inBuffer{};
602 
603     Adaptor adaptor;
604     bool isWriting = false;
605 
606     nghttp2_session ngSession;
607 
608     Handler* handler;
609     std::function<std::string()>& getCachedDateStr;
610 
611     using std::enable_shared_from_this<
612         HTTP2Connection<Adaptor, Handler>>::shared_from_this;
613 
614     using std::enable_shared_from_this<
615         HTTP2Connection<Adaptor, Handler>>::weak_from_this;
616 };
617 } // namespace crow
618