xref: /openbmc/bmcweb/http/http_connection.hpp (revision 6fde95fa)
1 #pragma once
2 #include "bmcweb_config.h"
3 
4 #include "async_resp.hpp"
5 #include "authentication.hpp"
6 #include "http_response.hpp"
7 #include "http_utility.hpp"
8 #include "json_html_serializer.hpp"
9 #include "logging.hpp"
10 #include "mutual_tls.hpp"
11 #include "security_headers.hpp"
12 #include "ssl_key_handler.hpp"
13 #include "utility.hpp"
14 
15 #include <boost/algorithm/string/predicate.hpp>
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/core/flat_static_buffer.hpp>
21 #include <boost/beast/http/error.hpp>
22 #include <boost/beast/http/parser.hpp>
23 #include <boost/beast/http/read.hpp>
24 #include <boost/beast/http/serializer.hpp>
25 #include <boost/beast/http/write.hpp>
26 #include <boost/beast/ssl/ssl_stream.hpp>
27 #include <boost/beast/websocket.hpp>
28 
29 #include <atomic>
30 #include <chrono>
31 #include <vector>
32 
33 namespace crow
34 {
35 
36 inline void prettyPrintJson(crow::Response& res)
37 {
38     json_html_util::dumpHtml(res.body(), res.jsonValue);
39 
40     res.addHeader(boost::beast::http::field::content_type,
41                   "text/html;charset=UTF-8");
42 }
43 
44 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
45 static int connectionCount = 0;
46 
47 // request body limit size set by the bmcwebHttpReqBodyLimitMb option
48 constexpr uint64_t httpReqBodyLimit = 1024UL * 1024UL *
49                                       bmcwebHttpReqBodyLimitMb;
50 
51 constexpr uint64_t loggedOutPostBodyLimit = 4096;
52 
53 constexpr uint32_t httpHeaderLimit = 8192;
54 
55 template <typename Adaptor, typename Handler>
56 class Connection :
57     public std::enable_shared_from_this<Connection<Adaptor, Handler>>
58 {
59     using self_type = Connection<Adaptor, Handler>;
60 
61   public:
62     Connection(Handler* handlerIn, boost::asio::steady_timer&& timerIn,
63                std::function<std::string()>& getCachedDateStrF,
64                Adaptor adaptorIn) :
65         adaptor(std::move(adaptorIn)),
66         handler(handlerIn), timer(std::move(timerIn)),
67         getCachedDateStr(getCachedDateStrF)
68     {
69         parser.emplace(std::piecewise_construct, std::make_tuple());
70         parser->body_limit(httpReqBodyLimit);
71         parser->header_limit(httpHeaderLimit);
72 
73 #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
74         prepareMutualTls();
75 #endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
76 
77         connectionCount++;
78 
79         BMCWEB_LOG_DEBUG << this << " Connection open, total "
80                          << connectionCount;
81     }
82 
83     ~Connection()
84     {
85         res.setCompleteRequestHandler(nullptr);
86         cancelDeadlineTimer();
87 
88         connectionCount--;
89         BMCWEB_LOG_DEBUG << this << " Connection closed, total "
90                          << connectionCount;
91     }
92 
93     Connection(const Connection&) = delete;
94     Connection(Connection&&) = delete;
95     Connection& operator=(const Connection&) = delete;
96     Connection& operator=(Connection&&) = delete;
97 
98     bool tlsVerifyCallback(bool preverified,
99                            boost::asio::ssl::verify_context& ctx)
100     {
101         // We always return true to allow full auth flow for resources that
102         // don't require auth
103         if (preverified)
104         {
105             mtlsSession = verifyMtlsUser(req->ipAddress, ctx);
106             if (mtlsSession)
107             {
108                 BMCWEB_LOG_DEBUG
109                     << this
110                     << " Generating TLS session: " << mtlsSession->uniqueId;
111             }
112         }
113         return true;
114     }
115 
116     void prepareMutualTls()
117     {
118         std::error_code error;
119         std::filesystem::path caPath(ensuressl::trustStorePath);
120         auto caAvailable = !std::filesystem::is_empty(caPath, error);
121         caAvailable = caAvailable && !error;
122         if (caAvailable && persistent_data::SessionStore::getInstance()
123                                .getAuthMethodsConfig()
124                                .tls)
125         {
126             adaptor.set_verify_mode(boost::asio::ssl::verify_peer);
127             std::string id = "bmcweb";
128 
129             const char* cStr = id.c_str();
130             // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
131             const auto* idC = reinterpret_cast<const unsigned char*>(cStr);
132             int ret = SSL_set_session_id_context(
133                 adaptor.native_handle(), idC,
134                 static_cast<unsigned int>(id.length()));
135             if (ret == 0)
136             {
137                 BMCWEB_LOG_ERROR << this << " failed to set SSL id";
138             }
139         }
140 
141         adaptor.set_verify_callback(
142             std::bind_front(&self_type::tlsVerifyCallback, this));
143     }
144 
145     Adaptor& socket()
146     {
147         return adaptor;
148     }
149 
150     void start()
151     {
152         if (connectionCount >= 100)
153         {
154             BMCWEB_LOG_CRITICAL << this << "Max connection count exceeded.";
155             return;
156         }
157 
158         startDeadline();
159 
160         // TODO(ed) Abstract this to a more clever class with the idea of an
161         // asynchronous "start"
162         if constexpr (std::is_same_v<Adaptor,
163                                      boost::beast::ssl_stream<
164                                          boost::asio::ip::tcp::socket>>)
165         {
166             adaptor.async_handshake(boost::asio::ssl::stream_base::server,
167                                     [this, self(shared_from_this())](
168                                         const boost::system::error_code& ec) {
169                 if (ec)
170                 {
171                     return;
172                 }
173                 doReadHeaders();
174             });
175         }
176         else
177         {
178             doReadHeaders();
179         }
180     }
181 
182     void handle()
183     {
184         std::error_code reqEc;
185         crow::Request& thisReq = req.emplace(parser->release(), reqEc);
186         if (reqEc)
187         {
188             BMCWEB_LOG_DEBUG << "Request failed to construct" << reqEc;
189             res.result(boost::beast::http::status::bad_request);
190             completeRequest(res);
191             return;
192         }
193         thisReq.session = userSession;
194 
195         // Fetch the client IP address
196         readClientIp();
197 
198         // Check for HTTP version 1.1.
199         if (thisReq.version() == 11)
200         {
201             if (thisReq.getHeaderValue(boost::beast::http::field::host).empty())
202             {
203                 res.result(boost::beast::http::status::bad_request);
204                 completeRequest(res);
205                 return;
206             }
207         }
208 
209         BMCWEB_LOG_INFO << "Request: "
210                         << " " << this << " HTTP/" << thisReq.version() / 10
211                         << "." << thisReq.version() % 10 << ' '
212                         << thisReq.methodString() << " " << thisReq.target()
213                         << " " << thisReq.ipAddress.to_string();
214 
215         res.isAliveHelper = [this]() -> bool { return isAlive(); };
216 
217         thisReq.ioService = static_cast<decltype(thisReq.ioService)>(
218             &adaptor.get_executor().context());
219 
220         if (res.completed)
221         {
222             completeRequest(res);
223             return;
224         }
225         keepAlive = thisReq.keepAlive();
226 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
227         if (!crow::authentication::isOnAllowlist(req->url().buffer(),
228                                                  req->method()) &&
229             thisReq.session == nullptr)
230         {
231             BMCWEB_LOG_WARNING << "Authentication failed";
232             forward_unauthorized::sendUnauthorized(
233                 req->url().encoded_path(),
234                 req->getHeaderValue("X-Requested-With"),
235                 req->getHeaderValue("Accept"), res);
236             completeRequest(res);
237             return;
238         }
239 #endif // BMCWEB_INSECURE_DISABLE_AUTHX
240         auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
241         BMCWEB_LOG_DEBUG << "Setting completion handler";
242         asyncResp->res.setCompleteRequestHandler(
243             [self(shared_from_this())](crow::Response& thisRes) {
244             self->completeRequest(thisRes);
245         });
246         bool isSse =
247             isContentTypeAllowed(req->getHeaderValue("Accept"),
248                                  http_helpers::ContentType::EventStream, false);
249         if ((thisReq.isUpgrade() &&
250              boost::iequals(
251                  thisReq.getHeaderValue(boost::beast::http::field::upgrade),
252                  "websocket")) ||
253             isSse)
254         {
255             asyncResp->res.setCompleteRequestHandler(
256                 [self(shared_from_this())](crow::Response& thisRes) {
257                 if (thisRes.result() != boost::beast::http::status::ok)
258                 {
259                     // When any error occurs before handle upgradation,
260                     // the result in response will be set to respective
261                     // error. By default the Result will be OK (200),
262                     // which implies successful handle upgrade. Response
263                     // needs to be sent over this connection only on
264                     // failure.
265                     self->completeRequest(thisRes);
266                     return;
267                 }
268             });
269             handler->handleUpgrade(thisReq, asyncResp, std::move(adaptor));
270             return;
271         }
272         std::string_view expected =
273             req->getHeaderValue(boost::beast::http::field::if_none_match);
274         if (!expected.empty())
275         {
276             res.setExpectedHash(expected);
277         }
278         handler->handle(thisReq, asyncResp);
279     }
280 
281     bool isAlive()
282     {
283         if constexpr (std::is_same_v<Adaptor,
284                                      boost::beast::ssl_stream<
285                                          boost::asio::ip::tcp::socket>>)
286         {
287             return adaptor.next_layer().is_open();
288         }
289         else
290         {
291             return adaptor.is_open();
292         }
293     }
294     void close()
295     {
296         if constexpr (std::is_same_v<Adaptor,
297                                      boost::beast::ssl_stream<
298                                          boost::asio::ip::tcp::socket>>)
299         {
300             adaptor.next_layer().close();
301             if (mtlsSession != nullptr)
302             {
303                 BMCWEB_LOG_DEBUG
304                     << this
305                     << " Removing TLS session: " << mtlsSession->uniqueId;
306                 persistent_data::SessionStore::getInstance().removeSession(
307                     mtlsSession);
308             }
309         }
310         else
311         {
312             adaptor.close();
313         }
314     }
315 
316     void completeRequest(crow::Response& thisRes)
317     {
318         if (!req)
319         {
320             return;
321         }
322         res = std::move(thisRes);
323         res.keepAlive(keepAlive);
324 
325         BMCWEB_LOG_INFO << "Response: " << this << ' '
326                         << req->url().encoded_path() << ' ' << res.resultInt()
327                         << " keepalive=" << keepAlive;
328 
329         addSecurityHeaders(*req, res);
330 
331         crow::authentication::cleanupTempSession(*req);
332 
333         if (!isAlive())
334         {
335             // BMCWEB_LOG_DEBUG << this << " delete (socket is closed) " <<
336             // isReading
337             // << ' ' << isWriting;
338             // delete this;
339 
340             // delete lambda with self shared_ptr
341             // to enable connection destruction
342             res.setCompleteRequestHandler(nullptr);
343             return;
344         }
345 
346         res.setHashAndHandleNotModified();
347 
348         if (res.body().empty() && res.jsonValue.is_structured())
349         {
350             using http_helpers::ContentType;
351             std::array<ContentType, 3> allowed{
352                 ContentType::CBOR, ContentType::JSON, ContentType::HTML};
353             ContentType prefered =
354                 getPreferedContentType(req->getHeaderValue("Accept"), allowed);
355 
356             if (prefered == ContentType::HTML)
357             {
358                 prettyPrintJson(res);
359             }
360             else if (prefered == ContentType::CBOR)
361             {
362                 res.addHeader(boost::beast::http::field::content_type,
363                               "application/cbor");
364                 nlohmann::json::to_cbor(res.jsonValue, res.body());
365             }
366             else
367             {
368                 // Technically prefered could also be NoMatch here, but we'd
369                 // like to default to something rather than return 400 for
370                 // backward compatibility.
371                 res.addHeader(boost::beast::http::field::content_type,
372                               "application/json");
373                 res.body() = res.jsonValue.dump(
374                     2, ' ', true, nlohmann::json::error_handler_t::replace);
375             }
376         }
377 
378         if (res.resultInt() >= 400 && res.body().empty())
379         {
380             res.body() = std::string(res.reason());
381         }
382 
383         res.addHeader(boost::beast::http::field::date, getCachedDateStr());
384 
385         doWrite(res);
386 
387         // delete lambda with self shared_ptr
388         // to enable connection destruction
389         res.setCompleteRequestHandler(nullptr);
390     }
391 
392     void readClientIp()
393     {
394         boost::asio::ip::address ip;
395         boost::system::error_code ec = getClientIp(ip);
396         if (ec)
397         {
398             return;
399         }
400         req->ipAddress = ip;
401     }
402 
403     boost::system::error_code getClientIp(boost::asio::ip::address& ip)
404     {
405         boost::system::error_code ec;
406         BMCWEB_LOG_DEBUG << "Fetch the client IP address";
407         boost::asio::ip::tcp::endpoint endpoint =
408             boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec);
409 
410         if (ec)
411         {
412             // If remote endpoint fails keep going. "ClientOriginIPAddress"
413             // will be empty.
414             BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : "
415                              << ec;
416             return ec;
417         }
418         ip = endpoint.address();
419         return ec;
420     }
421 
422   private:
423     void doReadHeaders()
424     {
425         BMCWEB_LOG_DEBUG << this << " doReadHeaders";
426 
427         // Clean up any previous Connection.
428         boost::beast::http::async_read_header(
429             adaptor, buffer, *parser,
430             [this,
431              self(shared_from_this())](const boost::system::error_code& ec,
432                                        std::size_t bytesTransferred) {
433             BMCWEB_LOG_DEBUG << this << " async_read_header "
434                              << bytesTransferred << " Bytes";
435             bool errorWhileReading = false;
436             if (ec)
437             {
438                 errorWhileReading = true;
439                 if (ec == boost::beast::http::error::end_of_stream)
440                 {
441                     BMCWEB_LOG_WARNING
442                         << this << " Error while reading: " << ec.message();
443                 }
444                 else
445                 {
446                     BMCWEB_LOG_ERROR
447                         << this << " Error while reading: " << ec.message();
448                 }
449             }
450             else
451             {
452                 // if the adaptor isn't open anymore, and wasn't handed to a
453                 // websocket, treat as an error
454                 if (!isAlive() &&
455                     !boost::beast::websocket::is_upgrade(parser->get()))
456                 {
457                     errorWhileReading = true;
458                 }
459             }
460 
461             cancelDeadlineTimer();
462 
463             if (errorWhileReading)
464             {
465                 close();
466                 BMCWEB_LOG_DEBUG << this << " from read(1)";
467                 return;
468             }
469 
470             readClientIp();
471 
472             boost::asio::ip::address ip;
473             if (getClientIp(ip))
474             {
475                 BMCWEB_LOG_DEBUG << "Unable to get client IP";
476             }
477 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
478             boost::beast::http::verb method = parser->get().method();
479             userSession = crow::authentication::authenticate(
480                 ip, res, method, parser->get().base(), mtlsSession);
481 
482             bool loggedIn = userSession != nullptr;
483             if (!loggedIn)
484             {
485                 const boost::optional<uint64_t> contentLength =
486                     parser->content_length();
487                 if (contentLength && *contentLength > loggedOutPostBodyLimit)
488                 {
489                     BMCWEB_LOG_DEBUG << "Content length greater than limit "
490                                      << *contentLength;
491                     close();
492                     return;
493                 }
494 
495                 BMCWEB_LOG_DEBUG << "Starting quick deadline";
496             }
497 #endif // BMCWEB_INSECURE_DISABLE_AUTHX
498 
499             if (parser->is_done())
500             {
501                 handle();
502                 return;
503             }
504 
505             doRead();
506             });
507     }
508 
509     void doRead()
510     {
511         BMCWEB_LOG_DEBUG << this << " doRead";
512         startDeadline();
513         boost::beast::http::async_read_some(
514             adaptor, buffer, *parser,
515             [this,
516              self(shared_from_this())](const boost::system::error_code& ec,
517                                        std::size_t bytesTransferred) {
518             BMCWEB_LOG_DEBUG << this << " async_read_some " << bytesTransferred
519                              << " Bytes";
520 
521             if (ec)
522             {
523                 BMCWEB_LOG_ERROR << this
524                                  << " Error while reading: " << ec.message();
525                 close();
526                 BMCWEB_LOG_DEBUG << this << " from read(1)";
527                 return;
528             }
529 
530             // If the user is logged in, allow them to send files incrementally
531             // one piece at a time. If authentication is disabled then there is
532             // no user session hence always allow to send one piece at a time.
533             if (userSession != nullptr)
534             {
535                 cancelDeadlineTimer();
536             }
537             if (!parser->is_done())
538             {
539                 doRead();
540                 return;
541             }
542 
543             cancelDeadlineTimer();
544             handle();
545             });
546     }
547 
548     void doWrite(crow::Response& thisRes)
549     {
550         BMCWEB_LOG_DEBUG << this << " doWrite";
551         thisRes.preparePayload();
552         serializer.emplace(*thisRes.stringResponse);
553         startDeadline();
554         boost::beast::http::async_write(adaptor, *serializer,
555                                         [this, self(shared_from_this())](
556                                             const boost::system::error_code& ec,
557                                             std::size_t bytesTransferred) {
558             BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred
559                              << " bytes";
560 
561             cancelDeadlineTimer();
562 
563             if (ec)
564             {
565                 BMCWEB_LOG_DEBUG << this << " from write(2)";
566                 return;
567             }
568             if (!keepAlive)
569             {
570                 close();
571                 BMCWEB_LOG_DEBUG << this << " from write(1)";
572                 return;
573             }
574 
575             serializer.reset();
576             BMCWEB_LOG_DEBUG << this << " Clearing response";
577             res.clear();
578             parser.emplace(std::piecewise_construct, std::make_tuple());
579             parser->body_limit(httpReqBodyLimit); // reset body limit for
580                                                   // newly created parser
581             buffer.consume(buffer.size());
582 
583             userSession = nullptr;
584 
585             // Destroy the Request via the std::optional
586             req.reset();
587             doReadHeaders();
588         });
589     }
590 
591     void cancelDeadlineTimer()
592     {
593         timer.cancel();
594     }
595 
596     void startDeadline()
597     {
598         // Timer is already started so no further action is required.
599         if (timerStarted)
600         {
601             return;
602         }
603 
604         std::chrono::seconds timeout(15);
605 
606         std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this();
607         timer.expires_after(timeout);
608         timer.async_wait([weakSelf](const boost::system::error_code& ec) {
609             // Note, we are ignoring other types of errors here;  If the timer
610             // failed for any reason, we should still close the connection
611             std::shared_ptr<Connection<Adaptor, Handler>> self =
612                 weakSelf.lock();
613             if (!self)
614             {
615                 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection";
616                 return;
617             }
618 
619             self->timerStarted = false;
620 
621             if (ec == boost::asio::error::operation_aborted)
622             {
623                 // Canceled wait means the path succeeeded.
624                 return;
625             }
626             if (ec)
627             {
628                 BMCWEB_LOG_CRITICAL << self << " timer failed " << ec;
629             }
630 
631             BMCWEB_LOG_WARNING << self << "Connection timed out, closing";
632 
633             self->close();
634         });
635 
636         timerStarted = true;
637         BMCWEB_LOG_DEBUG << this << " timer started";
638     }
639 
640     Adaptor adaptor;
641     Handler* handler;
642     // Making this a std::optional allows it to be efficiently destroyed and
643     // re-created on Connection reset
644     std::optional<
645         boost::beast::http::request_parser<boost::beast::http::string_body>>
646         parser;
647 
648     boost::beast::flat_static_buffer<8192> buffer;
649 
650     std::optional<boost::beast::http::response_serializer<
651         boost::beast::http::string_body>>
652         serializer;
653 
654     std::optional<crow::Request> req;
655     crow::Response res;
656 
657     std::shared_ptr<persistent_data::UserSession> userSession;
658     std::shared_ptr<persistent_data::UserSession> mtlsSession;
659 
660     boost::asio::steady_timer timer;
661 
662     bool keepAlive = true;
663 
664     bool timerStarted = false;
665 
666     std::function<std::string()>& getCachedDateStr;
667 
668     using std::enable_shared_from_this<
669         Connection<Adaptor, Handler>>::shared_from_this;
670 
671     using std::enable_shared_from_this<
672         Connection<Adaptor, Handler>>::weak_from_this;
673 };
674 } // namespace crow
675