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