xref: /openbmc/bmcweb/http/http_connection.hpp (revision a8d8f9d8)
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 
193         // Fetch the client IP address
194         readClientIp();
195 
196         // Check for HTTP version 1.1.
197         if (thisReq.version() == 11)
198         {
199             if (thisReq.getHeaderValue(boost::beast::http::field::host).empty())
200             {
201                 res.result(boost::beast::http::status::bad_request);
202                 completeRequest(res);
203                 return;
204             }
205         }
206 
207         BMCWEB_LOG_INFO << "Request: "
208                         << " " << this << " HTTP/" << thisReq.version() / 10
209                         << "." << thisReq.version() % 10 << ' '
210                         << thisReq.methodString() << " " << thisReq.target()
211                         << " " << thisReq.ipAddress.to_string();
212 
213         res.isAliveHelper = [this]() -> bool { return isAlive(); };
214 
215         thisReq.ioService = static_cast<decltype(thisReq.ioService)>(
216             &adaptor.get_executor().context());
217 
218         if (res.completed)
219         {
220             completeRequest(res);
221             return;
222         }
223         keepAlive = thisReq.keepAlive();
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         res.keepAlive(keepAlive);
306 
307         BMCWEB_LOG_INFO << "Response: " << this << ' ' << req->url << ' '
308                         << res.resultInt() << " keepalive=" << keepAlive;
309 
310         addSecurityHeaders(*req, res);
311 
312         crow::authentication::cleanupTempSession(*req);
313 
314         if (!isAlive())
315         {
316             // BMCWEB_LOG_DEBUG << this << " delete (socket is closed) " <<
317             // isReading
318             // << ' ' << isWriting;
319             // delete this;
320 
321             // delete lambda with self shared_ptr
322             // to enable connection destruction
323             res.setCompleteRequestHandler(nullptr);
324             return;
325         }
326 
327         res.setHashAndHandleNotModified();
328 
329         if (res.body().empty() && !res.jsonValue.empty())
330         {
331             using http_helpers::ContentType;
332             std::array<ContentType, 3> allowed{
333                 ContentType::CBOR, ContentType::JSON, ContentType::HTML};
334             ContentType prefered =
335                 getPreferedContentType(req->getHeaderValue("Accept"), allowed);
336 
337             if (prefered == ContentType::HTML)
338             {
339                 prettyPrintJson(res);
340             }
341             else if (prefered == ContentType::CBOR)
342             {
343                 res.addHeader(boost::beast::http::field::content_type,
344                               "application/cbor");
345                 nlohmann::json::to_cbor(res.jsonValue, res.body());
346             }
347             else
348             {
349                 // Technically prefered could also be NoMatch here, but we'd
350                 // like to default to something rather than return 400 for
351                 // backward compatibility.
352                 res.addHeader(boost::beast::http::field::content_type,
353                               "application/json");
354                 res.body() = res.jsonValue.dump(
355                     2, ' ', true, nlohmann::json::error_handler_t::replace);
356             }
357         }
358 
359         if (res.resultInt() >= 400 && res.body().empty())
360         {
361             res.body() = std::string(res.reason());
362         }
363 
364         if (res.result() == boost::beast::http::status::no_content)
365         {
366             // Boost beast throws if content is provided on a no-content
367             // response.  Ideally, this would never happen, but in the case that
368             // it does, we don't want to throw.
369             BMCWEB_LOG_CRITICAL
370                 << this << " Response content provided but code was no-content";
371             res.body().clear();
372         }
373 
374         res.addHeader(boost::beast::http::field::date, getCachedDateStr());
375 
376         doWrite(res);
377 
378         // delete lambda with self shared_ptr
379         // to enable connection destruction
380         res.setCompleteRequestHandler(nullptr);
381     }
382 
383     void readClientIp()
384     {
385         boost::asio::ip::address ip;
386         boost::system::error_code ec = getClientIp(ip);
387         if (ec)
388         {
389             return;
390         }
391         req->ipAddress = ip;
392     }
393 
394     boost::system::error_code getClientIp(boost::asio::ip::address& ip)
395     {
396         boost::system::error_code ec;
397         BMCWEB_LOG_DEBUG << "Fetch the client IP address";
398         boost::asio::ip::tcp::endpoint endpoint =
399             boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec);
400 
401         if (ec)
402         {
403             // If remote endpoint fails keep going. "ClientOriginIPAddress"
404             // will be empty.
405             BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : "
406                              << ec;
407             return ec;
408         }
409         ip = endpoint.address();
410         return ec;
411     }
412 
413   private:
414     void doReadHeaders()
415     {
416         BMCWEB_LOG_DEBUG << this << " doReadHeaders";
417 
418         // Clean up any previous Connection.
419         boost::beast::http::async_read_header(
420             adaptor, buffer, *parser,
421             [this,
422              self(shared_from_this())](const boost::system::error_code& ec,
423                                        std::size_t bytesTransferred) {
424             BMCWEB_LOG_DEBUG << this << " async_read_header "
425                              << bytesTransferred << " Bytes";
426             bool errorWhileReading = false;
427             if (ec)
428             {
429                 errorWhileReading = true;
430                 if (ec == boost::beast::http::error::end_of_stream)
431                 {
432                     BMCWEB_LOG_WARNING
433                         << this << " Error while reading: " << ec.message();
434                 }
435                 else
436                 {
437                     BMCWEB_LOG_ERROR
438                         << this << " Error while reading: " << ec.message();
439                 }
440             }
441             else
442             {
443                 // if the adaptor isn't open anymore, and wasn't handed to a
444                 // websocket, treat as an error
445                 if (!isAlive() &&
446                     !boost::beast::websocket::is_upgrade(parser->get()))
447                 {
448                     errorWhileReading = true;
449                 }
450             }
451 
452             cancelDeadlineTimer();
453 
454             if (errorWhileReading)
455             {
456                 close();
457                 BMCWEB_LOG_DEBUG << this << " from read(1)";
458                 return;
459             }
460 
461             readClientIp();
462 
463             boost::asio::ip::address ip;
464             if (getClientIp(ip))
465             {
466                 BMCWEB_LOG_DEBUG << "Unable to get client IP";
467             }
468             sessionIsFromTransport = false;
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(), userSession);
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             doRead();
492             });
493     }
494 
495     void doRead()
496     {
497         BMCWEB_LOG_DEBUG << this << " doRead";
498         startDeadline();
499         boost::beast::http::async_read(adaptor, buffer, *parser,
500                                        [this, self(shared_from_this())](
501                                            const boost::system::error_code& ec,
502                                            std::size_t bytesTransferred) {
503             BMCWEB_LOG_DEBUG << this << " async_read " << bytesTransferred
504                              << " Bytes";
505             cancelDeadlineTimer();
506             if (ec)
507             {
508                 BMCWEB_LOG_ERROR << this
509                                  << " Error while reading: " << ec.message();
510                 close();
511                 BMCWEB_LOG_DEBUG << this << " from read(1)";
512                 return;
513             }
514             handle();
515         });
516     }
517 
518     void doWrite(crow::Response& thisRes)
519     {
520         BMCWEB_LOG_DEBUG << this << " doWrite";
521         thisRes.preparePayload();
522         serializer.emplace(*thisRes.stringResponse);
523         startDeadline();
524         boost::beast::http::async_write(adaptor, *serializer,
525                                         [this, self(shared_from_this())](
526                                             const boost::system::error_code& ec,
527                                             std::size_t bytesTransferred) {
528             BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred
529                              << " bytes";
530 
531             cancelDeadlineTimer();
532 
533             if (ec)
534             {
535                 BMCWEB_LOG_DEBUG << this << " from write(2)";
536                 return;
537             }
538             if (!keepAlive)
539             {
540                 close();
541                 BMCWEB_LOG_DEBUG << this << " from write(1)";
542                 return;
543             }
544 
545             serializer.reset();
546             BMCWEB_LOG_DEBUG << this << " Clearing response";
547             res.clear();
548             parser.emplace(std::piecewise_construct, std::make_tuple());
549             parser->body_limit(httpReqBodyLimit); // reset body limit for
550                                                   // newly created parser
551             buffer.consume(buffer.size());
552 
553             // If the session was built from the transport, we don't need to
554             // clear it.  All other sessions are generated per request.
555             if (!sessionIsFromTransport)
556             {
557                 userSession = nullptr;
558             }
559 
560             // Destroy the Request via the std::optional
561             req.reset();
562             doReadHeaders();
563         });
564     }
565 
566     void cancelDeadlineTimer()
567     {
568         timer.cancel();
569     }
570 
571     void startDeadline()
572     {
573         cancelDeadlineTimer();
574 
575         std::chrono::seconds timeout(15);
576         // allow slow uploads for logged in users
577         bool loggedIn = userSession != nullptr;
578         if (loggedIn)
579         {
580             timeout = std::chrono::seconds(60);
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     bool keepAlive = true;
637 
638     std::function<std::string()>& getCachedDateStr;
639 
640     using std::enable_shared_from_this<
641         Connection<Adaptor, Handler>>::shared_from_this;
642 
643     using std::enable_shared_from_this<
644         Connection<Adaptor, Handler>>::weak_from_this;
645 };
646 } // namespace crow
647