xref: /openbmc/bmcweb/http/http_connection.hpp (revision 141d9431)
1 #pragma once
2 #include "bmcweb_config.h"
3 
4 #include "authorization.hpp"
5 #include "http_response.hpp"
6 #include "http_utility.hpp"
7 #include "logging.hpp"
8 #include "utility.hpp"
9 
10 #include <boost/algorithm/string.hpp>
11 #include <boost/algorithm/string/predicate.hpp>
12 #include <boost/asio/io_context.hpp>
13 #include <boost/asio/ip/tcp.hpp>
14 #include <boost/asio/ssl/stream.hpp>
15 #include <boost/asio/steady_timer.hpp>
16 #include <boost/beast/core/flat_static_buffer.hpp>
17 #include <boost/beast/ssl/ssl_stream.hpp>
18 #include <boost/beast/websocket.hpp>
19 #include <boost/url/url_view.hpp>
20 #include <json_html_serializer.hpp>
21 #include <security_headers.hpp>
22 #include <ssl_key_handler.hpp>
23 
24 #include <atomic>
25 #include <chrono>
26 #include <vector>
27 
28 namespace crow
29 {
30 
31 inline void prettyPrintJson(crow::Response& res)
32 {
33     json_html_util::dumpHtml(res.body(), res.jsonValue);
34 
35     res.addHeader("Content-Type", "text/html;charset=UTF-8");
36 }
37 
38 static int connectionCount = 0;
39 
40 // request body limit size set by the bmcwebHttpReqBodyLimitMb option
41 constexpr uint64_t httpReqBodyLimit =
42     1024UL * 1024UL * bmcwebHttpReqBodyLimitMb;
43 
44 constexpr uint64_t loggedOutPostBodyLimit = 4096;
45 
46 constexpr uint32_t httpHeaderLimit = 8192;
47 
48 template <typename Adaptor, typename Handler>
49 class Connection :
50     public std::enable_shared_from_this<Connection<Adaptor, Handler>>
51 {
52   public:
53     Connection(Handler* handlerIn, boost::asio::steady_timer&& timerIn,
54                std::function<std::string()>& getCachedDateStrF,
55                Adaptor adaptorIn) :
56         adaptor(std::move(adaptorIn)),
57         handler(handlerIn), timer(std::move(timerIn)),
58         getCachedDateStr(getCachedDateStrF)
59     {
60         parser.emplace(std::piecewise_construct, std::make_tuple());
61         parser->body_limit(httpReqBodyLimit);
62         parser->header_limit(httpHeaderLimit);
63 
64 #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
65         prepareMutualTls();
66 #endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
67 
68         connectionCount++;
69 
70         BMCWEB_LOG_DEBUG << this << " Connection open, total "
71                          << connectionCount;
72     }
73 
74     ~Connection()
75     {
76         res.setCompleteRequestHandler(nullptr);
77         cancelDeadlineTimer();
78 
79         connectionCount--;
80         BMCWEB_LOG_DEBUG << this << " Connection closed, total "
81                          << connectionCount;
82     }
83 
84     Connection(const Connection&) = delete;
85     Connection(Connection&&) = delete;
86     Connection& operator=(const Connection&) = delete;
87     Connection& operator=(Connection&&) = delete;
88 
89     void prepareMutualTls()
90     {
91         std::error_code error;
92         std::filesystem::path caPath(ensuressl::trustStorePath);
93         auto caAvailable = !std::filesystem::is_empty(caPath, error);
94         caAvailable = caAvailable && !error;
95         if (caAvailable && persistent_data::SessionStore::getInstance()
96                                .getAuthMethodsConfig()
97                                .tls)
98         {
99             adaptor.set_verify_mode(boost::asio::ssl::verify_peer);
100             std::string id = "bmcweb";
101 
102             const char* cStr = id.c_str();
103             // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
104             const auto* idC = reinterpret_cast<const unsigned char*>(cStr);
105             int ret = SSL_set_session_id_context(
106                 adaptor.native_handle(), idC,
107                 static_cast<unsigned int>(id.length()));
108             if (ret == 0)
109             {
110                 BMCWEB_LOG_ERROR << this << " failed to set SSL id";
111             }
112         }
113 
114         adaptor.set_verify_callback([this](
115                                         bool preverified,
116                                         boost::asio::ssl::verify_context& ctx) {
117             // do nothing if TLS is disabled
118             if (!persistent_data::SessionStore::getInstance()
119                      .getAuthMethodsConfig()
120                      .tls)
121             {
122                 BMCWEB_LOG_DEBUG << this << " TLS auth_config is disabled";
123                 return true;
124             }
125 
126             // We always return true to allow full auth flow
127             if (!preverified)
128             {
129                 BMCWEB_LOG_DEBUG << this << " TLS preverification failed.";
130                 return true;
131             }
132 
133             X509_STORE_CTX* cts = ctx.native_handle();
134             if (cts == nullptr)
135             {
136                 BMCWEB_LOG_DEBUG << this << " Cannot get native TLS handle.";
137                 return true;
138             }
139 
140             // Get certificate
141             X509* peerCert =
142                 X509_STORE_CTX_get_current_cert(ctx.native_handle());
143             if (peerCert == nullptr)
144             {
145                 BMCWEB_LOG_DEBUG << this
146                                  << " Cannot get current TLS certificate.";
147                 return true;
148             }
149 
150             // Check if certificate is OK
151             int error = X509_STORE_CTX_get_error(cts);
152             if (error != X509_V_OK)
153             {
154                 BMCWEB_LOG_INFO << this << " Last TLS error is: " << error;
155                 return true;
156             }
157             // Check that we have reached final certificate in chain
158             int32_t depth = X509_STORE_CTX_get_error_depth(cts);
159             if (depth != 0)
160 
161             {
162                 BMCWEB_LOG_DEBUG
163                     << this << " Certificate verification in progress (depth "
164                     << depth << "), waiting to reach final depth";
165                 return true;
166             }
167 
168             BMCWEB_LOG_DEBUG << this
169                              << " Certificate verification of final depth";
170 
171             // Verify KeyUsage
172             bool isKeyUsageDigitalSignature = false;
173             bool isKeyUsageKeyAgreement = false;
174 
175             ASN1_BIT_STRING* usage = static_cast<ASN1_BIT_STRING*>(
176                 X509_get_ext_d2i(peerCert, NID_key_usage, nullptr, nullptr));
177 
178             if (usage == nullptr)
179             {
180                 BMCWEB_LOG_DEBUG << this << " TLS usage is null";
181                 return true;
182             }
183 
184             for (int i = 0; i < usage->length; i++)
185             {
186                 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
187                 unsigned char usageChar = usage->data[i];
188                 if (KU_DIGITAL_SIGNATURE & usageChar)
189                 {
190                     isKeyUsageDigitalSignature = true;
191                 }
192                 if (KU_KEY_AGREEMENT & usageChar)
193                 {
194                     isKeyUsageKeyAgreement = true;
195                 }
196             }
197             ASN1_BIT_STRING_free(usage);
198 
199             if (!isKeyUsageDigitalSignature || !isKeyUsageKeyAgreement)
200             {
201                 BMCWEB_LOG_DEBUG << this
202                                  << " Certificate ExtendedKeyUsage does "
203                                     "not allow provided certificate to "
204                                     "be used for user authentication";
205                 return true;
206             }
207 
208             // Determine that ExtendedKeyUsage includes Client Auth
209 
210             stack_st_ASN1_OBJECT* extUsage =
211                 static_cast<stack_st_ASN1_OBJECT*>(X509_get_ext_d2i(
212                     peerCert, NID_ext_key_usage, nullptr, nullptr));
213 
214             if (extUsage == nullptr)
215             {
216                 BMCWEB_LOG_DEBUG << this << " TLS extUsage is null";
217                 return true;
218             }
219 
220             bool isExKeyUsageClientAuth = false;
221             for (int i = 0; i < sk_ASN1_OBJECT_num(extUsage); i++)
222             {
223                 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast)
224                 int nid = OBJ_obj2nid(sk_ASN1_OBJECT_value(extUsage, i));
225                 if (NID_client_auth == nid)
226                 {
227                     isExKeyUsageClientAuth = true;
228                     break;
229                 }
230             }
231             sk_ASN1_OBJECT_free(extUsage);
232 
233             // Certificate has to have proper key usages set
234             if (!isExKeyUsageClientAuth)
235             {
236                 BMCWEB_LOG_DEBUG << this
237                                  << " Certificate ExtendedKeyUsage does "
238                                     "not allow provided certificate to "
239                                     "be used for user authentication";
240                 return true;
241             }
242             std::string sslUser;
243             // Extract username contained in CommonName
244             sslUser.resize(256, '\0');
245 
246             int status = X509_NAME_get_text_by_NID(
247                 X509_get_subject_name(peerCert), NID_commonName, sslUser.data(),
248                 static_cast<int>(sslUser.size()));
249 
250             if (status == -1)
251             {
252                 BMCWEB_LOG_DEBUG
253                     << this << " TLS cannot get username to create session";
254                 return true;
255             }
256 
257             size_t lastChar = sslUser.find('\0');
258             if (lastChar == std::string::npos || lastChar == 0)
259             {
260                 BMCWEB_LOG_DEBUG << this << " Invalid TLS user name";
261                 return true;
262             }
263             sslUser.resize(lastChar);
264             std::string unsupportedClientId = "";
265             sessionIsFromTransport = true;
266             userSession = persistent_data::SessionStore::getInstance()
267                               .generateUserSession(
268                                   sslUser, req->ipAddress, unsupportedClientId,
269                                   persistent_data::PersistenceType::TIMEOUT);
270             if (userSession != nullptr)
271             {
272                 BMCWEB_LOG_DEBUG
273                     << this
274                     << " Generating TLS session: " << userSession->uniqueId;
275             }
276             return true;
277         });
278     }
279 
280     Adaptor& socket()
281     {
282         return adaptor;
283     }
284 
285     void start()
286     {
287         if (connectionCount >= 100)
288         {
289             BMCWEB_LOG_CRITICAL << this << "Max connection count exceeded.";
290             return;
291         }
292 
293         startDeadline();
294 
295         // TODO(ed) Abstract this to a more clever class with the idea of an
296         // asynchronous "start"
297         if constexpr (std::is_same_v<Adaptor,
298                                      boost::beast::ssl_stream<
299                                          boost::asio::ip::tcp::socket>>)
300         {
301             adaptor.async_handshake(boost::asio::ssl::stream_base::server,
302                                     [this, self(shared_from_this())](
303                                         const boost::system::error_code& ec) {
304                                         if (ec)
305                                         {
306                                             return;
307                                         }
308                                         doReadHeaders();
309                                     });
310         }
311         else
312         {
313             doReadHeaders();
314         }
315     }
316 
317     void handle()
318     {
319         std::error_code reqEc;
320         crow::Request& thisReq = req.emplace(parser->release(), reqEc);
321         if (reqEc)
322         {
323             BMCWEB_LOG_DEBUG << "Request failed to construct" << reqEc;
324             return;
325         }
326         thisReq.session = userSession;
327 
328         // Fetch the client IP address
329         readClientIp();
330 
331         // Check for HTTP version 1.1.
332         if (thisReq.version() == 11)
333         {
334             if (thisReq.getHeaderValue(boost::beast::http::field::host).empty())
335             {
336                 res.result(boost::beast::http::status::bad_request);
337                 completeRequest();
338                 return;
339             }
340         }
341 
342         BMCWEB_LOG_INFO << "Request: "
343                         << " " << this << " HTTP/" << thisReq.version() / 10
344                         << "." << thisReq.version() % 10 << ' '
345                         << thisReq.methodString() << " " << thisReq.target()
346                         << " " << thisReq.ipAddress;
347 
348         res.setCompleteRequestHandler(nullptr);
349         res.isAliveHelper = [this]() -> bool { return isAlive(); };
350 
351         thisReq.ioService = static_cast<decltype(thisReq.ioService)>(
352             &adaptor.get_executor().context());
353 
354         if (res.completed)
355         {
356             completeRequest();
357             return;
358         }
359 #ifndef BMCWEB_INSECURE_DISABLE_AUTHENTICATION
360         if (!crow::authorization::isOnAllowlist(req->url, req->method()) &&
361             thisReq.session == nullptr)
362         {
363             BMCWEB_LOG_WARNING << "[AuthMiddleware] authorization failed";
364             forward_unauthorized::sendUnauthorized(
365                 req->url, req->getHeaderValue("User-Agent"),
366                 req->getHeaderValue("Accept"), res);
367             completeRequest();
368             return;
369         }
370 #endif // BMCWEB_INSECURE_DISABLE_AUTHENTICATION
371         res.setCompleteRequestHandler([self(shared_from_this())] {
372             boost::asio::post(self->adaptor.get_executor(),
373                               [self] { self->completeRequest(); });
374         });
375 
376         if (thisReq.isUpgrade() &&
377             boost::iequals(
378                 thisReq.getHeaderValue(boost::beast::http::field::upgrade),
379                 "websocket"))
380         {
381             handler->handleUpgrade(thisReq, res, std::move(adaptor));
382             // delete lambda with self shared_ptr
383             // to enable connection destruction
384             res.setCompleteRequestHandler(nullptr);
385             return;
386         }
387         auto asyncResp = std::make_shared<bmcweb::AsyncResp>(res);
388         handler->handle(thisReq, asyncResp);
389     }
390 
391     bool isAlive()
392     {
393         if constexpr (std::is_same_v<Adaptor,
394                                      boost::beast::ssl_stream<
395                                          boost::asio::ip::tcp::socket>>)
396         {
397             return adaptor.next_layer().is_open();
398         }
399         else
400         {
401             return adaptor.is_open();
402         }
403     }
404     void close()
405     {
406         if constexpr (std::is_same_v<Adaptor,
407                                      boost::beast::ssl_stream<
408                                          boost::asio::ip::tcp::socket>>)
409         {
410             adaptor.next_layer().close();
411 #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
412             if (userSession != nullptr)
413             {
414                 BMCWEB_LOG_DEBUG
415                     << this
416                     << " Removing TLS session: " << userSession->uniqueId;
417                 persistent_data::SessionStore::getInstance().removeSession(
418                     userSession);
419             }
420 #endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
421         }
422         else
423         {
424             adaptor.close();
425         }
426     }
427 
428     void completeRequest()
429     {
430         if (!req)
431         {
432             return;
433         }
434         BMCWEB_LOG_INFO << "Response: " << this << ' ' << req->url << ' '
435                         << res.resultInt() << " keepalive=" << req->keepAlive();
436 
437         addSecurityHeaders(*req, res);
438 
439         crow::authorization::cleanupTempSession(*req);
440 
441         if (!isAlive())
442         {
443             // BMCWEB_LOG_DEBUG << this << " delete (socket is closed) " <<
444             // isReading
445             // << ' ' << isWriting;
446             // delete this;
447 
448             // delete lambda with self shared_ptr
449             // to enable connection destruction
450             res.setCompleteRequestHandler(nullptr);
451             return;
452         }
453         if (res.body().empty() && !res.jsonValue.empty())
454         {
455             if (http_helpers::requestPrefersHtml(req->getHeaderValue("Accept")))
456             {
457                 prettyPrintJson(res);
458             }
459             else
460             {
461                 res.jsonMode();
462                 res.body() = res.jsonValue.dump(
463                     2, ' ', true, nlohmann::json::error_handler_t::replace);
464             }
465         }
466 
467         if (res.resultInt() >= 400 && res.body().empty())
468         {
469             res.body() = std::string(res.reason());
470         }
471 
472         if (res.result() == boost::beast::http::status::no_content)
473         {
474             // Boost beast throws if content is provided on a no-content
475             // response.  Ideally, this would never happen, but in the case that
476             // it does, we don't want to throw.
477             BMCWEB_LOG_CRITICAL
478                 << this << " Response content provided but code was no-content";
479             res.body().clear();
480         }
481 
482         res.addHeader(boost::beast::http::field::date, getCachedDateStr());
483 
484         res.keepAlive(req->keepAlive());
485 
486         doWrite();
487 
488         // delete lambda with self shared_ptr
489         // to enable connection destruction
490         res.setCompleteRequestHandler(nullptr);
491     }
492 
493     void readClientIp()
494     {
495         boost::asio::ip::address ip;
496         boost::system::error_code ec = getClientIp(ip);
497         if (ec)
498         {
499             return;
500         }
501         req->ipAddress = ip;
502     }
503 
504     boost::system::error_code getClientIp(boost::asio::ip::address& ip)
505     {
506         boost::system::error_code ec;
507         BMCWEB_LOG_DEBUG << "Fetch the client IP address";
508         boost::asio::ip::tcp::endpoint endpoint =
509             boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec);
510 
511         if (ec)
512         {
513             // If remote endpoint fails keep going. "ClientOriginIPAddress"
514             // will be empty.
515             BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : "
516                              << ec;
517             return ec;
518         }
519         ip = endpoint.address();
520         return ec;
521     }
522 
523   private:
524     void doReadHeaders()
525     {
526         BMCWEB_LOG_DEBUG << this << " doReadHeaders";
527 
528         // Clean up any previous Connection.
529         boost::beast::http::async_read_header(
530             adaptor, buffer, *parser,
531             [this,
532              self(shared_from_this())](const boost::system::error_code& ec,
533                                        std::size_t bytesTransferred) {
534                 BMCWEB_LOG_DEBUG << this << " async_read_header "
535                                  << bytesTransferred << " Bytes";
536                 bool errorWhileReading = false;
537                 if (ec)
538                 {
539                     errorWhileReading = true;
540                     if (ec == boost::asio::error::eof)
541                     {
542                         BMCWEB_LOG_WARNING
543                             << this << " Error while reading: " << ec.message();
544                     }
545                     else
546                     {
547                         BMCWEB_LOG_ERROR
548                             << this << " Error while reading: " << ec.message();
549                     }
550                 }
551                 else
552                 {
553                     // if the adaptor isn't open anymore, and wasn't handed to a
554                     // websocket, treat as an error
555                     if (!isAlive() &&
556                         !boost::beast::websocket::is_upgrade(parser->get()))
557                     {
558                         errorWhileReading = true;
559                     }
560                 }
561 
562                 cancelDeadlineTimer();
563 
564                 if (errorWhileReading)
565                 {
566                     close();
567                     BMCWEB_LOG_DEBUG << this << " from read(1)";
568                     return;
569                 }
570 
571                 readClientIp();
572 
573                 boost::asio::ip::address ip;
574                 if (getClientIp(ip))
575                 {
576                     BMCWEB_LOG_DEBUG << "Unable to get client IP";
577                 }
578                 sessionIsFromTransport = false;
579 #ifndef BMCWEB_INSECURE_DISABLE_AUTHENTICATION
580                 boost::beast::http::verb method = parser->get().method();
581                 userSession = crow::authorization::authenticate(
582                     ip, res, method, parser->get().base(), userSession);
583 
584                 bool loggedIn = userSession != nullptr;
585                 if (!loggedIn)
586                 {
587                     const boost::optional<uint64_t> contentLength =
588                         parser->content_length();
589                     if (contentLength &&
590                         *contentLength > loggedOutPostBodyLimit)
591                     {
592                         BMCWEB_LOG_DEBUG << "Content length greater than limit "
593                                          << *contentLength;
594                         close();
595                         return;
596                     }
597 
598                     BMCWEB_LOG_DEBUG << "Starting quick deadline";
599                 }
600 #endif // BMCWEB_INSECURE_DISABLE_AUTHENTICATION
601 
602                 doRead();
603             });
604     }
605 
606     void doRead()
607     {
608         BMCWEB_LOG_DEBUG << this << " doRead";
609         startDeadline();
610         boost::beast::http::async_read(
611             adaptor, buffer, *parser,
612             [this,
613              self(shared_from_this())](const boost::system::error_code& ec,
614                                        std::size_t bytesTransferred) {
615                 BMCWEB_LOG_DEBUG << this << " async_read " << bytesTransferred
616                                  << " Bytes";
617                 cancelDeadlineTimer();
618                 if (ec)
619                 {
620                     BMCWEB_LOG_ERROR
621                         << this << " Error while reading: " << ec.message();
622                     close();
623                     BMCWEB_LOG_DEBUG << this << " from read(1)";
624                     return;
625                 }
626                 handle();
627             });
628     }
629 
630     void doWrite()
631     {
632         BMCWEB_LOG_DEBUG << this << " doWrite";
633         res.preparePayload();
634         serializer.emplace(*res.stringResponse);
635         startDeadline();
636         boost::beast::http::async_write(
637             adaptor, *serializer,
638             [this,
639              self(shared_from_this())](const boost::system::error_code& ec,
640                                        std::size_t bytesTransferred) {
641                 BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred
642                                  << " bytes";
643 
644                 cancelDeadlineTimer();
645 
646                 if (ec)
647                 {
648                     BMCWEB_LOG_DEBUG << this << " from write(2)";
649                     return;
650                 }
651                 if (!res.keepAlive())
652                 {
653                     close();
654                     BMCWEB_LOG_DEBUG << this << " from write(1)";
655                     return;
656                 }
657 
658                 serializer.reset();
659                 BMCWEB_LOG_DEBUG << this << " Clearing response";
660                 res.clear();
661                 parser.emplace(std::piecewise_construct, std::make_tuple());
662                 parser->body_limit(httpReqBodyLimit); // reset body limit for
663                                                       // newly created parser
664                 buffer.consume(buffer.size());
665 
666                 // If the session was built from the transport, we don't need to
667                 // clear it.  All other sessions are generated per request.
668                 if (!sessionIsFromTransport)
669                 {
670                     userSession = nullptr;
671                 }
672 
673                 // Destroy the Request via the std::optional
674                 req.reset();
675                 doReadHeaders();
676             });
677     }
678 
679     void cancelDeadlineTimer()
680     {
681         timer.cancel();
682     }
683 
684     void startDeadline()
685     {
686         cancelDeadlineTimer();
687 
688         std::chrono::seconds timeout(15);
689         // allow slow uploads for logged in users
690         bool loggedIn = userSession != nullptr;
691         if (loggedIn)
692         {
693             timeout = std::chrono::seconds(60);
694             return;
695         }
696 
697         std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this();
698         timer.expires_after(timeout);
699         timer.async_wait([weakSelf](const boost::system::error_code ec) {
700             // Note, we are ignoring other types of errors here;  If the timer
701             // failed for any reason, we should still close the connection
702 
703             std::shared_ptr<Connection<Adaptor, Handler>> self =
704                 weakSelf.lock();
705             if (!self)
706             {
707                 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection";
708                 return;
709             }
710             if (ec == boost::asio::error::operation_aborted)
711             {
712                 // Canceled wait means the path succeeeded.
713                 return;
714             }
715             if (ec)
716             {
717                 BMCWEB_LOG_CRITICAL << self << " timer failed " << ec;
718             }
719 
720             BMCWEB_LOG_WARNING << self << "Connection timed out, closing";
721 
722             self->close();
723         });
724 
725         BMCWEB_LOG_DEBUG << this << " timer started";
726     }
727 
728   private:
729     Adaptor adaptor;
730     Handler* handler;
731     // Making this a std::optional allows it to be efficiently destroyed and
732     // re-created on Connection reset
733     std::optional<
734         boost::beast::http::request_parser<boost::beast::http::string_body>>
735         parser;
736 
737     boost::beast::flat_static_buffer<8192> buffer;
738 
739     std::optional<boost::beast::http::response_serializer<
740         boost::beast::http::string_body>>
741         serializer;
742 
743     std::optional<crow::Request> req;
744     crow::Response res;
745 
746     bool sessionIsFromTransport = false;
747     std::shared_ptr<persistent_data::UserSession> userSession;
748 
749     boost::asio::steady_timer timer;
750 
751     std::function<std::string()>& getCachedDateStr;
752 
753     using std::enable_shared_from_this<
754         Connection<Adaptor, Handler>>::shared_from_this;
755 
756     using std::enable_shared_from_this<
757         Connection<Adaptor, Handler>>::weak_from_this;
758 };
759 } // namespace crow
760