xref: /openbmc/bmcweb/http/http_connection.hpp (revision f263e09c)
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 
247         if (thisReq.isUpgrade() &&
248             boost::iequals(
249                 thisReq.getHeaderValue(boost::beast::http::field::upgrade),
250                 "websocket"))
251         {
252             asyncResp->res.setCompleteRequestHandler(
253                 [self(shared_from_this())](crow::Response& thisRes) {
254                 if (thisRes.result() != boost::beast::http::status::ok)
255                 {
256                     // When any error occurs before handle upgradation,
257                     // the result in response will be set to respective
258                     // error. By default the Result will be OK (200),
259                     // which implies successful handle upgrade. Response
260                     // needs to be sent over this connection only on
261                     // failure.
262                     self->completeRequest(thisRes);
263                     return;
264                 }
265             });
266             handler->handleUpgrade(thisReq, asyncResp, std::move(adaptor));
267             return;
268         }
269         std::string_view expected =
270             req->getHeaderValue(boost::beast::http::field::if_none_match);
271         if (!expected.empty())
272         {
273             res.setExpectedHash(expected);
274         }
275         handler->handle(thisReq, asyncResp);
276     }
277 
278     bool isAlive()
279     {
280         if constexpr (std::is_same_v<Adaptor,
281                                      boost::beast::ssl_stream<
282                                          boost::asio::ip::tcp::socket>>)
283         {
284             return adaptor.next_layer().is_open();
285         }
286         else
287         {
288             return adaptor.is_open();
289         }
290     }
291     void close()
292     {
293         if constexpr (std::is_same_v<Adaptor,
294                                      boost::beast::ssl_stream<
295                                          boost::asio::ip::tcp::socket>>)
296         {
297             adaptor.next_layer().close();
298             if (mtlsSession != nullptr)
299             {
300                 BMCWEB_LOG_DEBUG
301                     << this
302                     << " Removing TLS session: " << mtlsSession->uniqueId;
303                 persistent_data::SessionStore::getInstance().removeSession(
304                     mtlsSession);
305             }
306         }
307         else
308         {
309             adaptor.close();
310         }
311     }
312 
313     void completeRequest(crow::Response& thisRes)
314     {
315         if (!req)
316         {
317             return;
318         }
319         res = std::move(thisRes);
320         res.keepAlive(keepAlive);
321 
322         BMCWEB_LOG_INFO << "Response: " << this << ' '
323                         << req->url().encoded_path() << ' ' << res.resultInt()
324                         << " keepalive=" << keepAlive;
325 
326         addSecurityHeaders(*req, res);
327 
328         crow::authentication::cleanupTempSession(*req);
329 
330         if (!isAlive())
331         {
332             // BMCWEB_LOG_DEBUG << this << " delete (socket is closed) " <<
333             // isReading
334             // << ' ' << isWriting;
335             // delete this;
336 
337             // delete lambda with self shared_ptr
338             // to enable connection destruction
339             res.setCompleteRequestHandler(nullptr);
340             return;
341         }
342 
343         res.setHashAndHandleNotModified();
344 
345         if (res.body().empty() && res.jsonValue.is_structured())
346         {
347             using http_helpers::ContentType;
348             std::array<ContentType, 3> allowed{
349                 ContentType::CBOR, ContentType::JSON, ContentType::HTML};
350             ContentType prefered =
351                 getPreferedContentType(req->getHeaderValue("Accept"), allowed);
352 
353             if (prefered == ContentType::HTML)
354             {
355                 prettyPrintJson(res);
356             }
357             else if (prefered == ContentType::CBOR)
358             {
359                 res.addHeader(boost::beast::http::field::content_type,
360                               "application/cbor");
361                 nlohmann::json::to_cbor(res.jsonValue, res.body());
362             }
363             else
364             {
365                 // Technically prefered could also be NoMatch here, but we'd
366                 // like to default to something rather than return 400 for
367                 // backward compatibility.
368                 res.addHeader(boost::beast::http::field::content_type,
369                               "application/json");
370                 res.body() = res.jsonValue.dump(
371                     2, ' ', true, nlohmann::json::error_handler_t::replace);
372             }
373         }
374 
375         if (res.resultInt() >= 400 && res.body().empty())
376         {
377             res.body() = std::string(res.reason());
378         }
379 
380         res.addHeader(boost::beast::http::field::date, getCachedDateStr());
381 
382         doWrite(res);
383 
384         // delete lambda with self shared_ptr
385         // to enable connection destruction
386         res.setCompleteRequestHandler(nullptr);
387     }
388 
389     void readClientIp()
390     {
391         boost::asio::ip::address ip;
392         boost::system::error_code ec = getClientIp(ip);
393         if (ec)
394         {
395             return;
396         }
397         req->ipAddress = ip;
398     }
399 
400     boost::system::error_code getClientIp(boost::asio::ip::address& ip)
401     {
402         boost::system::error_code ec;
403         BMCWEB_LOG_DEBUG << "Fetch the client IP address";
404         boost::asio::ip::tcp::endpoint endpoint =
405             boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec);
406 
407         if (ec)
408         {
409             // If remote endpoint fails keep going. "ClientOriginIPAddress"
410             // will be empty.
411             BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : "
412                              << ec;
413             return ec;
414         }
415         ip = endpoint.address();
416         return ec;
417     }
418 
419   private:
420     void doReadHeaders()
421     {
422         BMCWEB_LOG_DEBUG << this << " doReadHeaders";
423 
424         // Clean up any previous Connection.
425         boost::beast::http::async_read_header(
426             adaptor, buffer, *parser,
427             [this,
428              self(shared_from_this())](const boost::system::error_code& ec,
429                                        std::size_t bytesTransferred) {
430             BMCWEB_LOG_DEBUG << this << " async_read_header "
431                              << bytesTransferred << " Bytes";
432             bool errorWhileReading = false;
433             if (ec)
434             {
435                 errorWhileReading = true;
436                 if (ec == boost::beast::http::error::end_of_stream)
437                 {
438                     BMCWEB_LOG_WARNING
439                         << this << " Error while reading: " << ec.message();
440                 }
441                 else
442                 {
443                     BMCWEB_LOG_ERROR
444                         << this << " Error while reading: " << ec.message();
445                 }
446             }
447             else
448             {
449                 // if the adaptor isn't open anymore, and wasn't handed to a
450                 // websocket, treat as an error
451                 if (!isAlive() &&
452                     !boost::beast::websocket::is_upgrade(parser->get()))
453                 {
454                     errorWhileReading = true;
455                 }
456             }
457 
458             cancelDeadlineTimer();
459 
460             if (errorWhileReading)
461             {
462                 close();
463                 BMCWEB_LOG_DEBUG << this << " from read(1)";
464                 return;
465             }
466 
467             readClientIp();
468 
469             boost::asio::ip::address ip;
470             if (getClientIp(ip))
471             {
472                 BMCWEB_LOG_DEBUG << "Unable to get client IP";
473             }
474 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
475             boost::beast::http::verb method = parser->get().method();
476             userSession = crow::authentication::authenticate(
477                 ip, res, method, parser->get().base(), mtlsSession);
478 
479             bool loggedIn = userSession != nullptr;
480             if (!loggedIn)
481             {
482                 const boost::optional<uint64_t> contentLength =
483                     parser->content_length();
484                 if (contentLength && *contentLength > loggedOutPostBodyLimit)
485                 {
486                     BMCWEB_LOG_DEBUG << "Content length greater than limit "
487                                      << *contentLength;
488                     close();
489                     return;
490                 }
491 
492                 BMCWEB_LOG_DEBUG << "Starting quick deadline";
493             }
494 #endif // BMCWEB_INSECURE_DISABLE_AUTHX
495 
496             if (parser->is_done())
497             {
498                 handle();
499                 return;
500             }
501 
502             doRead();
503             });
504     }
505 
506     void doRead()
507     {
508         BMCWEB_LOG_DEBUG << this << " doRead";
509         startDeadline();
510         boost::beast::http::async_read_some(
511             adaptor, buffer, *parser,
512             [this,
513              self(shared_from_this())](const boost::system::error_code& ec,
514                                        std::size_t bytesTransferred) {
515             BMCWEB_LOG_DEBUG << this << " async_read_some " << bytesTransferred
516                              << " Bytes";
517 
518             if (ec)
519             {
520                 BMCWEB_LOG_ERROR << this
521                                  << " Error while reading: " << ec.message();
522                 close();
523                 BMCWEB_LOG_DEBUG << this << " from read(1)";
524                 return;
525             }
526 
527             // If the user is logged in, allow them to send files incrementally
528             // one piece at a time. If authentication is disabled then there is
529             // no user session hence always allow to send one piece at a time.
530             if (userSession != nullptr)
531             {
532                 cancelDeadlineTimer();
533             }
534             if (!parser->is_done())
535             {
536                 doRead();
537                 return;
538             }
539 
540             cancelDeadlineTimer();
541             handle();
542             });
543     }
544 
545     void doWrite(crow::Response& thisRes)
546     {
547         BMCWEB_LOG_DEBUG << this << " doWrite";
548         thisRes.preparePayload();
549         serializer.emplace(*thisRes.stringResponse);
550         startDeadline();
551         boost::beast::http::async_write(adaptor, *serializer,
552                                         [this, self(shared_from_this())](
553                                             const boost::system::error_code& ec,
554                                             std::size_t bytesTransferred) {
555             BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred
556                              << " bytes";
557 
558             cancelDeadlineTimer();
559 
560             if (ec)
561             {
562                 BMCWEB_LOG_DEBUG << this << " from write(2)";
563                 return;
564             }
565             if (!keepAlive)
566             {
567                 close();
568                 BMCWEB_LOG_DEBUG << this << " from write(1)";
569                 return;
570             }
571 
572             serializer.reset();
573             BMCWEB_LOG_DEBUG << this << " Clearing response";
574             res.clear();
575             parser.emplace(std::piecewise_construct, std::make_tuple());
576             parser->body_limit(httpReqBodyLimit); // reset body limit for
577                                                   // newly created parser
578             buffer.consume(buffer.size());
579 
580             userSession = nullptr;
581 
582             // Destroy the Request via the std::optional
583             req.reset();
584             doReadHeaders();
585         });
586     }
587 
588     void cancelDeadlineTimer()
589     {
590         timer.cancel();
591     }
592 
593     void startDeadline()
594     {
595         // Timer is already started so no further action is required.
596         if (timerStarted)
597         {
598             return;
599         }
600 
601         std::chrono::seconds timeout(15);
602 
603         std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this();
604         timer.expires_after(timeout);
605         timer.async_wait([weakSelf](const boost::system::error_code& ec) {
606             // Note, we are ignoring other types of errors here;  If the timer
607             // failed for any reason, we should still close the connection
608             std::shared_ptr<Connection<Adaptor, Handler>> self =
609                 weakSelf.lock();
610             if (!self)
611             {
612                 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection";
613                 return;
614             }
615 
616             self->timerStarted = false;
617 
618             if (ec == boost::asio::error::operation_aborted)
619             {
620                 // Canceled wait means the path succeeeded.
621                 return;
622             }
623             if (ec)
624             {
625                 BMCWEB_LOG_CRITICAL << self << " timer failed " << ec;
626             }
627 
628             BMCWEB_LOG_WARNING << self << "Connection timed out, closing";
629 
630             self->close();
631         });
632 
633         timerStarted = true;
634         BMCWEB_LOG_DEBUG << this << " timer started";
635     }
636 
637     Adaptor adaptor;
638     Handler* handler;
639     // Making this a std::optional allows it to be efficiently destroyed and
640     // re-created on Connection reset
641     std::optional<
642         boost::beast::http::request_parser<boost::beast::http::string_body>>
643         parser;
644 
645     boost::beast::flat_static_buffer<8192> buffer;
646 
647     std::optional<boost::beast::http::response_serializer<
648         boost::beast::http::string_body>>
649         serializer;
650 
651     std::optional<crow::Request> req;
652     crow::Response res;
653 
654     std::shared_ptr<persistent_data::UserSession> userSession;
655     std::shared_ptr<persistent_data::UserSession> mtlsSession;
656 
657     boost::asio::steady_timer timer;
658 
659     bool keepAlive = true;
660 
661     bool timerStarted = false;
662 
663     std::function<std::string()>& getCachedDateStr;
664 
665     using std::enable_shared_from_this<
666         Connection<Adaptor, Handler>>::shared_from_this;
667 
668     using std::enable_shared_from_this<
669         Connection<Adaptor, Handler>>::weak_from_this;
670 };
671 } // namespace crow
672