xref: /openbmc/bmcweb/http/http2_connection.hpp (revision 83328316)
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     std::shared_ptr<Request> req = std::make_shared<Request>();
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:
HTTP2Connection(Adaptor && adaptorIn,Handler * handlerIn,std::function<std::string ()> & getCachedDateStrF)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 
start()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 
sendServerConnectionHeader()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 
fileReadCallback(nghttp2_session *,int32_t streamId,uint8_t * buf,size_t length,uint32_t * dataFlags,nghttp2_data_source *,void * userPtr)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 
headerFromStringViews(std::string_view name,std::string_view value,uint8_t flags)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 
sendResponse(Response & completedRes,int32_t streamId)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         crow::Request& thisReq = *stream.req;
174 
175         completeResponseFields(thisReq, res);
176         res.addHeader(boost::beast::http::field::date, getCachedDateStr());
177         res.preparePayload();
178 
179         boost::beast::http::fields& fields = res.fields();
180         std::string code = std::to_string(res.resultInt());
181         std::vector<nghttp2_nv> hdr;
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         http::response<bmcweb::HttpBody>& fbody = res.response;
190         stream.writer.emplace(fbody.base(), fbody.body());
191 
192         nghttp2_data_provider dataPrd{
193             .source = {.fd = 0},
194             .read_callback = fileReadCallback,
195         };
196 
197         int rv = ngSession.submitResponse(streamId, hdr, &dataPrd);
198         if (rv != 0)
199         {
200             BMCWEB_LOG_ERROR("Fatal error: {}", nghttp2_strerror(rv));
201             close();
202             return -1;
203         }
204         writeBuffer();
205 
206         return 0;
207     }
208 
initializeNghttp2Session()209     nghttp2_session initializeNghttp2Session()
210     {
211         nghttp2_session_callbacks callbacks;
212         callbacks.setOnFrameRecvCallback(onFrameRecvCallbackStatic);
213         callbacks.setOnStreamCloseCallback(onStreamCloseCallbackStatic);
214         callbacks.setOnHeaderCallback(onHeaderCallbackStatic);
215         callbacks.setOnBeginHeadersCallback(onBeginHeadersCallbackStatic);
216         callbacks.setOnDataChunkRecvCallback(onDataChunkRecvStatic);
217 
218         nghttp2_session session(callbacks);
219         session.setUserData(this);
220 
221         return session;
222     }
223 
onRequestRecv(int32_t streamId)224     int onRequestRecv(int32_t streamId)
225     {
226         BMCWEB_LOG_DEBUG("on_request_recv");
227 
228         auto it = streams.find(streamId);
229         if (it == streams.end())
230         {
231             close();
232             return -1;
233         }
234         auto& reqReader = it->second.reqReader;
235         if (reqReader)
236         {
237             boost::beast::error_code ec;
238             reqReader->finish(ec);
239             if (ec)
240             {
241                 BMCWEB_LOG_CRITICAL("Failed to finalize payload");
242                 close();
243                 return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
244             }
245         }
246         crow::Request& thisReq = *it->second.req;
247         thisReq.ioService = static_cast<decltype(thisReq.ioService)>(
248             &adaptor.get_executor().context());
249         BMCWEB_LOG_DEBUG("Handling {} \"{}\"", logPtr(&thisReq),
250                          thisReq.url().encoded_path());
251 
252         crow::Response& thisRes = it->second.res;
253 
254         thisRes.setCompleteRequestHandler(
255             [this, streamId](Response& completeRes) {
256             BMCWEB_LOG_DEBUG("res.completeRequestHandler called");
257             if (sendResponse(completeRes, streamId) != 0)
258             {
259                 close();
260                 return;
261             }
262         });
263         auto asyncResp =
264             std::make_shared<bmcweb::AsyncResp>(std::move(it->second.res));
265         if constexpr (!BMCWEB_INSECURE_DISABLE_AUTH)
266         {
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                 return 0;
279             }
280         }
281         std::string_view expected =
282             thisReq.getHeaderValue(boost::beast::http::field::if_none_match);
283         BMCWEB_LOG_DEBUG("Setting expected hash {}", expected);
284         if (!expected.empty())
285         {
286             asyncResp->res.setExpectedHash(expected);
287         }
288         handler->handle(it->second.req, asyncResp);
289         return 0;
290     }
291 
onDataChunkRecvCallback(uint8_t,int32_t streamId,const uint8_t * data,size_t len)292     int onDataChunkRecvCallback(uint8_t /*flags*/, int32_t streamId,
293                                 const uint8_t* data, size_t len)
294     {
295         auto thisStream = streams.find(streamId);
296         if (thisStream == streams.end())
297         {
298             BMCWEB_LOG_ERROR("Unknown stream{}", streamId);
299             close();
300             return -1;
301         }
302 
303         std::optional<bmcweb::HttpBody::reader>& reqReader =
304             thisStream->second.reqReader;
305         if (!reqReader)
306         {
307             reqReader.emplace(
308                 bmcweb::HttpBody::reader(thisStream->second.req->req.base(),
309                                          thisStream->second.req->req.body()));
310         }
311         boost::beast::error_code ec;
312         reqReader->put(boost::asio::const_buffer(data, len), ec);
313         if (ec)
314         {
315             BMCWEB_LOG_CRITICAL("Failed to write payload");
316             return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
317         }
318         return 0;
319     }
320 
onDataChunkRecvStatic(nghttp2_session *,uint8_t flags,int32_t streamId,const uint8_t * data,size_t len,void * userData)321     static int onDataChunkRecvStatic(nghttp2_session* /* session */,
322                                      uint8_t flags, int32_t streamId,
323                                      const uint8_t* data, size_t len,
324                                      void* userData)
325     {
326         BMCWEB_LOG_DEBUG("on_frame_recv_callback");
327         if (userData == nullptr)
328         {
329             BMCWEB_LOG_CRITICAL("user data was null?");
330             return NGHTTP2_ERR_CALLBACK_FAILURE;
331         }
332         return userPtrToSelf(userData).onDataChunkRecvCallback(flags, streamId,
333                                                                data, len);
334     }
335 
onFrameRecvCallback(const nghttp2_frame & frame)336     int onFrameRecvCallback(const nghttp2_frame& frame)
337     {
338         BMCWEB_LOG_DEBUG("frame type {}", static_cast<int>(frame.hd.type));
339         switch (frame.hd.type)
340         {
341             case NGHTTP2_DATA:
342             case NGHTTP2_HEADERS:
343                 // Check that the client request has finished
344                 if ((frame.hd.flags & NGHTTP2_FLAG_END_STREAM) != 0)
345                 {
346                     return onRequestRecv(frame.hd.stream_id);
347                 }
348                 break;
349             default:
350                 break;
351         }
352         return 0;
353     }
354 
onFrameRecvCallbackStatic(nghttp2_session *,const nghttp2_frame * frame,void * userData)355     static int onFrameRecvCallbackStatic(nghttp2_session* /* session */,
356                                          const nghttp2_frame* frame,
357                                          void* userData)
358     {
359         BMCWEB_LOG_DEBUG("on_frame_recv_callback");
360         if (userData == nullptr)
361         {
362             BMCWEB_LOG_CRITICAL("user data was null?");
363             return NGHTTP2_ERR_CALLBACK_FAILURE;
364         }
365         if (frame == nullptr)
366         {
367             BMCWEB_LOG_CRITICAL("frame was null?");
368             return NGHTTP2_ERR_CALLBACK_FAILURE;
369         }
370         return userPtrToSelf(userData).onFrameRecvCallback(*frame);
371     }
372 
userPtrToSelf(void * userData)373     static self_type& userPtrToSelf(void* userData)
374     {
375         // This method exists to keep the unsafe reinterpret cast in one
376         // place.
377         // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
378         return *reinterpret_cast<self_type*>(userData);
379     }
380 
onStreamCloseCallbackStatic(nghttp2_session *,int32_t streamId,uint32_t,void * userData)381     static int onStreamCloseCallbackStatic(nghttp2_session* /* session */,
382                                            int32_t streamId,
383                                            uint32_t /*unused*/, void* userData)
384     {
385         BMCWEB_LOG_DEBUG("on_stream_close_callback stream {}", streamId);
386         if (userData == nullptr)
387         {
388             BMCWEB_LOG_CRITICAL("user data was null?");
389             return NGHTTP2_ERR_CALLBACK_FAILURE;
390         }
391         if (userPtrToSelf(userData).streams.erase(streamId) <= 0)
392         {
393             return -1;
394         }
395         return 0;
396     }
397 
onHeaderCallback(const nghttp2_frame & frame,std::span<const uint8_t> name,std::span<const uint8_t> value)398     int onHeaderCallback(const nghttp2_frame& frame,
399                          std::span<const uint8_t> name,
400                          std::span<const uint8_t> value)
401     {
402         // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
403         std::string_view nameSv(reinterpret_cast<const char*>(name.data()),
404                                 name.size());
405         // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
406         std::string_view valueSv(reinterpret_cast<const char*>(value.data()),
407                                  value.size());
408 
409         BMCWEB_LOG_DEBUG("on_header_callback name: {} value {}", nameSv,
410                          valueSv);
411         if (frame.hd.type != NGHTTP2_HEADERS)
412         {
413             return 0;
414         }
415         if (frame.headers.cat != NGHTTP2_HCAT_REQUEST)
416         {
417             return 0;
418         }
419         auto thisStream = streams.find(frame.hd.stream_id);
420         if (thisStream == streams.end())
421         {
422             BMCWEB_LOG_ERROR("Unknown stream{}", frame.hd.stream_id);
423             close();
424             return -1;
425         }
426 
427         crow::Request& thisReq = *thisStream->second.req;
428 
429         if (nameSv == ":path")
430         {
431             thisReq.target(valueSv);
432         }
433         else if (nameSv == ":method")
434         {
435             boost::beast::http::verb verb =
436                 boost::beast::http::string_to_verb(valueSv);
437             if (verb == boost::beast::http::verb::unknown)
438             {
439                 BMCWEB_LOG_ERROR("Unknown http verb {}", valueSv);
440                 close();
441                 return -1;
442             }
443             thisReq.method(verb);
444         }
445         else if (nameSv == ":scheme")
446         {
447             // Nothing to check on scheme
448         }
449         else
450         {
451             thisReq.addHeader(nameSv, valueSv);
452         }
453         return 0;
454     }
455 
onHeaderCallbackStatic(nghttp2_session *,const nghttp2_frame * frame,const uint8_t * name,size_t namelen,const uint8_t * value,size_t vallen,uint8_t,void * userData)456     static int onHeaderCallbackStatic(nghttp2_session* /* session */,
457                                       const nghttp2_frame* frame,
458                                       const uint8_t* name, size_t namelen,
459                                       const uint8_t* value, size_t vallen,
460                                       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 
onBeginHeadersCallback(const nghttp2_frame & frame)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             Http2StreamData& stream = streams[frame.hd.stream_id];
494             // http2 is by definition always tls
495             stream.req->isSecure = true;
496         }
497         return 0;
498     }
499 
onBeginHeadersCallbackStatic(nghttp2_session *,const nghttp2_frame * frame,void * userData)500     static int onBeginHeadersCallbackStatic(nghttp2_session* /* session */,
501                                             const nghttp2_frame* frame,
502                                             void* userData)
503     {
504         BMCWEB_LOG_DEBUG("on_begin_headers_callback");
505         if (userData == nullptr)
506         {
507             BMCWEB_LOG_CRITICAL("user data was null?");
508             return NGHTTP2_ERR_CALLBACK_FAILURE;
509         }
510         if (frame == nullptr)
511         {
512             BMCWEB_LOG_CRITICAL("frame was null?");
513             return NGHTTP2_ERR_CALLBACK_FAILURE;
514         }
515         return userPtrToSelf(userData).onBeginHeadersCallback(*frame);
516     }
517 
afterWriteBuffer(const std::shared_ptr<self_type> & self,const boost::system::error_code & ec,size_t sendLength)518     static void afterWriteBuffer(const std::shared_ptr<self_type>& self,
519                                  const boost::system::error_code& ec,
520                                  size_t sendLength)
521     {
522         self->isWriting = false;
523         BMCWEB_LOG_DEBUG("Sent {}", sendLength);
524         if (ec)
525         {
526             self->close();
527             return;
528         }
529         self->writeBuffer();
530     }
531 
writeBuffer()532     void writeBuffer()
533     {
534         if (isWriting)
535         {
536             return;
537         }
538         std::span<const uint8_t> data = ngSession.memSend();
539         if (data.empty())
540         {
541             return;
542         }
543         isWriting = true;
544         boost::asio::async_write(
545             adaptor, boost::asio::const_buffer(data.data(), data.size()),
546             std::bind_front(afterWriteBuffer, shared_from_this()));
547     }
548 
close()549     void close()
550     {
551         if constexpr (std::is_same_v<Adaptor,
552                                      boost::asio::ssl::stream<
553                                          boost::asio::ip::tcp::socket>>)
554         {
555             adaptor.next_layer().close();
556         }
557         else
558         {
559             adaptor.close();
560         }
561     }
562 
afterDoRead(const std::shared_ptr<self_type> &,const boost::system::error_code & ec,size_t bytesTransferred)563     void afterDoRead(const std::shared_ptr<self_type>& /*self*/,
564                      const boost::system::error_code& ec,
565                      size_t bytesTransferred)
566     {
567         BMCWEB_LOG_DEBUG("{} async_read_some {} Bytes", logPtr(this),
568                          bytesTransferred);
569 
570         if (ec)
571         {
572             BMCWEB_LOG_ERROR("{} Error while reading: {}", logPtr(this),
573                              ec.message());
574             close();
575             BMCWEB_LOG_DEBUG("{} from read(1)", logPtr(this));
576             return;
577         }
578         std::span<uint8_t> bufferSpan{inBuffer.data(), bytesTransferred};
579 
580         ssize_t readLen = ngSession.memRecv(bufferSpan);
581         if (readLen < 0)
582         {
583             BMCWEB_LOG_ERROR("nghttp2_session_mem_recv returned {}", readLen);
584             close();
585             return;
586         }
587         writeBuffer();
588 
589         doRead();
590     }
591 
doRead()592     void doRead()
593     {
594         BMCWEB_LOG_DEBUG("{} doRead", logPtr(this));
595         adaptor.async_read_some(
596             boost::asio::buffer(inBuffer),
597             std::bind_front(&self_type::afterDoRead, this, shared_from_this()));
598     }
599 
600     // A mapping from http2 stream ID to Stream Data
601     std::map<int32_t, Http2StreamData> streams;
602 
603     std::array<uint8_t, 8192> inBuffer{};
604 
605     Adaptor adaptor;
606     bool isWriting = false;
607 
608     nghttp2_session ngSession;
609 
610     Handler* handler;
611     std::function<std::string()>& getCachedDateStr;
612 
613     using std::enable_shared_from_this<
614         HTTP2Connection<Adaptor, Handler>>::shared_from_this;
615 
616     using std::enable_shared_from_this<
617         HTTP2Connection<Adaptor, Handler>>::weak_from_this;
618 };
619 } // namespace crow
620