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