xref: /openbmc/bmcweb/http/http_connection.hpp (revision 8b2521a5)
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 =
49     1024UL * 1024UL * 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.empty())
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         if (res.result() == boost::beast::http::status::no_content)
381         {
382             // Boost beast throws if content is provided on a no-content
383             // response.  Ideally, this would never happen, but in the case that
384             // it does, we don't want to throw.
385             BMCWEB_LOG_CRITICAL
386                 << this << " Response content provided but code was no-content";
387             res.body().clear();
388         }
389 
390         res.addHeader(boost::beast::http::field::date, getCachedDateStr());
391 
392         doWrite(res);
393 
394         // delete lambda with self shared_ptr
395         // to enable connection destruction
396         res.setCompleteRequestHandler(nullptr);
397     }
398 
399     void readClientIp()
400     {
401         boost::asio::ip::address ip;
402         boost::system::error_code ec = getClientIp(ip);
403         if (ec)
404         {
405             return;
406         }
407         req->ipAddress = ip;
408     }
409 
410     boost::system::error_code getClientIp(boost::asio::ip::address& ip)
411     {
412         boost::system::error_code ec;
413         BMCWEB_LOG_DEBUG << "Fetch the client IP address";
414         boost::asio::ip::tcp::endpoint endpoint =
415             boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec);
416 
417         if (ec)
418         {
419             // If remote endpoint fails keep going. "ClientOriginIPAddress"
420             // will be empty.
421             BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : "
422                              << ec;
423             return ec;
424         }
425         ip = endpoint.address();
426         return ec;
427     }
428 
429   private:
430     void doReadHeaders()
431     {
432         BMCWEB_LOG_DEBUG << this << " doReadHeaders";
433 
434         // Clean up any previous Connection.
435         boost::beast::http::async_read_header(
436             adaptor, buffer, *parser,
437             [this,
438              self(shared_from_this())](const boost::system::error_code& ec,
439                                        std::size_t bytesTransferred) {
440             BMCWEB_LOG_DEBUG << this << " async_read_header "
441                              << bytesTransferred << " Bytes";
442             bool errorWhileReading = false;
443             if (ec)
444             {
445                 errorWhileReading = true;
446                 if (ec == boost::beast::http::error::end_of_stream)
447                 {
448                     BMCWEB_LOG_WARNING
449                         << this << " Error while reading: " << ec.message();
450                 }
451                 else
452                 {
453                     BMCWEB_LOG_ERROR
454                         << this << " Error while reading: " << ec.message();
455                 }
456             }
457             else
458             {
459                 // if the adaptor isn't open anymore, and wasn't handed to a
460                 // websocket, treat as an error
461                 if (!isAlive() &&
462                     !boost::beast::websocket::is_upgrade(parser->get()))
463                 {
464                     errorWhileReading = true;
465                 }
466             }
467 
468             cancelDeadlineTimer();
469 
470             if (errorWhileReading)
471             {
472                 close();
473                 BMCWEB_LOG_DEBUG << this << " from read(1)";
474                 return;
475             }
476 
477             readClientIp();
478 
479             boost::asio::ip::address ip;
480             if (getClientIp(ip))
481             {
482                 BMCWEB_LOG_DEBUG << "Unable to get client IP";
483             }
484 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
485             boost::beast::http::verb method = parser->get().method();
486             userSession = crow::authentication::authenticate(
487                 ip, res, method, parser->get().base(), mtlsSession);
488 
489             bool loggedIn = userSession != nullptr;
490             if (!loggedIn)
491             {
492                 const boost::optional<uint64_t> contentLength =
493                     parser->content_length();
494                 if (contentLength && *contentLength > loggedOutPostBodyLimit)
495                 {
496                     BMCWEB_LOG_DEBUG << "Content length greater than limit "
497                                      << *contentLength;
498                     close();
499                     return;
500                 }
501 
502                 BMCWEB_LOG_DEBUG << "Starting quick deadline";
503             }
504 #endif // BMCWEB_INSECURE_DISABLE_AUTHX
505 
506             if (parser->is_done())
507             {
508                 handle();
509                 return;
510             }
511 
512             doRead();
513             });
514     }
515 
516     void doRead()
517     {
518         BMCWEB_LOG_DEBUG << this << " doRead";
519         startDeadline();
520         boost::beast::http::async_read_some(
521             adaptor, buffer, *parser,
522             [this,
523              self(shared_from_this())](const boost::system::error_code& ec,
524                                        std::size_t bytesTransferred) {
525             BMCWEB_LOG_DEBUG << this << " async_read_some " << bytesTransferred
526                              << " Bytes";
527 
528             if (ec)
529             {
530                 BMCWEB_LOG_ERROR << this
531                                  << " Error while reading: " << ec.message();
532                 close();
533                 BMCWEB_LOG_DEBUG << this << " from read(1)";
534                 return;
535             }
536 
537             // If the user is logged in, allow them to send files incrementally
538             // one piece at a time. If authentication is disabled then there is
539             // no user session hence always allow to send one piece at a time.
540             if (userSession != nullptr)
541             {
542                 cancelDeadlineTimer();
543             }
544             if (!parser->is_done())
545             {
546                 doRead();
547                 return;
548             }
549 
550             cancelDeadlineTimer();
551             handle();
552             });
553     }
554 
555     void doWrite(crow::Response& thisRes)
556     {
557         BMCWEB_LOG_DEBUG << this << " doWrite";
558         thisRes.preparePayload();
559         serializer.emplace(*thisRes.stringResponse);
560         startDeadline();
561         boost::beast::http::async_write(adaptor, *serializer,
562                                         [this, self(shared_from_this())](
563                                             const boost::system::error_code& ec,
564                                             std::size_t bytesTransferred) {
565             BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred
566                              << " bytes";
567 
568             cancelDeadlineTimer();
569 
570             if (ec)
571             {
572                 BMCWEB_LOG_DEBUG << this << " from write(2)";
573                 return;
574             }
575             if (!keepAlive)
576             {
577                 close();
578                 BMCWEB_LOG_DEBUG << this << " from write(1)";
579                 return;
580             }
581 
582             serializer.reset();
583             BMCWEB_LOG_DEBUG << this << " Clearing response";
584             res.clear();
585             parser.emplace(std::piecewise_construct, std::make_tuple());
586             parser->body_limit(httpReqBodyLimit); // reset body limit for
587                                                   // newly created parser
588             buffer.consume(buffer.size());
589 
590             userSession = nullptr;
591 
592             // Destroy the Request via the std::optional
593             req.reset();
594             doReadHeaders();
595         });
596     }
597 
598     void cancelDeadlineTimer()
599     {
600         timer.cancel();
601     }
602 
603     void startDeadline()
604     {
605         // Timer is already started so no further action is required.
606         if (timerStarted)
607         {
608             return;
609         }
610 
611         std::chrono::seconds timeout(15);
612 
613         std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this();
614         timer.expires_after(timeout);
615         timer.async_wait([weakSelf](const boost::system::error_code ec) {
616             // Note, we are ignoring other types of errors here;  If the timer
617             // failed for any reason, we should still close the connection
618             std::shared_ptr<Connection<Adaptor, Handler>> self =
619                 weakSelf.lock();
620             if (!self)
621             {
622                 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection";
623                 return;
624             }
625 
626             self->timerStarted = false;
627 
628             if (ec == boost::asio::error::operation_aborted)
629             {
630                 // Canceled wait means the path succeeeded.
631                 return;
632             }
633             if (ec)
634             {
635                 BMCWEB_LOG_CRITICAL << self << " timer failed " << ec;
636             }
637 
638             BMCWEB_LOG_WARNING << self << "Connection timed out, closing";
639 
640             self->close();
641         });
642 
643         timerStarted = true;
644         BMCWEB_LOG_DEBUG << this << " timer started";
645     }
646 
647     Adaptor adaptor;
648     Handler* handler;
649     // Making this a std::optional allows it to be efficiently destroyed and
650     // re-created on Connection reset
651     std::optional<
652         boost::beast::http::request_parser<boost::beast::http::string_body>>
653         parser;
654 
655     boost::beast::flat_static_buffer<8192> buffer;
656 
657     std::optional<boost::beast::http::response_serializer<
658         boost::beast::http::string_body>>
659         serializer;
660 
661     std::optional<crow::Request> req;
662     crow::Response res;
663 
664     std::shared_ptr<persistent_data::UserSession> userSession;
665     std::shared_ptr<persistent_data::UserSession> mtlsSession;
666 
667     boost::asio::steady_timer timer;
668 
669     bool keepAlive = true;
670 
671     bool timerStarted = false;
672 
673     std::function<std::string()>& getCachedDateStr;
674 
675     using std::enable_shared_from_this<
676         Connection<Adaptor, Handler>>::shared_from_this;
677 
678     using std::enable_shared_from_this<
679         Connection<Adaptor, Handler>>::weak_from_this;
680 };
681 } // namespace crow
682