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