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