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